Browse Source

first commit

apple 1 month ago
commit
a06e36d81f
100 changed files with 52734 additions and 0 deletions
  1. BIN
      .DS_Store
  2. 3 0
      .vscode/settings.json
  3. 67 0
      examples/README.md
  4. 40 0
      examples/html-template.html
  5. 288 0
      examples/test.html
  6. 191 0
      examples/test1.html
  7. 435 0
      examples/test2.html
  8. 238 0
      examples/upload-image-demo.html
  9. BIN
      js/.DS_Store
  10. 152 0
      js/PageJs/infoHomep.js
  11. 0 0
      js/PageJs/mainMenuDefHome.js
  12. 146 0
      js/PageJs/taskHomep.js
  13. 1 0
      js/PageJs/urgeHomep.js
  14. 0 0
      js/base.js
  15. 31 0
      js/cms.js
  16. 1 0
      js/common.js
  17. 0 0
      js/display.js
  18. 0 0
      js/edit.js
  19. 75 0
      js/form.js
  20. 14631 0
      js/jodit/ace.js
  21. 0 0
      js/jodit/beautify-html.min.js
  22. 0 0
      js/jodit/beautify.min.js
  23. 9 0
      js/jodit/jodit.js
  24. 0 0
      js/jodit/mode-html.js
  25. 0 0
      js/jodit/theme-idle_fingers.js
  26. 1 0
      js/mp_axios/axios.min.js
  27. 149 0
      js/mp_base/base.js
  28. 474 0
      js/mp_bridge/bridge.js
  29. 244 0
      js/mp_common/common.js
  30. 9 0
      js/mp_cropper/cropper.min.js
  31. 0 0
      js/mp_dayjs/dayjs.min.js
  32. 1 0
      js/mp_jq/jquery1.11.3.min.js
  33. 0 0
      js/mp_jweixin/jweixin-1.6.0.js
  34. 418 0
      js/mp_request/request.js
  35. 2747 0
      js/mp_ss_components/mp-ss-components.js
  36. 104 0
      js/mp_toast/toast.js
  37. 182 0
      js/mp_user_api/user-api.js
  38. 469 0
      js/mp_utils/field-formatter.js
  39. 221 0
      js/mp_utils/navigation.js
  40. 375 0
      js/mp_validation/validation-manager.js
  41. 137 0
      js/mp_validator/validator-rules.js
  42. 0 0
      js/mp_vant/vant.min.js
  43. 282 0
      js/mp_vue/vue.global.js
  44. 138 0
      js/vue/EventBus.js
  45. 233 0
      js/vue/icon-config.js
  46. 6142 0
      js/vue/ss-components.js
  47. 316 0
      js/vue/ss-echarts-compnents.js
  48. 1156 0
      js/vue/ss-index-components.js
  49. 203 0
      js/vue/tools.js
  50. 3854 0
      js/vue/vue-router.gloabl.js
  51. 268 0
      js/vue/vue3.js
  52. 0 0
      js/wdDrag.js
  53. 441 0
      page/autoLogin.html
  54. 848 0
      page/bjdm_bzrDm.html
  55. 949 0
      page/login.html
  56. 216 0
      page/mp_addSure.html
  57. 682 0
      page/mp_ccChk.html
  58. 208 0
      page/mp_clClyd_baseInfo.html
  59. 236 0
      page/mp_clyy_details.html
  60. 340 0
      page/mp_clyy_form.html
  61. 467 0
      page/mp_clyy_inp.html
  62. 112 0
      page/mp_objInfo.html
  63. 499 0
      page/mp_objInp.html
  64. 1261 0
      page/mp_objList.html
  65. 440 0
      page/mp_rcXcdjl_excelZxxzAdd.html
  66. 485 0
      page/mp_shList.html
  67. 227 0
      page/mp_xyqj_baseInfo.html
  68. 217 0
      page/mp_xyqj_inp.html
  69. BIN
      skin/.DS_Store
  70. 6914 0
      skin/easy/css/base.css
  71. 305 0
      skin/easy/css/cropper.css
  72. 0 0
      skin/easy/css/element-plus.css
  73. 126 0
      skin/easy/css/font_4273728_tnvxftfb8j.css
  74. 750 0
      skin/easy/css/font_4279221_5h4vbt6831w.css
  75. 984 0
      skin/easy/css/iconfont.css
  76. 1 0
      skin/easy/css/jodit.css
  77. 36 0
      skin/easy/css/var.css
  78. 1051 0
      skin/easy/dlg.css
  79. BIN
      skin/easy/fonts/.smbdeleteAAAa00000012eea9
  80. BIN
      skin/easy/fonts/iconfont.ttf
  81. BIN
      skin/easy/fonts/iconfont.woff
  82. BIN
      skin/easy/fonts/iconfont.woff2
  83. BIN
      skin/easy/fonts/iconfont_4273728.woff2
  84. BIN
      skin/easy/fonts/iconfont_4279221.woff2
  85. 508 0
      skin/easy/gate.css
  86. BIN
      skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/)]BDJ_U}%HB2`9(~5X[_3H6.gif
  87. BIN
      skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/0iocntuq0mw77.gif
  88. BIN
      skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/1~CK74Y6~VGAWKZ0]3$9S@I.jpg
  89. BIN
      skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/2c06a90652cac5f927a377a7ee1fe333_5bad6ca91bd0b.gif
  90. BIN
      skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/3c7e711ca2adea83e957467144ce9d2e_5a557e794f261.gif
  91. BIN
      skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/3f27e969531aab06b0dbc7d1521aec99_b158b81479e745b4ab28925d38bd54bc.gif
  92. BIN
      skin/easy/help/PC/29d/c3c/dc6/da1/398/eb3/42c/c18/c11/ca3/01/9.png
  93. BIN
      skin/easy/help/PC/7f40ef61/565a6831/efb01283/7adbb1db/9ce6efb31aa5c42b3329ce03aaf5aae9_5b8b5b7301a57.gif
  94. BIN
      skin/easy/help/PC/ry/0fjcklyqviz95.gif
  95. BIN
      skin/easy/help/PC/ry/25def9e1846aab73ceca6d2d959ea08e_5b513d51c5b39.gif
  96. BIN
      skin/easy/help/PC/ry/XQADGBVNXRFCUZNQ`25UBC.jpg
  97. BIN
      skin/easy/help/PC/ry/mmexport1552022002827.jpg
  98. BIN
      skin/easy/help/PC/ry/喵喵2.gif
  99. BIN
      skin/easy/help/PC/ry/微信图片_20171012135344.png
  100. BIN
      skin/easy/help/PC/ws/20150816193536_AkCru.jpeg

BIN
.DS_Store


+ 3 - 0
.vscode/settings.json

@@ -0,0 +1,3 @@
+{
+    "liveServer.settings.port": 5501
+}

+ 67 - 0
examples/README.md

@@ -0,0 +1,67 @@
+# H5与小程序通信示例
+
+这个文件夹包含了H5页面与小程序通信的完整示例。
+
+## 📁 文件说明
+
+### 页面文件
+- `test.html` - 主测试页面,包含所有通信功能
+- `test1.html` - 简单跳转页面示例
+- `test2.html` - 表单页面示例(包含日志功能)
+
+### 工具库
+- `../js/utils/h5-bridge.js` - H5与小程序通信工具函数库
+
+## 🎯 功能演示
+
+### test.html - 主要功能
+- 📷 **扫码** - 调用小程序原生扫码功能
+- 🖼️ **选择图片** - 调用小程序原生图片选择
+- 📤 **H5图片上传** - 纯H5实现的图片上传
+- 📎 **H5附件上传** - 纯H5实现的文件上传
+- 📞 **拨打电话** - 调用小程序拨号功能
+- 💾 **存储操作** - 小程序本地存储读写
+- 🔙 **返回小程序** - 智能返回功能
+
+### test1.html - 页面跳转
+- 简单的页面间跳转示例
+- 演示不需要结果处理的场景
+
+### test2.html - 表单应用
+- 完整的表单页面示例
+- 扫码结果填入表单
+- 图片选择添加到表单
+- 表单数据持久化
+- 操作日志记录
+
+## 🔧 技术特点
+
+### 通信机制
+- 使用中央控制器模式
+- 支持数据双向传递
+- 完整的错误处理
+
+### 两种上传方式
+1. **小程序原生上传** - 调用小程序API,需要页面跳转
+2. **H5原生上传** - 纯前端实现,无需跳转,体验更好
+
+### 函数库设计
+- 简单的函数调用,无需类初始化
+- 统一的错误处理
+- 灵活的回调机制
+
+## 🚀 使用方法
+
+1. 在小程序中配置webview页面
+2. 引入h5-bridge.js工具库
+3. 调用相应的通信函数
+4. 在handleResult中处理返回结果
+
+## 📱 测试建议
+
+建议在真实的微信小程序环境中测试:
+- H5上传功能的兼容性
+- 各种原生功能的调用
+- 页面跳转和数据传递
+
+这些示例为正式业务开发提供了完整的技术基础。

+ 40 - 0
examples/html-template.html

@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>预约详情</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+            display: flex;
+            flex-direction: column;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                    }
+                }
+            })
+        })
+    </script>
+</body>

+ 288 - 0
examples/test.html

@@ -0,0 +1,288 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>H5通信测试</title>
+    <!-- 微信 JS-SDK -->
+    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
+    <!-- Vue 3 CDN -->
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <!-- H5工具类 -->
+    <script src="../js/utils/h5-bridge.js"></script>
+</head>
+<body>
+    <div id="app">
+        <div class="container">
+            <h1>🎛️ H5通信功能测试</h1>
+            <p>测试H5页面与小程序的各种通信功能</p>
+
+            <div class="button-group">
+                <button class="btn primary" @click="goToTest1">
+                    ➡️ 进入Test1
+                </button>
+                <button class="btn secondary" @click="handleScanCode">
+                    📷 扫码
+                </button>
+                <button class="btn success" @click="handleChooseImage">
+                    🖼️ 选择图片
+                </button>
+                <button class="btn warning" @click="handleH5UploadImage">
+                    📤 H5图片上传
+                </button>
+                <button class="btn purple" @click="handleH5UploadFile">
+                    📎 H5附件上传
+                </button>
+
+                <button class="btn info" @click="handleSetStorage">
+                    💾 设置存储
+                </button>
+                <button class="btn info" @click="handleGetStorage">
+                    📥 获取存储
+                </button>
+                <button class="btn danger" @click="handleGoBack">
+                    🔙 返回小程序
+                </button>
+
+            </div>
+
+
+        </div>
+    </div>
+
+    <script>
+        const { createApp } = Vue
+
+        createApp({
+            data() {
+                return {}
+            },
+
+            mounted() {
+                // 初始化页面,使用自定义结果处理
+                initPage('test', this.handleResult)
+            },
+
+            methods: {
+                // 处理操作结果
+                handleResult(data) {
+                    const { action, success, result, error, message, key, value, found, phoneNumber, count, paths, files } = data
+
+                    if (success) {
+                        switch (action) {
+                            case 'scanCode':
+                                console.log(`📷 扫码成功: ${result}`)
+                                alert(`扫码结果: ${result}`)
+                                break
+                            case 'chooseImage':
+                                console.log(`🖼️ 选择图片成功: ${count}张`)
+                                if (paths && paths.length > 0) {
+                                    console.log(`📁 图片路径: ${paths[0]}`)
+                                }
+                                alert(`选择了 ${count} 张图片`)
+                                break
+
+                            case 'getStorage':
+                                if (found) {
+                                    console.log(`📥 获取存储成功: ${key} = ${JSON.stringify(value)}`)
+                                    alert(`存储内容: ${JSON.stringify(value)}`)
+                                } else {
+                                    console.log(`📥 存储中没有找到: ${key}`)
+                                    alert(`存储中没有找到: ${key}`)
+                                }
+                                break
+                            case 'setStorage':
+                                console.log(`💾 存储成功: ${key} = ${JSON.stringify(value)}`)
+                                alert('存储设置成功!')
+                                break
+                            case 'makePhoneCall':
+                                console.log(`📞 拨打电话成功: ${phoneNumber}`)
+                                break
+                            default:
+                                console.log(`✅ ${action} 操作成功`)
+                        }
+                    } else {
+                        console.error(`❌ ${action} 操作失败: ${error}`)
+                        alert(`${action} 操作失败: ${error}`)
+                    }
+                },
+
+                // 扫码
+                handleScanCode() {
+                    scanCode('扫描二维码')
+                },
+
+                // 选择图片
+                handleChooseImage() {
+                    chooseImage('选择图片')
+                },
+
+                // H5图片上传
+                handleH5UploadImage() {
+                    // 创建文件输入元素
+                    const input = document.createElement('input')
+                    input.type = 'file'
+                    input.accept = 'image/*'
+                    input.multiple = false
+
+                    input.onchange = (event) => {
+                        const file = event.target.files[0]
+                        if (file) {
+                            this.uploadFileToServer(file, 'image')
+                        }
+                    }
+
+                    input.click()
+                },
+
+                // H5附件上传
+                handleH5UploadFile() {
+                    // 创建文件输入元素
+                    const input = document.createElement('input')
+                    input.type = 'file'
+                    input.multiple = true
+                    input.accept = '*/*'
+
+                    input.onchange = (event) => {
+                        const files = Array.from(event.target.files)
+                        if (files.length > 0) {
+                            files.forEach(file => {
+                                this.uploadFileToServer(file, 'file')
+                            })
+                        }
+                    }
+
+                    input.click()
+                },
+
+                // 上传文件到服务器
+                async uploadFileToServer(file, type) {
+                    try {
+                        console.log(`📤 开始上传${type}:`, file.name)
+
+                        const formData = new FormData()
+                        formData.append('file', file)
+                        formData.append('type', type)
+
+                        const response = await fetch('https://httpbin.org/post', {
+                            method: 'POST',
+                            body: formData
+                        })
+
+                        const result = await response.json()
+                        console.log('✅ 上传成功:', result)
+
+                        alert(`${type === 'image' ? '图片' : '文件'}上传成功!\n文件名: ${file.name}\n大小: ${(file.size / 1024).toFixed(2)}KB`)
+
+                    } catch (error) {
+                        console.error('❌ 上传失败:', error)
+                        alert(`${type === 'image' ? '图片' : '文件'}上传失败: ${error.message}`)
+                    }
+                },
+
+                // 拨打电话
+                handleMakePhoneCall() {
+                    makePhoneCall('10086', '拨打电话')
+                },
+
+                // 获取存储
+                handleGetStorage() {
+                    getStorage('test_data', '获取存储数据')
+                },
+
+                // 设置存储
+                handleSetStorage() {
+                    setStorage('test_data', {
+                        message: '来自H5的测试数据',
+                        timestamp: Date.now(),
+                        user: 'H5用户'
+                    }, '设置存储数据')
+                },
+
+                // 跳转到Test1页面
+                goToTest1() {
+                    navigateTo('test1', { from: 'home' })
+                },
+
+                // 返回小程序
+                handleGoBack() {
+                    goBack('从Home页面返回')
+                }
+            }
+        }).mount('#app')
+    </script>
+
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: #f5f5f5;
+            padding: 20px;
+        }
+
+        .container {
+            max-width: 600px;
+            margin: 0 auto;
+            background: white;
+            border-radius: 12px;
+            padding: 24px;
+            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+        }
+
+        h1 {
+            text-align: center;
+            color: #333;
+            margin-bottom: 8px;
+        }
+
+        p {
+            text-align: center;
+            color: #666;
+            margin-bottom: 32px;
+        }
+
+        .button-group {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 12px;
+            margin-bottom: 32px;
+        }
+
+        .btn {
+            padding: 12px 16px;
+            border: none;
+            border-radius: 8px;
+            font-size: 14px;
+            font-weight: 500;
+            cursor: pointer;
+            transition: all 0.2s;
+        }
+
+        .btn.primary { background: #007AFF; color: white; }
+        .btn.secondary { background: #6c757d; color: white; }
+        .btn.success { background: #28a745; color: white; }
+        .btn.warning { background: #ffc107; color: #212529; }
+        .btn.info { background: #17a2b8; color: white; }
+        .btn.danger { background: #dc3545; color: white; }
+        .btn.purple { background: #6f42c1; color: white; }
+
+        .btn:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+        }
+
+
+
+        @media (max-width: 480px) {
+            .button-group {
+                grid-template-columns: 1fr;
+            }
+        }
+    </style>
+</body>
+</html>

+ 191 - 0
examples/test1.html

@@ -0,0 +1,191 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>Test1 - 第二层页面</title>
+    <!-- 微信 JS-SDK -->
+    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
+    <!-- Vue 3 CDN -->
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <!-- H5工具函数库 -->
+    <script src="../js/utils/h5-bridge.js"></script>
+</head>
+<body>
+    <div id="app">
+        <div class="container">
+            <h1>📄 Test1 页面</h1>
+            <p>这是第二层页面,可以继续跳转或返回</p>
+
+            <div class="nav-info">
+                <div class="breadcrumb">
+                    <span>Home</span> → <span class="current">Test1</span>
+                </div>
+            </div>
+
+            <div class="button-group">
+                <button class="btn primary" @click="goToTest2">
+                    ➡️ 进入Test2
+                </button>
+                <button class="btn secondary" @click="goBackToHome">
+                    ⬅️ 返回Home
+                </button>
+                <button class="btn warning" @click="handleScanCode">
+                    📷 扫码测试
+                </button>
+                <button class="btn danger" @click="goBackToMiniProgram">
+                    🔙 返回小程序
+                </button>
+            </div>
+
+
+        </div>
+    </div>
+
+    <script>
+        const { createApp } = Vue
+
+        createApp({
+            data() {
+                return {}
+            },
+
+            mounted() {
+                // 初始化页面,使用自定义结果处理
+                // initPage('test1', this.handleResult)
+            },
+
+            methods: {
+                // 处理操作结果
+                handleResult(data) {
+                    const { action, success, result, error } = data
+
+                    if (success) {
+                        switch (action) {
+                            case 'scanCode':
+                                console.log(`📷 扫码成功: ${result}`)
+                                alert(`扫码结果: ${result}`)
+                                break
+                            default:
+                                console.log(`✅ ${action} 操作成功`)
+                        }
+                    } else {
+                        console.error(`❌ ${action} 操作失败: ${error}`)
+                        alert(`${action} 操作失败: ${error}`)
+                    }
+                },
+
+                // 跳转到Test2页面
+                goToTest2() {
+                    navigateTo('test2', { from: 'test1' })
+                },
+
+                // 返回Home页面
+                goBackToHome() {
+                    navigateTo('home', { from: 'test1' })
+                },
+
+                // 扫码测试
+                handleScanCode() {
+                    scanCode('从Test1页面扫码')
+                },
+
+                // 返回小程序
+                goBackToMiniProgram() {
+                    goBack('从Test1页面返回')
+                },
+
+
+            }
+        }).mount('#app')
+    </script>
+
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            min-height: 100vh;
+            padding: 20px;
+        }
+
+        .container {
+            max-width: 600px;
+            margin: 0 auto;
+            background: white;
+            border-radius: 12px;
+            padding: 24px;
+            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+        }
+
+        h1 {
+            text-align: center;
+            color: #333;
+            margin-bottom: 8px;
+        }
+
+        p {
+            text-align: center;
+            color: #666;
+            margin-bottom: 24px;
+        }
+
+        .nav-info {
+            background: #f8f9fa;
+            border-radius: 8px;
+            padding: 16px;
+            margin-bottom: 24px;
+        }
+
+        .breadcrumb {
+            font-size: 14px;
+            color: #666;
+        }
+
+        .breadcrumb .current {
+            color: #007AFF;
+            font-weight: 500;
+        }
+
+        .button-group {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 12px;
+            margin-bottom: 32px;
+        }
+
+        .btn {
+            padding: 12px 16px;
+            border: none;
+            border-radius: 8px;
+            font-size: 14px;
+            font-weight: 500;
+            cursor: pointer;
+            transition: all 0.2s;
+        }
+
+        .btn.primary { background: #007AFF; color: white; }
+        .btn.secondary { background: #6c757d; color: white; }
+        .btn.warning { background: #ffc107; color: #212529; }
+        .btn.danger { background: #dc3545; color: white; }
+
+        .btn:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+        }
+
+
+
+        @media (max-width: 480px) {
+            .button-group {
+                grid-template-columns: 1fr;
+            }
+        }
+    </style>
+</body>
+</html>

+ 435 - 0
examples/test2.html

@@ -0,0 +1,435 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>Test2 - 第三层页面</title>
+    <!-- 微信 JS-SDK -->
+    <script src="https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script>
+    <!-- Vue 3 CDN -->
+    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
+    <!-- H5工具函数库 -->
+    <script src="../js/utils/h5-bridge.js"></script>
+</head>
+<body>
+    <div id="app">
+        <div class="container">
+            <h1>📋 Test2 页面</h1>
+            <p>这是第三层页面,最深层级的业务页面</p>
+
+            <div class="nav-info">
+                <div class="breadcrumb">
+                    <span>Home</span> → <span>Test1</span> → <span class="current">Test2</span>
+                </div>
+                <div class="level-info">
+                    当前层级:第3层 | 这里模拟表单提交等业务操作
+                </div>
+            </div>
+
+            <div class="business-section">
+                <h3>🏢 业务操作区</h3>
+                <div class="form-group">
+                    <label>模拟表单数据:</label>
+                    <input v-model="formData.title" @input="saveFormData" placeholder="请输入标题" class="form-input">
+                    <textarea v-model="formData.content" @input="saveFormData" placeholder="请输入内容" class="form-textarea"></textarea>
+
+                    <!-- 显示已选择的图片 -->
+                    <div v-if="formData.images.length > 0" class="images-section">
+                        <label>已选择的图片 ({{ formData.images.length }}张):</label>
+                        <div class="images-list">
+                            <div v-for="(image, index) in formData.images" :key="index" class="image-item">
+                                <span class="image-index">{{ index + 1 }}.</span>
+                                <span class="image-path">{{ image.path }}</span>
+                                <button @click="removeImage(index)" class="remove-btn">❌</button>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div class="button-group">
+                <button class="btn success" @click="submitForm">
+                    ✅ 提交表单
+                </button>
+                <button class="btn warning" @click="chooseImage">
+                    🖼️ 选择图片 ({{ formData.images.length }})
+                </button>
+                <button class="btn info" @click="scanCode">
+                    📷 扫码
+                </button>
+                <button class="btn light" @click="clearFormData">
+                    🗑️ 清除表单
+                </button>
+                <button class="btn secondary" @click="goBackToTest1">
+                    ⬅️ 返回Test1
+                </button>
+                <button class="btn danger" @click="goBackToMiniProgram">
+                    🔙 返回小程序
+                </button>
+            </div>
+
+           
+        </div>
+    </div>
+
+    <script>
+        const { createApp } = Vue
+
+        createApp({
+            data() {
+                return {
+                    logs: [],
+                    formData: {
+                        title: '',
+                        content: '',
+                        images: [] // 存储选择的图片
+                    }
+                }
+            },
+
+            mounted() {
+                // 初始化页面
+                initPage('test2', this.handleResult)
+
+                // 恢复表单数据(简化版)
+                this.loadFormData()
+            },
+
+            methods: {
+                
+
+                // 处理操作结果
+                handleResult(data) {
+                    const { action, success, result, error, count, paths } = data
+
+                    if (success) {
+                        switch (action) {
+                            case 'scanCode':
+                                console.log(`📷 扫码成功: ${result}`)
+                                // 将扫码结果填入表单
+                                if (result) {
+                                    this.formData.content += `\n扫码结果: ${result}`
+                                    this.saveFormData() // 保存表单数据
+                                }
+                                break
+                            case 'chooseImage':
+                                console.log(`🖼️ 选择图片成功: ${count}张`)
+                                if (paths && paths.length > 0) {
+                                    alert(JSON.stringify(paths))
+                                    // 添加图片到数组中
+                                    this.formData.images.push({
+                                        path: paths[0],
+                                        timestamp: Date.now()
+                                    })
+                                    console.log(`📁 已添加图片 ${this.formData.images.length}: ${paths[0]}`)
+                                    this.saveFormData() // 保存表单数据
+                                }
+                                break
+                            default:
+                                console.log(`✅ ${action} 操作成功`)
+                        }
+                    } else {
+                        console.error(`❌ ${action} 操作失败: ${error}`)
+                    }
+                },
+
+                // 提交表单
+                submitForm() {
+                    if (!this.formData.title.trim()) {
+                       
+                        return
+                    }
+
+                   
+
+                    // 模拟提交成功后清除数据并返回小程序
+                    setTimeout(() => {
+                        this.clearFormData() // 提交成功后清除保存的数据
+                        goBack('表单提交完成')
+                    }, 1000)
+                },
+
+                // 移除图片
+                removeImage(index) {
+                    this.formData.images.splice(index, 1)
+                    this.saveFormData()
+                },
+
+                // 选择图片
+                chooseImage() {
+                    chooseImage('从Test2页面选择图片')
+                },
+
+                // 扫码
+                scanCode() {
+                    scanCode('从Test2页面扫码')
+                },
+
+                // 返回Test1页面
+                goBackToTest1() {
+                    window.location.href = './test1.html?from=test2'
+                },
+
+                // 返回Home页面
+                goBackToHome() {
+                    window.location.href = './home.html?from=test2'
+                },
+
+                // 返回小程序
+                goBackToMiniProgram() {
+                    goBack('从Test2页面返回')
+                },
+
+
+
+                // 保存表单数据到localStorage
+                saveFormData() {
+                    try {
+                        localStorage.setItem('test2_form_data', JSON.stringify(this.formData))
+                        console.log('💾 表单数据已保存')
+                    } catch (error) {
+                        console.error('保存表单数据失败:', error)
+                    }
+                },
+
+                // 从localStorage恢复表单数据
+                loadFormData() {
+                    try {
+                        const savedData = localStorage.getItem('test2_form_data')
+                        if (savedData) {
+                            this.formData = { ...this.formData, ...JSON.parse(savedData) }
+                            this.addLog(`📥 恢复表单数据: ${this.formData.images.length}张图片`)
+                        }
+                    } catch (error) {
+                        console.error('恢复表单数据失败:', error)
+                    }
+                },
+
+                // 清除保存的表单数据
+                clearFormData() {
+                    try {
+                        localStorage.removeItem('test2_form_data')
+                        this.formData = {
+                            title: '',
+                            content: '',
+                            images: []
+                        }
+                    } catch (error) {
+                        console.error('清除表单数据失败:', error)
+                    }
+                },
+
+              
+            }
+        }).mount('#app')
+    </script>
+
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%);
+            min-height: 100vh;
+            padding: 20px;
+        }
+
+        .container {
+            max-width: 600px;
+            margin: 0 auto;
+            background: white;
+            border-radius: 12px;
+            padding: 24px;
+            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+        }
+
+        h1 {
+            text-align: center;
+            color: #333;
+            margin-bottom: 8px;
+        }
+
+        p {
+            text-align: center;
+            color: #666;
+            margin-bottom: 24px;
+        }
+
+        .nav-info {
+            background: #f8f9fa;
+            border-radius: 8px;
+            padding: 16px;
+            margin-bottom: 24px;
+        }
+
+        .breadcrumb {
+            font-size: 14px;
+            color: #666;
+            margin-bottom: 8px;
+        }
+
+        .breadcrumb .current {
+            color: #e91e63;
+            font-weight: 500;
+        }
+
+        .level-info {
+            font-size: 12px;
+            color: #999;
+        }
+
+        .business-section {
+            background: #fff3cd;
+            border-radius: 8px;
+            padding: 16px;
+            margin-bottom: 24px;
+        }
+
+        .business-section h3 {
+            color: #856404;
+            margin-bottom: 16px;
+        }
+
+        .form-group {
+            display: flex;
+            flex-direction: column;
+            gap: 12px;
+        }
+
+        .form-group label {
+            font-weight: 500;
+            color: #333;
+        }
+
+        .form-input, .form-textarea {
+            padding: 12px;
+            border: 1px solid #ddd;
+            border-radius: 6px;
+            font-size: 14px;
+        }
+
+        .form-textarea {
+            min-height: 80px;
+            resize: vertical;
+        }
+
+        .images-section {
+            margin-top: 16px;
+            padding: 12px;
+            background: #f0f8ff;
+            border-radius: 6px;
+        }
+
+        .images-list {
+            margin-top: 8px;
+        }
+
+        .image-item {
+            display: flex;
+            align-items: center;
+            padding: 8px;
+            background: white;
+            border-radius: 4px;
+            margin-bottom: 8px;
+            border: 1px solid #e0e0e0;
+        }
+
+        .image-index {
+            font-weight: 500;
+            color: #666;
+            margin-right: 8px;
+            min-width: 20px;
+        }
+
+        .image-path {
+            flex: 1;
+            font-size: 12px;
+            color: #333;
+            word-break: break-all;
+        }
+
+        .remove-btn {
+            background: none;
+            border: none;
+            cursor: pointer;
+            font-size: 12px;
+            padding: 4px;
+            margin-left: 8px;
+        }
+
+        .button-group {
+            display: grid;
+            grid-template-columns: 1fr 1fr;
+            gap: 12px;
+            margin-bottom: 32px;
+        }
+
+        .btn {
+            padding: 12px 16px;
+            border: none;
+            border-radius: 8px;
+            font-size: 14px;
+            font-weight: 500;
+            cursor: pointer;
+            transition: all 0.2s;
+        }
+
+        .btn.primary { background: #007AFF; color: white; }
+        .btn.secondary { background: #6c757d; color: white; }
+        .btn.success { background: #28a745; color: white; }
+        .btn.warning { background: #ffc107; color: #212529; }
+        .btn.info { background: #17a2b8; color: white; }
+        .btn.danger { background: #dc3545; color: white; }
+        .btn.light { background: #f8f9fa; color: #333; border: 1px solid #ddd; }
+
+        .btn:hover {
+            transform: translateY(-1px);
+            box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+        }
+
+        .log-section {
+            border-top: 1px solid #eee;
+            padding-top: 24px;
+        }
+
+        .log-section h3 {
+            color: #333;
+            margin-bottom: 16px;
+        }
+
+        .log-container {
+            background: #f8f9fa;
+            border-radius: 8px;
+            padding: 16px;
+            max-height: 250px;
+            overflow-y: auto;
+        }
+
+        .log-item {
+            display: flex;
+            margin-bottom: 8px;
+            font-size: 12px;
+        }
+
+        .log-time {
+            color: #999;
+            margin-right: 12px;
+            min-width: 80px;
+        }
+
+        .log-message {
+            color: #333;
+        }
+
+        @media (max-width: 480px) {
+            .button-group {
+                grid-template-columns: 1fr;
+            }
+        }
+    </style>
+</body>
+</html>

+ 238 - 0
examples/upload-image-demo.html

@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+	<meta charset="UTF-8" />
+	<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
+	<title>图片上传裁剪示例</title>
+	<script src="/js/mp_base/base.js"></script>
+</head>
+<body>
+	<div id="app" v-cloak>
+		<div class="demo-container">
+			<h2>图片和文件上传组件示例</h2>
+
+			<!-- 单图上传 -->
+			<h3>1. 圆形头像 (160x160) - 单图</h3>
+			<table>
+				<tr>
+					<th>头像</th>
+					<td>
+						<ss-upload-image
+							v-model="avatar1"
+							:width="160"
+							:height="160"
+							shape="circle"
+							:aspect-ratio="1"
+							@updated="onImageUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ avatar1 || '未上传' }}
+						</div>
+					</td>
+				</tr>
+			</table>
+
+			<h3>2. 方形头像 (120x120) - 单图</h3>
+			<table>
+				<tr>
+					<th>艺术照</th>
+					<td>
+						<ss-upload-image
+							v-model="avatar2"
+							:width="120"
+							:height="120"
+							shape="square"
+							:aspect-ratio="1"
+							@updated="onImageUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ avatar2 || '未上传' }}
+						</div>
+					</td>
+				</tr>
+			</table>
+
+			<h3>3. 矩形图片 (200x150, 4:3比例) - 单图</h3>
+			<table>
+				<tr>
+					<th>证件照</th>
+					<td>
+						<ss-upload-image
+							v-model="photo"
+							:width="200"
+							:height="150"
+							shape="square"
+							:aspect-ratio="4/3"
+							:output-width="400"
+							:output-height="300"
+							@updated="onImageUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ photo || '未上传' }}
+						</div>
+					</td>
+				</tr>
+			</table>
+
+			<!-- 多图上传 -->
+			<h3>4. 多图上传 (最多9张) - 方形80x80</h3>
+			<table>
+				<tr>
+					<th>相册</th>
+					<td>
+						<ss-upload-image
+							v-model="gallery"
+							:max="9"
+							:width="80"
+							:height="80"
+							shape="square"
+							:aspect-ratio="1"
+							@updated="onImageUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ Array.isArray(gallery) ? gallery.join(', ') : (gallery || '未上传') }}
+						</div>
+					</td>
+				</tr>
+			</table>
+
+			<h3>5. 多图上传 (最多3张) - 圆形100x100</h3>
+			<table>
+				<tr>
+					<th>图集</th>
+					<td>
+						<ss-upload-image
+							v-model="photoSet"
+							:max="3"
+							:width="100"
+							:height="100"
+							shape="circle"
+							:aspect-ratio="1"
+							@updated="onImageUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ Array.isArray(photoSet) ? photoSet.join(', ') : (photoSet || '未上传') }}
+						</div>
+					</td>
+				</tr>
+			</table>
+
+			<!-- 文件上传 -->
+			<h3>6. 单文件上传 (默认5MB)</h3>
+			<table>
+				<tr>
+					<th>文档</th>
+					<td>
+						<ss-upload-file
+							v-model="document"
+							@updated="onFileUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ document || '未上传' }}
+						</div>
+					</td>
+				</tr>
+			</table>
+
+			<h3>7. 多文件上传 (最多5个,限制10MB)</h3>
+			<table>
+				<tr>
+					<th>附件</th>
+					<td>
+						<ss-upload-file
+							v-model="attachments"
+							:max="5"
+							:max-size="10"
+							@updated="onFileUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ Array.isArray(attachments) ? attachments.join(', ') : (attachments || '未上传') }}
+						</div>
+					</td>
+				</tr>
+			</table>
+
+			<h3>8. 限制文件类型 (仅图片)</h3>
+			<table>
+				<tr>
+					<th>图片文件</th>
+					<td>
+						<ss-upload-file
+							v-model="imageFile"
+							accept=".jpg,.jpeg,.png,.gif"
+							@updated="onFileUpdated"
+						/>
+						<div style="margin-top: 10px; font-size: 12px; color: #666;">
+							当前值: {{ imageFile || '未上传' }}
+						</div>
+					</td>
+				</tr>
+			</table>
+		</div>
+	</div>
+
+	<script>
+		SS.ready(function () {
+			window.SS.dom.initializeFormApp({
+				el: '#app',
+				data() {
+					return {
+						// 单图上传
+						avatar1: '',
+						avatar2: '',
+						photo: '',
+
+						// 多图上传
+						gallery: [],
+						photoSet: [],
+
+						// 文件上传
+						document: '',
+						attachments: [],
+						imageFile: ''
+					};
+				},
+
+				methods: {
+					onImageUpdated(serverPath) {
+						console.log('图片上传成功,服务器路径:', serverPath);
+						alert('图片上传成功!路径:' + serverPath);
+					},
+
+					onFileUpdated(serverPath) {
+						console.log('文件上传成功,服务器路径:', serverPath);
+						alert('文件上传成功!路径:' + serverPath);
+					}
+				}
+			});
+		});
+	</script>
+
+	<style>
+		[v-cloak] {
+			display: none !important;
+		}
+
+		.demo-container {
+			padding: 20px;
+		}
+
+		h2 {
+			text-align: center;
+			margin-bottom: 30px;
+			color: #333;
+		}
+
+		h3 {
+			margin-top: 30px;
+			margin-bottom: 15px;
+			color: #666;
+			font-size: 18px;
+		}
+
+		table {
+			margin-bottom: 30px;
+		}
+	</style>
+</body>
+</html>

BIN
js/.DS_Store


+ 152 - 0
js/PageJs/infoHomep.js

@@ -0,0 +1,152 @@
+function init(img) {
+  var e = parseFloat("${edgzl}");
+  (e = isNaN("${edgzl}") ? 0 : e), (e = e > 0 ? e : 100);
+  var g = parseFloat("${grgzl}");
+  g = isNaN("${grgzl}") ? 0 : g;
+  var p = g / e;
+  function showTime() {
+    var t = new Date(),
+      e = t.getFullYear(),
+      i = t.getMonth() + 1,
+      n = t.getDate(),
+      o = t.getMinutes() < 10 ? "0" + t.getMinutes() : t.getMinutes();
+    $(".nowDateGrxx").text(
+      e + "\u5e74" + i + "\u6708" + n + "\u65e5 " + t.getHours() + ":" + o
+    );
+  }
+  $(".gzl1").css("width", 100 * (p > 1 ? 1 : p) + "%"),
+    $(".gzlpe").html((100 * p).toFixed(2)),
+    setInterval(showTime, 1e3);
+  var timer = null;
+  function tips(t, e) {
+    if (null != $("#" + e).html()) {
+      var i = $(t)[0].getBoundingClientRect(),
+        n = this.recursionIframe(),
+        o = i.left + n.left,
+        r = i.top + i.height + n.top,
+        s =
+          "<div class='whkDivHover' style = 'width:" +
+          $(t).width() +
+          "px; height: 102px; position: absolute; left: " +
+          o +
+          "px; top:" +
+          r +
+          "px; z-index:999999;' ><div class='popup-div smallScrollbar' style='width: 100%;padding: 0px;overflow:auto'><div class='smallScr cursor-click ' style='max-height: 120px;height: 100px;width: calc(100% - 2px);padding-right: 2px;box-sizing: border-box;margin-top: 2px;'>";
+      (s += $("#" + e)
+        .find(".smallScr")
+        .html()),
+        (s += "</div></div></div>"),
+        top.$("body").append(s);
+      var d = document.createElement("script");
+      $(d).append("initNiceScorll();"),
+        top.$("body").append(d),
+        top.$(".whkDivHover").find(".popup-div").show(),
+        top.$(".whkDivHover").mouseenter(function () {
+          clearTimeout(timer);
+        }),
+        top
+          .$(".whkDivHover")
+          .find(".popup-div")
+          .mouseleave(function () {
+            tipsHidden();
+          });
+    }
+  }
+  function tipsHidden() {
+    top.$(".whkDivHover").remove();
+  }
+  $(".dHover").mouseenter(function () {
+    tips($(this), "cardList");
+  }),
+    $(".dHover").mouseleave(function () {
+      clearTimeout(timer),
+        (timer = setTimeout(function () {
+          tipsHidden();
+        }, 100));
+    }),
+    $(".dHover2").mouseenter(function (t) {
+      t.stopPropagation(),
+        tips($(this), "cardList2"),
+        setTimeout(function () {
+          top.loadSmallScorll(top.$(".popup-div"));
+        }, 100);
+    }),
+    $(".dHover2").mouseleave(function (t) {
+      clearTimeout(timer),
+        (timer = setTimeout(function () {
+          tipsHidden();
+        }, 100));
+    }),
+    (this.recursionIframe = function (t) {
+      if ((t = t || window).parent == top && t.parent == t)
+        return { left: 0, top: 0 };
+      for (
+        var e,
+          i = t.parent,
+          n = i.document.getElementsByTagName("IFRAME"),
+          o = t.document.getElementsByTagName("IFRAME"),
+          r = 0;
+        r < o.length;
+        r++
+      ) {
+        if ((s = o[r]).contentWindow == t) {
+          e = s;
+          break;
+        }
+      }
+      for (r = 0; r < n.length; r++) {
+        var s;
+        if ((s = n[r]).contentWindow == t) {
+          e = s;
+          break;
+        }
+      }
+      var d = {
+        left: (function t(e) {
+          var i = e.offsetLeft;
+          return null != e.offsetParent && (i += t(e.offsetParent)), i;
+        })(e),
+        top: (function t(e) {
+          var i = e.offsetTop;
+          return null != e.offsetParent && (i += t(e.offsetParent)), i;
+        })(e),
+      };
+      if (i.parent != i) {
+        var l = this.recursionIframe(i);
+        (d.left += l.left), (d.top += l.top);
+      }
+      return d;
+    }),
+    $("#wh_content").find(".popup-div").hide(),
+    $("#rc_content").find(".popup-div").hide(),
+    $(".popup-div").hide(),
+    $("#wh_content").click(function () {
+      var url = window.a;
+      eval(url), $("div.zhdlsj").show(), $("#wh_content").hide();
+    });
+}
+$("#grtjList-div").mouseleave(function () {
+  $("#grtjList-child-div").css("display", "none");
+}),
+  $(".tjlb-child").mouseenter(function () {
+    $("#grtjList-child-div").css("display", "block");
+  }),
+  $("#grtjList-child-div").mouseleave(function () {
+    $("#grtjList-child-div").css("display", "none");
+  }),
+  loadSmallScorll($("#grtjList-child-div"));
+var divw = $(".grtjList-num").width();
+$(".grtjList-num").mouseleave(function () {
+  $(this).find("div[parentId]").css("display", "none");
+}),
+  $(".grtjList-num").mouseenter(function () {
+    var t = $(this).attr("id");
+    t &&
+      ($("div[parentId]").css("display", "none"),
+      $("div[parentId='" + t + "']").css("display", "block")),
+      $(".grtjList-child-num").width(divw);
+  }),
+  setTimeout(function () {
+    loadSmallScorll($(".grtjList-child-num"));
+  }, 100),
+  $(".grtjList-child-num").width(divw);

File diff suppressed because it is too large
+ 0 - 0
js/PageJs/mainMenuDefHome.js


+ 146 - 0
js/PageJs/taskHomep.js

@@ -0,0 +1,146 @@
+function init(img) {
+
+  //获取整体宽度
+var ztW = $('#shList').width();
+var ztW1 =ztW - 255 ;
+console.log(ztW,ztW1,ztW1 - 70)
+//动态给收起来的标题赋宽度值
+$(".bb1").css({
+// "max-width": ztW1 - 70 +'px'
+})
+console.log('被拖动了2');
+
+  var f = $(".shList").find(".file");
+  var trClass="";
+  //图标样式
+  var indenterClass="icon-folder icon-openFolder icon-point icon-doc icon-fullFolder icon-openFullFolder";
+  var hLineClass="";
+  var groupNum=99;
+  //文字样式
+  var titleClass="list-highlight list-highlight list2 list-highlight list-highlight";
+  $('#shList table').treetable({
+      expandable: true,
+      indent:"",
+      stringCollapse:"点击关闭",
+      stringExpand:"点击展开",
+      onNodeCollapse:function(a){
+          //console.log(this);
+
+          var trEle=$(this.treeCell).parent();
+          var titleEle=trEle.find(".title").removeClass(titleClass);
+          var indenter=$(this.indenter).removeClass(indenterClass);
+
+          if(this.children.length>groupNum){
+              indenter.addClass("icon-fullFolder")
+              titleEle.addClass("list-highlight");
+          }else{
+              indenter.addClass("icon-folder")
+              titleEle.addClass("list-highlight");
+          }
+         trEle.addClass(hLineClass);
+      },onNodeExpand:function(a){
+          var trEle=$(this.treeCell).parent();
+          var titleEle=trEle.find(".title").removeClass(titleClass);
+          var indenter=$(this.indenter).removeClass(indenterClass);
+
+          if(this.children.length>groupNum){
+              indenter.addClass("icon-openFullFolder");
+              titleEle.addClass("list-highlight")
+          }else{
+              indenter.addClass("icon-openFolder");
+              titleEle.addClass("list-highlight")
+          }
+
+          trEle.removeClass(hLineClass);
+          $(this.children[this.children.length-1].treeCell).parent().addClass(hLineClass);
+
+      },onNodeInitialized:function(){
+          //console.log(this);
+          var trEle=$(this.treeCell).parent().removeClass(hLineClass);
+          var titleEle=trEle.find(".title").removeClass(titleClass);
+          var indenter=$(this.indenter).removeClass(indenterClass);
+          //文件夹
+          if(this.children.length>0){
+              console.log(this);
+              console.log(this.children.length+" "+ groupNum+" "+(this.children.length> groupNum));
+              if(this.children.length> groupNum){
+                  indenter.addClass("icon-fullFolder")
+                  titleEle.addClass("list-highlight");
+              }else{
+                  indenter.addClass("icon-folder")
+                  titleEle.addClass("list-highlight");
+              }
+            trEle.find(".tdSj").click(function(){
+                  $(this).parent().find(".indenter").trigger("click");
+                  return false;
+            })
+          //单个
+          }else if(!this.parentId){
+              indenter.addClass("icon-doc")
+              titleEle.addClass("list2");
+
+          //子
+          }else {
+
+              indenter.addClass("icon-point")
+              titleEle.addClass("list2");
+          }
+          if(this.children.length>0||!this.parentId){
+              trEle.addClass(hLineClass);
+          }
+
+          var id=trEle.find("[id]").attr("id");
+
+          setTimeout(function(){
+            //console.log("gh:"+id+">"+document.querySelector("#"+id).offsetWidth+"  "+document.querySelector("#"+id).offsetHeight)
+            wd.display.initGrowHighByIds([id],"80px");
+          },500)
+      }
+  });
+  $.each(f, function() {
+      var itm = $(this);
+      if (typeof(itm.parent().parent().attr("data-tt-parent-id")) != "undefined") {
+          itm.css("margin-left", "2em");
+      }
+  });
+  //获取整体宽度
+  var bjW= $('.home-list-div').width() - 180;
+  var ztW = $('.dbtreetable').width();
+  //动态给左边赋宽度值
+  var tdW = ztW - 50;
+  $(".tdBt").width(tdW);
+  var leftW = $(".tdBt").width();
+   var aW=leftW - 100;
+  //动态给收起来的标题赋宽度值
+  $(".bb").css({
+    // "max-width": aW +'px'
+  })
+  //动态给放出来的标题赋宽度值
+  $(".btA").css({
+    // "max-width": bjW +'px'
+  })
+  //console.log('被拖动了')
+//$(".tdSj").click(function(){$(this).prev(".tdBt").children(".indenter").children("a").trigger("click")});
+
+$("[taskCount]").each(function(){
+  var taskCount=parseInt($(this).attr("taskCount"));
+  var c=taskCount;
+  if(isNaN(taskCount)||taskCount>groupNum)c="";
+  $(this).find(".indenter a").html(c);
+
+})
+
+  // wd.display.initGrowHighByIds(growHeightIds,"80px");
+// if($("#sybj_db_val").val() == undefined) {
+   //  console.error("sybj_db_val.....................")
+   //  $("body").append('<input type="hidden" value="1" id="sybj_db_val" />');
+   //  loadSmallScorll($("#sybj_db"));
+// }
+
+$(img).parents("[initurl]").find(".nicescroll-rails").remove();
+ loadSmallScorll($(img).parents("[initurl]").find(".smallScrollbar"));
+//    $('.nicescroll-rails-vr').attr('style','padding-left: 0px;padding-right: 2px;width: 3px;z-index: 999999;background: transparent;cursor: default;position: absolute;top: 53px;left: 344px;height: 210px;display: block;')
+ setTimeout(function(){
+  $('.db').parents('.ContainerForLoad').siblings('.nicescroll-rails-vr').css('padding-left','0.5px')
+ },1000)
+}

+ 1 - 0
js/PageJs/urgeHomep.js

@@ -0,0 +1 @@
+function init(t){var r=$("#sqList").width()-255;$(".bb1").css({"max-width":r-70+"px"}),$(t).parents("[initurl]").find(".nicescroll-rails").remove(),loadSmallScorll($(t).parents("[initurl]").find(".smallScrollbar")),setTimeout((function(){$("#cb").parents(".ContainerForLoad").siblings(".nicescroll-rails-vr").attr("style","padding-left: 1px!important;padding-right: 2px;width: 3px;z-index: 999999;background: transparent;cursor: default;position: absolute;top: 53px;float: right;right: 0px; height: 300px;display: block;")}),2e3);for(var i=$(".listProgress-done").attr("percentage"),e=0;e<$(".listProgress-done").length;e++)(i=parseInt($(".listProgress-done").eq(e).attr("percentage")))>100&&(i=100),$(".listProgress-done").eq(e).attr("style","width:"+i+"%")}

File diff suppressed because it is too large
+ 0 - 0
js/base.js


+ 31 - 0
js/cms.js

@@ -0,0 +1,31 @@
+window.ss.cms||(window.ss.cms={});
+
+
+
+//查询子内容列表
+ss.display.querySubNrList = function (nrid, jlztm) {
+    var result = [];
+    $.ajax({
+        type: 'post',
+        url: "/service",
+        dataType: "json",
+        data: {
+            ssServ: "wrCmsList",    // wdService: "nrxtsy",。Lin
+            nrid: "T-" + nrid,
+            sfbf: 1,
+            jlztm: jlztm
+        },
+        async: false,
+        success: function (jsonData) {
+            for (var i = 0; i < jsonData.length; i++) {
+                result.push({
+                    name: jsonData[i].mc,
+                    value: jsonData[i].mswj,
+                    nrjslbm: jsonData[i].nrjslbm
+                });
+            }
+        }
+    });
+    return result;
+}
+

+ 1 - 0
js/common.js

@@ -0,0 +1 @@
+window.ss||(window.ss={}),ss.c={},ss.c.wdAjax=function(e){var t=function(){if("undefined"!=typeof XMLHttpRequest)return new XMLHttpRequest;if("undefined"==typeof ActiveXObject)throw new Error("您的系统或浏览器不支持XHR对象!");for(var e=["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"],t=0;t<e.length;t++)try{return new ActiveXObject(e[t])}catch(e){}}();function n(){200==t.status?"json"==e.dataType?e.success(JSON.parse(t.responseText)):e.success(t.responseText):e.error&&e.error("获取数据失败,错误代号为:"+t.status+"错误信息为:"+t.statusText)}e.url=e.url,e.data=function(e){var t=[];for(var n in e)t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t.join("&")}(e.data),"get"===e.type&&(e.url+="-1"==e.url.indexOf("?")?"?"+e.data:"&"+e.data),!0===e.async&&(t.onreadystatechange=function(){4==t.readyState&&n()}),t.open(e.type,e.url,e.async),"post"===e.type?(t.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),t.send(e.data)):t.send(null),!1===e.async&&n()};

File diff suppressed because it is too large
+ 0 - 0
js/display.js


File diff suppressed because it is too large
+ 0 - 0
js/edit.js


+ 75 - 0
js/form.js

@@ -0,0 +1,75 @@
+//表单操作相关js文件
+window.ss.form = window.ss.form || {};
+
+//提交表单的方法
+ss.form.submit = function (param){
+    const {
+        id, url, width, height, minHeight, maxHeight, targetWin
+    } = param;
+
+    //如果表单校验通过
+    if(window.ssVm&&window.ssVm.validateAllAndShowMsg()){
+        let f = null;//表单元素对象
+        if (id) {
+            // 如果id不为空,根据id获取元素
+            f = document.getElementById(id);
+        } else {
+            // 如果id为空,获取页面第一个form元素
+            f = document.querySelector('form');
+        }
+
+        if(!f){
+            alert('表单不存在');
+            return;
+        }
+
+        if(url)
+            f.action = url;
+
+        if (width && height && minHeight && maxHeight) {
+            ss.display.resizeComponent(
+                width,
+                height,
+                minHeight,
+                maxHeight
+            );
+        }
+
+        if(targetWin){
+            ss.form.submit2TargetWin(targetWin);
+        }else{
+            f.submit();
+        }
+
+        return true;
+    }else
+        return false;
+
+}
+
+
+ss.form.submit2TargetWin = function (targetWin) {
+    const firstForm = document.querySelector('form');
+    if(firstForm==null){
+        console.log("找不到form!");
+    }
+    var myform = firstForm.cloneNode(true);
+    myform.style.display = "none";
+    firstForm.parentNode.innerHTML = "";
+
+    targetWin.document.body.appendChild(myform);
+    myform.submit();
+}
+
+ss.form.confirm = function (title,msg,yesFun,noFun){
+    const isConfirmed = confirm(msg);
+    if (isConfirmed) {
+        console.log("用户确认yes");
+        if(yesFun)
+            yesFun();
+    } else {
+        console.log("用户取消");
+        if(noFun)
+            noFun();
+    }
+}

File diff suppressed because it is too large
+ 14631 - 0
js/jodit/ace.js


File diff suppressed because it is too large
+ 0 - 0
js/jodit/beautify-html.min.js


File diff suppressed because it is too large
+ 0 - 0
js/jodit/beautify.min.js


File diff suppressed because it is too large
+ 9 - 0
js/jodit/jodit.js


File diff suppressed because it is too large
+ 0 - 0
js/jodit/mode-html.js


File diff suppressed because it is too large
+ 0 - 0
js/jodit/theme-idle_fingers.js


File diff suppressed because it is too large
+ 1 - 0
js/mp_axios/axios.min.js


+ 149 - 0
js/mp_base/base.js

@@ -0,0 +1,149 @@
+window.ss = {
+	dom: {
+		TYPE: {
+			INPUT: 1,
+			OBJP: 2,
+			DATE: 3,
+			TIME: 4,
+			DATETIME: 5,
+			ONOFFBTN: 6,
+			PIC: 7,
+			HTML: 9,
+			CCPSINGLE: 51, //只对应一个对象属性的,带编码规则的级联菜单,如“出生地区码”(对应省、市、区)
+			CCPMUTIPLE: 52, //对应多个对象属性的级联菜单
+			SEARCHINPUT: 31, //查询页用到的文本框
+			SEARCHDATE: 33, //查询页用到的日期选择
+			SEARCHTIME: 34, //查询页用到的时间选择
+			SEARCHDATETIME: 35, //查询页用到的日期时间选择
+		},
+	},
+	config: {},
+};
+
+window.SS = window.ss; //最终都用小写ss,暂时先兼容大写SS
+
+(function () {
+	// ========== 移动端适配:设置根元素font-size ==========
+	// 设计稿宽度430px,分成10份,1rem = 43px(在430px屏幕上)
+	function setRootFontSize() {
+		const designWidth = 430; // 设计稿宽度
+		const rootValue = designWidth / 10; // 43px
+		const clientWidth = document.documentElement.clientWidth || window.innerWidth;
+
+		// 计算当前屏幕对应的根font-size
+		const rootFontSize = (clientWidth / designWidth) * rootValue;
+
+		// 设置根元素font-size
+		document.documentElement.style.fontSize = rootFontSize + 'px';
+
+		// 调试信息(可选)
+		// console.log(`屏幕宽度: ${clientWidth}px, 根font-size: ${rootFontSize}px, 1rem = ${rootFontSize}px`);
+	}
+
+	// 立即执行
+	setRootFontSize();
+
+	// 监听窗口大小变化
+	window.addEventListener('resize', setRootFontSize);
+
+	// 监听屏幕旋转
+	window.addEventListener('orientationchange', setRootFontSize);
+	// ========== 移动端适配结束 ==========
+
+	const _deps = [
+		['script', '/js/mp_jq/jquery1.11.3.min.js', ''],
+		['script', '/js/mp_vue/vue.global.js', ''],
+		['script', '/js/mp_axios/axios.min.js', ''],
+		// 通用富文本依赖(PC/H5共享目录) by xu 2026-03-01
+		['style', '/skin/easy/css/jodit.css'],
+		['script', '/js/jodit/jodit.js', ''],
+		['script', '/js/mp_jweixin/jweixin-1.6.0.js', ''],
+		['script', '/js/mp_common/common.js', ''],
+		['script', '/js/mp_toast/toast.js', ''],
+		['script', '/js/mp_request/request.js', ''],
+		['script', '/js/mp_user_api/user-api.js', ''],
+		['script', '/js/mp_validator/validator-rules.js', ''],
+		['script', '/js/mp_validation/validation-manager.js', ''],
+		['script', '/js/mp_ss_components/mp-ss-components.js', ''],
+		['script', '/js/mp_bridge/bridge.js', ''],
+		['script', '/js/mp_utils/field-formatter.js', ''],
+		['script', '/js/mp_utils/navigation.js', ''],
+		// 引入 Vant UI 库
+		['script', '/js/mp_vant/vant.min.js'],
+		['style', '/skin/mp_easy/vant.min.css'],
+
+		// 引入 Day.js 日期处理库
+		['script', '/js/mp_dayjs/dayjs.min.js'],
+
+		// 引入 Cropper.js 图片裁剪库
+		['script', '/js/mp_cropper/cropper.min.js'],
+		['style', '/skin/mp_easy/cropper.min.css'],
+
+		// ['style', '/skin/mp_easy/base.css'],
+		['style', '/skin/mp_easy/base.css'],
+		['style', '/skin/mp_easy/iconfont.css'],
+	];
+
+	function loadResource(type, src, module) {
+		return new Promise((resolve, reject) => {
+			const elem = document.createElement(
+				type === 'script' ? 'script' : 'link'
+			);
+
+			if (type === 'script') {
+				elem.src = src;
+				elem.type = module ? 'module' : '';
+			} else {
+				elem.rel = 'stylesheet';
+				elem.href = src;
+			}
+
+			elem.onload = () => {
+				if (type === 'script') {
+					// elem.remove();
+				}
+				resolve();
+			};
+
+			elem.onerror = reject;
+			document.head.appendChild(elem);
+		});
+	}
+
+	// 等待所有依赖加载完成后再执行回调
+	window.SS.ready = function (callback) {
+		if (window.SS && window.SS.dom && window.SS.dom.initializeFormApp) {
+			callback();
+		} else {
+			document.addEventListener('SSReady', () => {
+				// 确保组件初始化完成后再执行回调
+				if (
+					window.SS &&
+					window.SS.dom &&
+					window.SS.dom.initializeFormApp
+				) {
+					callback();
+				}
+			});
+		}
+	};
+
+	async function loadAll() {
+		// 然后加载资源
+		for (const [type, src, module] of _deps) {
+			await loadResource(type, src, module);
+			if (src.includes('ace.js')) {
+				window.ace.require('ace/config').set('workerPath', null);
+			}
+		}
+		document.dispatchEvent(new Event('SSReady'));
+	}
+
+	// 当 DOM 加载完成后执行清理和加载
+	if (document.readyState === 'loading') {
+		document.addEventListener('DOMContentLoaded', loadAll);
+		console.log('DOMContentLoaded');
+	} else {
+		loadAll();
+	}
+})();

+ 474 - 0
js/mp_bridge/bridge.js

@@ -0,0 +1,474 @@
+/**
+ * H5与小程序通信桥接工具函数库
+ * 提供统一的小程序原生功能调用接口
+ */
+
+// 全局变量
+window.H5_PAGE_NAME = window.H5_PAGE_NAME || "default";
+
+/**
+ * 设置当前页面名称
+ * @param {string} pageName 页面名称
+ */
+function setPageName(pageName) {
+  window.H5_PAGE_NAME = pageName;
+}
+
+/**
+ * 检查微信小程序环境
+ * @returns {boolean} 是否在微信小程序环境
+ */
+function checkEnvironment() {
+  return typeof wx !== "undefined" && wx.miniProgram;
+}
+
+/**
+ * 解析URL参数(兼容性处理)
+ * @param {string} search URL查询字符串
+ * @returns {Object} 参数对象
+ */
+function parseUrlParams(search) {
+  const params = {};
+  if (search) {
+    search
+      .substring(1)
+      .split("&")
+      .forEach((param) => {
+        const [key, value] = param.split("=");
+        if (key && value) {
+          params[decodeURIComponent(key)] = decodeURIComponent(value);
+        }
+      });
+  }
+  return params;
+}
+
+/**
+ * 检查是否有返回的结果数据
+ * @param {Function} onResult 结果处理回调
+ */
+function checkResult(onResult) {
+  const search = window.location.search;
+  console.log("🔍 检查URL参数:", search);
+
+  if (search) {
+    const params = parseUrlParams(search);
+    console.log("🔍 解析的参数:", params);
+
+    if (params.result) {
+      try {
+        const resultData = JSON.parse(decodeURIComponent(params.result));
+        console.log("🔍 解析的结果数据:", resultData);
+
+        // 调用回调处理结果
+        if (onResult) {
+          onResult(resultData);
+        }
+      } catch (error) {
+        console.error("解析结果数据失败:", error);
+      }
+    } else {
+      console.log("🔍 URL中没有result参数");
+    }
+  } else {
+    console.log("🔍 URL中没有查询参数");
+  }
+}
+
+/**
+ * 调用小程序原生功能
+ * @param {string} action 操作类型
+ * @param {string} message 消息描述
+ * @param {Object} data 附加数据
+ */
+function callNative(action, message = "", data = null) {
+  if (!checkEnvironment()) {
+    console.error("❌ 不在微信小程序环境");
+    return;
+  }
+
+  const params = {
+    action: action,
+    message: message || "",
+    source: window.H5_PAGE_NAME,
+  };
+
+  // 处理附加数据
+  if (data && typeof data === "object") {
+    try {
+      const jsonString = JSON.stringify(data);
+      params.data = encodeURIComponent(jsonString);
+      console.log("📦 数据编码:", {
+        original: data,
+        json: jsonString,
+        encoded: params.data,
+      });
+    } catch (error) {
+      console.error("❌ JSON序列化失败:", error);
+      return;
+    }
+  }
+
+  // 构建URL参数
+  const queryParts = [];
+  Object.keys(params).forEach((key) => {
+    if (params[key] !== undefined && params[key] !== null) {
+      if (key === "data") {
+        queryParts.push(`${key}=${params[key]}`);
+      } else {
+        queryParts.push(`${key}=${encodeURIComponent(params[key])}`);
+      }
+    }
+  });
+
+  const controllerUrl = `/pages/common/h5-controller?${queryParts.join("&")}`;
+
+  console.log("🎛️ 跳转到控制器:", controllerUrl);
+
+  // 调用微信小程序API,添加降级处理
+  wx.miniProgram.navigateTo({
+    url: controllerUrl,
+    success: (res) => {
+      console.log("✅ 成功跳转到控制器", res);
+    },
+    fail: (err) => {
+      console.error("❌ 控制器调用失败,尝试降级方案:", err);
+      // 降级方案:使用URL参数通信(兼容老版本webview)
+      fallbackUrlCommunication(action, message, data);
+    },
+  });
+}
+
+/**
+ * 降级通信方案:通过URL参数与webview通信
+ * @param {string} action 操作类型
+ * @param {string} message 消息描述
+ * @param {Object} data 附加数据
+ */
+function fallbackUrlCommunication(action, message = "", data = null) {
+  console.log("🔄 使用降级通信方案");
+
+  const params = {
+    h5_action: action,
+    h5_message: message,
+    timestamp: Date.now(),
+  };
+
+  // 如果有额外数据,编码后添加
+  if (data && typeof data === "object") {
+    try {
+      params.h5_data = encodeURIComponent(JSON.stringify(data));
+    } catch (error) {
+      console.error("❌ 数据编码失败:", error);
+    }
+  }
+
+  // 保留原有的非指令参数
+  const currentParams = parseUrlParams(window.location.search);
+  Object.keys(currentParams).forEach((key) => {
+    if (
+      !key.startsWith("h5_") &&
+      !key.startsWith("mp_") &&
+      key !== "timestamp"
+    ) {
+      params[key] = currentParams[key];
+    }
+  });
+
+  // 构建新URL
+  const baseUrl = `${window.location.origin}${window.location.pathname}`;
+  const queryParts = [];
+  Object.keys(params).forEach((key) => {
+    if (params[key] !== undefined && params[key] !== null) {
+      queryParts.push(
+        `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`
+      );
+    }
+  });
+
+  const url = `${baseUrl}?${queryParts.join("&")}`;
+  console.log("🔄 降级跳转URL:", url);
+
+  // 使用location.href触发webview的load事件
+  window.location.href = url;
+}
+
+/**
+ * 扫码
+ * @param {string} message 消息描述
+ * @returns {Promise} 调用结果
+ */
+function scanCode(message = "扫描二维码") {
+  return callNative("scanCode", message);
+}
+
+/**
+ * 选择图片
+ * @param {string} message 消息描述
+ * @returns {Promise} 调用结果
+ */
+function chooseImage(message = "选择图片") {
+  return callNative("chooseImage", message);
+}
+
+/**
+ * 拨打电话
+ * @param {string} phoneNumber 电话号码
+ * @param {string} message 消息描述
+ * @returns {Promise} 调用结果
+ */
+function makePhoneCall(phoneNumber, message = "拨打电话") {
+  return callNative("makePhoneCall", message, { phoneNumber });
+}
+
+/**
+ * 设置存储
+ * @param {string} key 存储键
+ * @param {*} value 存储值
+ * @param {string} message 消息描述
+ * @returns {Promise} 调用结果
+ */
+function setStorage(key, value, message = "设置存储数据") {
+  return callNative("setStorage", message, { key, value });
+}
+
+/**
+ * 获取存储
+ * @param {string} key 存储键
+ * @param {string} message 消息描述
+ * @returns {Promise} 调用结果
+ */
+function getStorage(key, message = "获取存储数据") {
+  return callNative("getStorage", message, { key });
+}
+
+/**
+ * 返回小程序
+ * @param {string} message 消息描述
+ * @returns {Promise} 调用结果
+ */
+function goBack(message = "返回小程序") {
+  return callNative("goBack", message);
+}
+
+
+/**
+ * 页面跳转
+ * @param {string} page 目标页面
+ * @param {Object} params 跳转参数
+ */
+function navigateTo(page, params = {}) {
+  const queryString =
+    Object.keys(params).length > 0
+      ? "?" +
+        Object.keys(params)
+          .map((key) => `${key}=${encodeURIComponent(params[key])}`)
+          .join("&")
+      : "";
+  window.location.href = `./${page}.html${queryString}`;
+}
+
+/**
+ * 保存数据到localStorage
+ * @param {string} key 存储键
+ * @param {*} data 存储数据
+ */
+function saveData(key, data) {
+  try {
+    const storageKey = `h5_${window.H5_PAGE_NAME}_${key}`;
+    localStorage.setItem(storageKey, JSON.stringify(data));
+    console.log("💾 数据已保存:", storageKey);
+  } catch (error) {
+    console.error("保存数据失败:", error);
+  }
+}
+
+/**
+ * 从localStorage加载数据
+ * @param {string} key 存储键
+ * @param {*} defaultValue 默认值
+ * @returns {*} 加载的数据
+ */
+function loadData(key, defaultValue = null) {
+  try {
+    const storageKey = `h5_${window.H5_PAGE_NAME}_${key}`;
+    const savedData = localStorage.getItem(storageKey);
+    if (savedData) {
+      return JSON.parse(savedData);
+    }
+  } catch (error) {
+    console.error("加载数据失败:", error);
+  }
+  return defaultValue;
+}
+
+/**
+ * 清除保存的数据
+ * @param {string} key 存储键
+ */
+function clearData(key) {
+  try {
+    const storageKey = `h5_${window.H5_PAGE_NAME}_${key}`;
+    localStorage.removeItem(storageKey);
+    console.log("🗑️ 数据已清除:", storageKey);
+  } catch (error) {
+    console.error("清除数据失败:", error);
+  }
+}
+
+/**
+ * 默认的结果处理函数
+ * @param {Object} data 结果数据
+ */
+function defaultResultHandler(data) {
+  const { action, success, result, error } = data;
+
+  if (success) {
+    console.log(`✅ ${action} 操作成功`, result);
+  } else {
+    console.error(`❌ ${action} 操作失败`, error);
+  }
+}
+
+/**
+ * 初始化页面(可选调用)
+ * @param {string} pageName 页面名称
+ * @param {Function} onResult 结果处理回调,不传则使用默认处理
+ */
+function initPage(pageName, onResult) {
+  setPageName(pageName);
+  checkEnvironment();
+
+  // 如果没有提供自定义处理,使用默认处理
+  const resultHandler = onResult || defaultResultHandler;
+  checkResult(resultHandler);
+}
+
+// ==================== UI工具函数 ====================
+
+/**
+ * 显示Toast效果(H5页面UI)
+ * @param {string} message Toast消息
+ * @param {number} duration 显示时长(毫秒)
+ * @param {string} type 类型:success|error|warning|info
+ */
+function showToastEffect(message, duration = 2000, type = "success") {
+  const colors = {
+    success: "rgba(40, 167, 69, 0.9)",
+    error: "rgba(220, 53, 69, 0.9)",
+    warning: "rgba(255, 193, 7, 0.9)",
+    info: "rgba(23, 162, 184, 0.9)",
+  };
+
+  const toast = document.createElement("div");
+  toast.textContent = message;
+  toast.style.cssText = `
+        position: fixed;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        background: ${colors[type] || colors.success};
+        color: white;
+        padding: 12px 24px;
+        border-radius: 8px;
+        z-index: 9999;
+        font-size: 14px;
+        pointer-events: none;
+        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+    `;
+
+  document.body.appendChild(toast);
+
+  setTimeout(() => {
+    if (toast.parentNode) {
+      toast.parentNode.removeChild(toast);
+    }
+  }, duration);
+}
+
+/**
+ * 显示加载中效果
+ * @param {string} message 加载消息
+ * @returns {Function} 关闭加载的函数
+ */
+function showLoading(message = "加载中...") {
+  const loading = document.createElement("div");
+  loading.innerHTML = `
+        <div style="
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            flex-direction: column;
+            color: white;
+        ">
+            <div style="
+                width: 30px;
+                height: 30px;
+                border: 3px solid rgba(255, 255, 255, 0.3);
+                border-top: 3px solid white;
+                border-radius: 50%;
+                animation: spin 1s linear infinite;
+                margin-bottom: 12px;
+            "></div>
+            <div>${message}</div>
+        </div>
+    `;
+  loading.style.cssText = `
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        background: rgba(0, 0, 0, 0.7);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        z-index: 10000;
+        font-size: 14px;
+    `;
+
+  // 添加旋转动画
+  if (!document.querySelector("#loading-style")) {
+    const style = document.createElement("style");
+    style.id = "loading-style";
+    style.textContent = `
+            @keyframes spin {
+                0% { transform: rotate(0deg); }
+                100% { transform: rotate(360deg); }
+            }
+        `;
+    document.head.appendChild(style);
+  }
+
+  document.body.appendChild(loading);
+
+  return function closeLoading() {
+    if (loading.parentNode) {
+      loading.parentNode.removeChild(loading);
+    }
+  };
+}
+
+/**
+ * 格式化日期
+ * @param {Date|string|number} date 日期
+ * @param {string} format 格式
+ * @returns {string} 格式化后的日期
+ */
+function formatDate(date, format = "YYYY-MM-DD HH:mm:ss") {
+  const d = new Date(date);
+  const year = d.getFullYear();
+  const month = String(d.getMonth() + 1).padStart(2, "0");
+  const day = String(d.getDate()).padStart(2, "0");
+  const hours = String(d.getHours()).padStart(2, "0");
+  const minutes = String(d.getMinutes()).padStart(2, "0");
+  const seconds = String(d.getSeconds()).padStart(2, "0");
+
+  return format
+    .replace("YYYY", year)
+    .replace("MM", month)
+    .replace("DD", day)
+    .replace("HH", hours)
+    .replace("mm", minutes)
+    .replace("ss", seconds);
+}

+ 244 - 0
js/mp_common/common.js

@@ -0,0 +1,244 @@
+/**
+ * H5公共工具函数
+ * 避免在多个文件中重复定义相同的函数
+ */
+
+// 获取URL参数的工具函数
+function getUrlParams() {
+  const params = {}
+  const urlSearchParams = new URLSearchParams(window.location.search)
+  for (const [key, value] of urlSearchParams) {
+    params[key] = decodeURIComponent(value)
+  }
+  return params
+}
+
+// 获取设备信息 (优先从URL参数获取,备用从localStorage获取)
+function getDeviceInfo() {
+  const urlParams = getUrlParams()
+
+  // 优先从URL参数获取
+  let deviceId = urlParams.devId || ''
+  let model = urlParams.sbmc || ''
+
+  // 如果URL参数中没有,尝试从localStorage获取
+  if (!deviceId || !model) {
+    const cachedDeviceInfo = localStorage.getItem('deviceInfo')
+    if (cachedDeviceInfo) {
+      try {
+        const parsed = JSON.parse(cachedDeviceInfo)
+        deviceId = deviceId || parsed.deviceId || ''
+        model = model || parsed.model || ''
+        console.log('🔄 从localStorage获取设备信息:', { deviceId, model })
+      } catch (e) {
+        console.warn('解析localStorage中的设备信息失败:', e)
+      }
+    }
+  }
+
+  // 如果获取到了设备信息,保存到localStorage作为备用
+  if (deviceId && model) {
+    const deviceInfo = { deviceId, model }
+    localStorage.setItem('deviceInfo', JSON.stringify(deviceInfo))
+  }
+
+  return {
+    deviceId: deviceId,
+    model: model
+  }
+}
+
+// 获取JSESSIONID (多源获取,确保可用性)
+function getJSessionId() {
+  // 优先级:localStorage > URL参数 > Cookie
+  let jsessionId = localStorage.getItem('JSESSIONID')
+  
+  if (!jsessionId) {
+    const urlParams = getUrlParams()
+    jsessionId = urlParams.JSESSIONID
+  }
+  
+  if (!jsessionId) {
+    // 尝试从document.cookie中获取
+    const cookies = document.cookie.split(';')
+    for (let cookie of cookies) {
+      const [name, value] = cookie.trim().split('=')
+      if (name === 'JSESSIONID') {
+        jsessionId = value
+        break
+      }
+    }
+  }
+  
+  console.log('🔍 获取JSESSIONID:', {
+    fromLocalStorage: localStorage.getItem('JSESSIONID'),
+    fromURL: getUrlParams().JSESSIONID,
+    final: jsessionId
+  })
+  
+  return jsessionId || ''
+}
+
+// 保存JSESSIONID到localStorage
+function saveJSessionId(jsessionId) {
+  if (jsessionId) {
+    localStorage.setItem('JSESSIONID', jsessionId)
+    console.log('✅ H5 JSESSIONID已保存:', jsessionId)
+  }
+}
+
+// 模拟微信授权码获取 (H5环境下的处理)
+function getMockWechatCode() {
+  // 在H5环境下,我们可以生成一个模拟的code
+  // 实际项目中可能需要接入微信H5授权
+  return 'h5_mock_code_' + Date.now()
+}
+
+// 用户信息管理
+const userManager = {
+  // 保存用户信息
+  saveUserInfo(userInfo) {
+    console.log('💾 H5保存用户信息:', userInfo)
+    localStorage.setItem('userInfo', JSON.stringify(userInfo))
+  },
+
+  // 获取保存的用户信息
+  getSavedUserInfo() {
+    const userInfo = localStorage.getItem('userInfo')
+    return userInfo ? JSON.parse(userInfo) : null
+  },
+
+  // 检查登录状态
+  checkLoginStatus() {
+    const userInfo = localStorage.getItem('userInfo')
+    const jsessionId = localStorage.getItem('JSESSIONID')
+    
+    console.log('🔍 H5检查登录状态:', { 
+      hasUserInfo: !!userInfo, 
+      hasJSessionId: !!jsessionId 
+    })
+
+    return {
+      isLoggedIn: !!(userInfo && jsessionId),
+      userInfo: userInfo ? JSON.parse(userInfo) : null,
+      jsessionId: jsessionId
+    }
+  },
+
+  // 清除登录信息
+  clearLoginInfo() {
+    localStorage.removeItem('userInfo')
+    localStorage.removeItem('JSESSIONID')
+    localStorage.removeItem('deviceInfo')
+    console.log('🧹 已清除登录信息')
+  }
+}
+
+// 获取图片URL (用于图片回显)
+function getImageUrl(path) {
+  if (!path) return ''
+
+  // 如果已经是完整URL,直接返回
+  if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('blob:')) {
+    return path
+  }
+
+  // 获取baseUrl (从request中获取或使用默认值)
+  const baseUrl = window.request?.defaults?.baseURL || window.location.origin
+
+  // 构造图片下载URL
+  return baseUrl + '/service?ssServ=dlByHttp&wdConfirmationCaptchaService=0&type=img&path=' + path
+}
+
+// 获取文件URL (用于文件下载)
+function getFileUrl(path) {
+  if (!path) return ''
+
+  // 如果已经是完整URL,直接返回
+  if (path.startsWith('http://') || path.startsWith('https://')) {
+    return path
+  }
+
+  // 获取baseUrl
+  const baseUrl = window.request?.defaults?.baseURL || window.location.origin
+
+  // 构造文件下载URL
+  return baseUrl + '/service?ssServ=dlByHttp&wdConfirmationCaptchaService=0&type=file&path=' + path
+}
+
+// 格式化日期时间 (使用 dayjs)
+function formatDate(dateStr, format = 'YYYY-MM-DD HH:mm:ss') {
+  if (!dateStr) {
+    console.log('formatDate: 时间字符串为空')
+    return ''
+  }
+
+  console.log('formatDate 输入:', dateStr, '格式要求:', format)
+
+  // 检查 dayjs 是否可用
+  if (typeof dayjs === 'undefined') {
+    console.error('❌ dayjs 未加载,无法格式化时间')
+    return dateStr
+  }
+
+  // 清理字符串:移除特殊空格字符(如 \u202F),替换为普通空格
+  const cleanedDateStr = String(dateStr)
+    .replace(/[\u202F\u00A0]/g, ' ')  // 替换不间断空格
+    .replace(/\s+/g, ' ')              // 多个空格合并为一个
+    .trim()
+
+  console.log('清理后的字符串:', cleanedDateStr)
+
+  // 尝试使用原生 Date 解析(兼容性最好)
+  let date = null
+  try {
+    const jsDate = new Date(cleanedDateStr)
+    if (!isNaN(jsDate.getTime())) {
+      date = dayjs(jsDate)
+      console.log('✅ 使用 Date 解析成功')
+    }
+  } catch (e) {
+    console.warn('Date 解析失败:', e)
+  }
+
+  // 如果 Date 解析失败,尝试直接用 dayjs
+  if (!date || !date.isValid()) {
+    date = dayjs(cleanedDateStr)
+    console.log('尝试使用 dayjs 直接解析')
+  }
+
+  console.log('dayjs 解析结果 isValid:', date ? date.isValid() : 'null')
+
+  if (!date || !date.isValid()) {
+    console.warn('⚠️ 无效的日期格式:', dateStr)
+    return '' // 解析失败返回空字符串
+  }
+
+  // dayjs 的格式化 (支持常见格式)
+  // dayjs 使用大写的格式标记: YYYY-MM-DD HH:mm:ss
+  const result = date.format(format)
+  console.log('格式化结果:', result)
+
+  return result
+}
+
+// 导出到全局
+window.getUrlParams = getUrlParams
+window.getDeviceInfo = getDeviceInfo
+window.getJSessionId = getJSessionId
+window.saveJSessionId = saveJSessionId
+window.getMockWechatCode = getMockWechatCode
+window.userManager = userManager
+window.getImageUrl = getImageUrl
+window.getFileUrl = getFileUrl
+window.formatDate = formatDate
+
+// 同时挂载到SS.utils下(兼容组件调用)
+if (!window.SS.utils) {
+  window.SS.utils = {}
+}
+window.SS.utils.getImageUrl = getImageUrl
+window.SS.utils.getFileUrl = getFileUrl
+window.SS.utils.formatDate = formatDate
+
+console.log('✅ H5公共工具已加载')

File diff suppressed because it is too large
+ 9 - 0
js/mp_cropper/cropper.min.js


File diff suppressed because it is too large
+ 0 - 0
js/mp_dayjs/dayjs.min.js


File diff suppressed because it is too large
+ 1 - 0
js/mp_jq/jquery1.11.3.min.js


File diff suppressed because it is too large
+ 0 - 0
js/mp_jweixin/jweixin-1.6.0.js


+ 418 - 0
js/mp_request/request.js

@@ -0,0 +1,418 @@
+/**
+ * H5版本的请求工具
+ * 基于axios,兼容小程序的request接口
+ * 依赖:common.js (提供公共工具函数)
+ */
+
+// 环境配置
+const env = {
+  baseUrl: "https://m.hfdcschool.com",
+  // baseUrl: 'https://yx.newfeifan.cn'
+};
+
+// Loading 管理器 (H5版本)
+const loadingManager = {
+  loadingCount: 0,
+  loadingTimer: null,
+  loadingElement: null,
+
+  // 显示 loading
+  show(options = {}) {
+    const config = {
+      title: "加载中...",
+      mask: true,
+      delay: 300,
+      ...options,
+    };
+
+    // 如果已经有 loading 在显示,只增加计数
+    if (this.loadingCount > 0) {
+      this.loadingCount++;
+      return;
+    }
+
+    // 延迟显示,避免快速请求造成闪烁
+    this.loadingTimer = setTimeout(() => {
+      if (this.loadingCount > 0) {
+        this.createLoadingElement(config.title);
+      }
+    }, config.delay);
+
+    this.loadingCount++;
+  },
+
+  // 创建Loading元素
+  createLoadingElement(title) {
+    if (this.loadingElement) return;
+
+    this.loadingElement = document.createElement("div");
+    this.loadingElement.className = "h5-loading-mask";
+    this.loadingElement.innerHTML = `
+      <div class="h5-loading-content">
+        <div class="h5-loading-spinner"></div>
+        <div class="h5-loading-text">${title}</div>
+      </div>
+    `;
+
+    // 添加样式
+    const style = document.createElement("style");
+    style.textContent = `
+      .h5-loading-mask {
+        position: fixed;
+        top: 0;
+        left: 0;
+        right: 0;
+        bottom: 0;
+        background: rgba(0, 0, 0, 0.5);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        z-index: 10000;
+      }
+      .h5-loading-content {
+        background: white;
+        padding: 20px;
+        border-radius: 8px;
+        text-align: center;
+        min-width: 120px;
+      }
+      .h5-loading-spinner {
+        width: 30px;
+        height: 30px;
+        border: 3px solid #f3f3f3;
+        border-top: 3px solid #40ac6d;
+        border-radius: 50%;
+        animation: h5-spin 1s linear infinite;
+        margin: 0 auto 10px;
+      }
+      @keyframes h5-spin {
+        0% { transform: rotate(0deg); }
+        100% { transform: rotate(360deg); }
+      }
+      .h5-loading-text {
+        color: #333;
+        font-size: 14px;
+      }
+    `;
+    document.head.appendChild(style);
+    document.body.appendChild(this.loadingElement);
+  },
+
+  // 隐藏 loading
+  hide() {
+    this.loadingCount = Math.max(0, this.loadingCount - 1);
+
+    if (this.loadingCount === 0) {
+      // 清除延迟显示的定时器
+      if (this.loadingTimer) {
+        clearTimeout(this.loadingTimer);
+        this.loadingTimer = null;
+      }
+
+      // 移除 loading 元素
+      if (this.loadingElement) {
+        document.body.removeChild(this.loadingElement);
+        this.loadingElement = null;
+      }
+    }
+  },
+
+  // 强制隐藏所有 loading
+  forceHide() {
+    this.loadingCount = 0;
+    if (this.loadingTimer) {
+      clearTimeout(this.loadingTimer);
+      this.loadingTimer = null;
+    }
+    if (this.loadingElement) {
+      document.body.removeChild(this.loadingElement);
+      this.loadingElement = null;
+    }
+  },
+};
+
+// 注意:getUrlParams, getDeviceInfo, getJSessionId, saveJSessionId
+// 这些函数已在 common.js 中定义,这里直接使用全局函数
+
+const request = {
+  async get(url, params = {}, options = {}) {
+    return this.request(url, "GET", params, options);
+  },
+
+  async post(url, data = {}, options = {}) {
+    return this.request(url, "POST", data, options);
+  },
+
+  async put(url, data = {}, options = {}) {
+    return this.request(url, "PUT", data, options);
+  },
+
+  async delete(url, data = {}, options = {}) {
+    return this.request(url, "DELETE", data, options);
+  },
+
+  async request(url, method, data, options = {}) {
+    // 解析 loading 配置
+    let loadingConfig;
+    if (options.loading === false) {
+      loadingConfig = false;
+    } else {
+      loadingConfig = {
+        show: true,
+        title: "加载中...",
+        mask: true,
+        delay: 300,
+        timeout: 10000,
+        ...(typeof options.loading === "object" ? options.loading : {}),
+      };
+    }
+
+    // 解析请求配置
+    const requestConfig = {
+      timeout: 15000,
+      ...options.request,
+    };
+
+    const shouldShowLoading =
+      loadingConfig !== false && loadingConfig.show !== false;
+
+    // 显示 loading
+    if (shouldShowLoading) {
+      loadingManager.show(loadingConfig);
+    }
+
+    // 获取设备信息
+    const deviceInfo = getDeviceInfo();
+    const devId = deviceInfo.deviceId || "";
+    const sbmc = deviceInfo.model || "";
+
+    // 处理URL,添加设备参数
+    const separator = url.includes("?") ? "&" : "?";
+    const finalUrl = `${url}${separator}devId=${devId}&sbmc=${sbmc}`;
+
+    // 超时处理
+    let timeoutTimer = null;
+    if (shouldShowLoading && loadingConfig.timeout) {
+      timeoutTimer = setTimeout(() => {
+        loadingManager.hide();
+        if (typeof showToastEffect !== "undefined") {
+          showToastEffect("请求超时", 3000, "error");
+        } else {
+          // alert('请求超时')
+        }
+      }, loadingConfig.timeout);
+    }
+
+    try {
+      // 配置axios请求
+      const axiosConfig = {
+        url: `${env.baseUrl}${finalUrl}`,
+        method: method.toLowerCase(),
+        timeout: requestConfig.timeout,
+        headers: {},
+      };
+
+      // 处理请求数据
+      if (method.toUpperCase() === "GET") {
+        axiosConfig.params = data;
+      } else {
+        if (options.formData) {
+          axiosConfig.headers["Content-Type"] =
+            "application/x-www-form-urlencoded";
+          // 转换为表单格式
+          const formData = new URLSearchParams();
+          Object.keys(data).forEach((key) => {
+            const value = data[key];
+            // 如果是数组,为每个元素单独添加参数(同名参数重复)
+            if (Array.isArray(value)) {
+              value.forEach((item) => {
+                formData.append(key, item);
+              });
+            } else {
+              formData.append(key, value);
+            }
+          });
+          axiosConfig.data = formData;
+        } else {
+          axiosConfig.headers["Content-Type"] = "application/json";
+          axiosConfig.data = data;
+        }
+      }
+
+      // 发送请求
+      const response = await axios(axiosConfig);
+
+      // 清除超时定时器
+      if (timeoutTimer) {
+        clearTimeout(timeoutTimer);
+      }
+
+      // 检查服务器处理错误
+      if (
+        response.data &&
+        response.data.msg &&
+        response.data.msg.includes("页面执行时错误")
+      ) {
+        throw new Error("服务器处理错误");
+      }
+
+      // 检查没有服务授权 - 直接回到小程序
+      if (
+        response.data &&
+        response.data.msg &&
+        response.data.msg.includes("没有服务授权")
+      ) {
+        handleH5NoServiceAuth(response.data.msg);
+        throw new Error(response.data.msg);
+      }
+
+      // 检查登录过期 - 根据实际响应体格式
+      if (
+        response &&
+        (response.data.errorcode === 1 ||
+          response.data.msg === "登录已失效,请重新登录" ||
+          response.data.message === "登录过期" ||
+          response.data.error === "UNAUTHORIZED")
+      ) {
+        console.log("H5检测到登录过期,触发自动登录", response);
+        handleH5LoginExpired();
+        throw new Error(response.msg || "登录过期");
+      }
+
+      // 返回与小程序兼容的格式
+      return {
+        data: response.data,
+      };
+    } catch (error) {
+      // 清除超时定时器
+      if (timeoutTimer) {
+        clearTimeout(timeoutTimer);
+      }
+      // 处理错误
+      let errorMessage = "请求失败";
+      if (error.response) {
+        errorMessage = `请求失败: ${error.response.status}`;
+      } else if (error.request) {
+        errorMessage = "网络连接失败";
+      } else {
+        errorMessage = error.message || "未知错误";
+      }
+
+      if (typeof showToastEffect !== "undefined") {
+        showToastEffect(errorMessage, 3000, "error");
+      } else {
+        // alert(errorMessage)
+      }
+
+      throw error;
+    } finally {
+      // 隐藏 loading
+      if (shouldShowLoading) {
+        loadingManager.hide();
+      }
+    }
+  },
+};
+
+// H5登录过期处理
+const handleH5LoginExpired = () => {
+  console.log("🔒 H5处理登录过期");
+
+  // 获取yhsbToken,有token就自动登录,没有就跳转登录页
+  const userInfo = localStorage.getItem("userInfo");
+  let yhsbToken = "";
+
+  if (userInfo) {
+    try {
+      const userData = JSON.parse(userInfo);
+      yhsbToken = userData.yhsbToken;
+    } catch (e) {
+      console.error("解析用户信息失败:", e);
+    }
+  }
+
+  if (yhsbToken) {
+    // 直接调用自动登录接口(和自动登录页面一样的逻辑)
+    request
+      .post(
+        `/service?ssServ=ssLogin&wdConfirmationCaptchaService=0&mdToken=${yhsbToken}`,
+        { mdToken: yhsbToken },
+        { loading: false }
+      )
+      .then((response) => {
+        if (response && response.data) {
+          console.log("✅ H5自动登录成功");
+
+          // 构建用户数据(和自动登录页面一样)
+          const userData = {
+            devId: response.data.devId,
+            sbmc: response.data.sbmc,
+            sessId: response.data.sessId,
+            userId: response.data.userId,
+            xm: response.data.xm,
+            yhsbToken: response.data.yhsbToken,
+            onlineToken: response.data.onlineToken,
+          };
+
+          // 保存用户信息到H5本地存储
+          if (window.h5UserApi) {
+            window.h5UserApi.saveUserInfo(userData);
+          }
+
+          // 通知小程序更新token
+          if (typeof callNative === "function") {
+            callNative("loginSuccess", "H5自动登录成功", {
+              success: true,
+              userInfo: userData,
+              isAutoLogin: true,
+            });
+          }
+
+          // 刷新当前页面
+          setTimeout(() => window.location.reload(), 1000);
+        }
+      })
+      .catch((error) => {
+        console.error("❌ H5自动登录失败:", error);
+        window.location.href = "/page/login.html?from=expired";
+      });
+  } else {
+    console.log("⚠️ 未找到yhsbToken,跳转登录页面");
+    window.location.href = "/page/login.html?from=expired";
+  }
+};
+
+// H5没有服务授权处理
+const handleH5NoServiceAuth = (errorMsg) => {
+  // 显示错误提示
+  if (typeof showToastEffect !== "undefined") {
+    // showToastEffect('没有服务授权,即将返回', 2000, 'error');
+  } else {
+  }
+
+  // 延迟一下让用户看到提示,然后返回小程序
+  setTimeout(() => {
+    // 通知小程序返回
+    if (typeof callNative === "function") {
+      callNative("noServiceAuth", "没有服务授权", {
+        error: true,
+        message: errorMsg,
+        action: "goBack",
+      });
+    } else {
+      // 降级处理:直接关闭页面
+      if (window.history.length > 1) {
+        window.history.back();
+      } else {
+        window.close();
+      }
+    }
+  }, 1500);
+};
+
+// 导出到全局
+window.request = request;
+window.handleH5LoginExpired = handleH5LoginExpired;
+window.handleH5NoServiceAuth = handleH5NoServiceAuth;
+
+console.log("✅ H5 Request工具已加载");

+ 2747 - 0
js/mp_ss_components/mp-ss-components.js

@@ -0,0 +1,2747 @@
+// H5版本的小程序组件库
+// 参考 alf/ss-components.js 的组件形式
+
+(function () {
+	const { ref, createApp, watch, inject, onMounted, onBeforeUnmount, computed } = Vue;
+
+	// ===== 公共上传函数 =====
+	/**
+	 * 统一文件上传函数
+	 * @param {File|Blob} file - 文件或Blob对象
+	 * @param {String} type - 'image' | 'file'
+	 * @param {String} fileName - 文件名(可选)
+	 * @returns {Promise<String>} 服务器路径
+	 */
+	async function uploadFile(file, type = 'image', fileName) {
+		try {
+			window.showToast?.('上传中...', 'loading');
+
+			const formData = new FormData();
+			const name = fileName || (file instanceof Blob ? 'file' : file.name);
+			formData.append('fileEdit', file, name);
+			formData.append('application', '');
+
+			// 根据类型选择接口参数
+			const apiType = type === 'image' ? 'img' : 'file';
+
+			const result = await window.request.post(
+				`/service?ssServ=ulByHttp&type=${apiType}`,
+				formData,
+				{ loading: false }
+			);
+
+			if (result?.data?.fileList?.[0]?.path) {
+				const serverPath = result.data.fileList[0].path;
+				window.showToast?.('上传成功', 'success');
+				return serverPath;
+			} else {
+				throw new Error('上传返回数据格式错误');
+			}
+		} catch (error) {
+			console.error('上传失败:', error);
+			window.showToast?.('上传失败,请重试', 'error');
+			throw error;
+		}
+	}
+
+	const SsCommonIcon = {
+		name: "SsCommonIcon",
+		props: {
+			class: {
+				type: String,
+				required: true,
+			},
+		},
+		setup(props) {
+			const { h } = Vue;
+			return () =>
+				h("i", {
+					class: props.class + " common-icon",
+				});
+		},
+	};
+
+	// ss-input 智能输入组件
+	const SsInput = {
+		name: 'SsInput',
+		inheritAttrs: false, // 不直接继承属性到组件根元素
+		props: {
+			// v-model 绑定的值
+			modelValue: {
+				type: [String, Number],
+				default: '',
+			},
+			// 字段名称
+			name: {
+				type: String,
+				default: '',
+			},
+			// 占位符
+			placeholder: {
+				type: String,
+				default: '请输入',
+			},
+			// 错误提示
+			errTip: {
+				type: String,
+				default: '',
+			},
+		},
+		emits: ['update:modelValue', 'input', 'blur', 'change', 'focus'],
+		setup(props, { emit }) {
+			const inputValue = ref(props.modelValue || '');
+			const validationState = ref({
+				hasError: false,
+				errorMessage: '',
+				isRequired: false,
+				isEmpty: true,
+				hasInteracted: false, // 是否已经交互过
+				isSubmitMode: false, // 是否处于提交模式
+			});
+
+			// 从ValidatedTd注入事件处理函数(兼容小程序方式)
+			const onInputInject = inject('onInput', null);
+			const onBlurInject = inject('onBlur', null);
+
+			// 检查是否为必填字段
+			const checkRequired = () => {
+				if (window.ssVm && props.name) {
+					validationState.value.isRequired = window.ssVm.isRequired(
+						props.name
+					);
+				}
+			};
+
+			// 更新父级td的class
+			const updateTdClass = () => {
+				// 找到父级td元素
+				const inputElement = document.querySelector(
+					`input[name="${props.name}"]`
+				);
+				if (inputElement) {
+					const tdElement = inputElement.closest('td');
+					if (tdElement) {
+						// 移除所有校验相关的class
+						tdElement.classList.remove('td-required', 'td-error');
+
+						// 逻辑分离:
+						// 1. 初始状态:必填且为空 → 只有左侧红线 (td-required)
+						// 2. 实时校验失败 → 左侧红线 + 底部红线 + 错误文字 (td-error)
+
+						if (
+							validationState.value.hasError &&
+							validationState.value.hasInteracted
+						) {
+							// 用户交互后校验失败:左侧红线 + 底部红线 + 错误文字
+							tdElement.classList.add('td-error');
+						} else if (
+							validationState.value.isRequired &&
+							validationState.value.isEmpty
+						) {
+							// 必填且为空:只有左侧红线
+							tdElement.classList.add('td-required');
+						}
+					}
+				}
+			};
+
+			// 校验字段
+			const validateField = (value) => {
+				if (window.ssVm && props.name) {
+					const result = window.ssVm.validateField(props.name);
+					validationState.value.hasError = !result.valid;
+					validationState.value.errorMessage = result.message || '';
+					validationState.value.isEmpty =
+						!value || value.trim() === '';
+
+					// 更新td的class
+					setTimeout(updateTdClass, 0);
+
+					return result;
+				}
+				return { valid: true, message: '' };
+			};
+
+			// 监听props变化
+			watch(
+				() => props.modelValue,
+				(newVal) => {
+					inputValue.value = newVal;
+					validateField(newVal);
+				}
+			);
+
+			// 挂载时初始化
+			onMounted(() => {
+				checkRequired();
+				validateField(inputValue.value);
+
+				// 监听ssVm规则更新事件
+				const inputElement = document.querySelector(
+					`input[name="${props.name}"]`
+				);
+				if (inputElement) {
+					inputElement.addEventListener('ssvm-rules-updated', () => {
+						console.log(`收到规则更新通知: ${props.name}`);
+						checkRequired();
+						validateField(inputValue.value);
+						updateTdClass();
+					});
+				}
+
+				// 确保初始状态正确显示,延迟更长时间确保DOM完全渲染
+				setTimeout(() => {
+					updateTdClass();
+				}, 200);
+			});
+
+			// 事件处理函数
+			const handleInput = (event) => {
+				const value = event.target.value;
+				inputValue.value = value;
+
+				// 标记已交互
+				validationState.value.hasInteracted = true;
+
+				// 1. 支持v-model
+				emit('update:modelValue', value);
+				emit('input', event);
+
+				// 2. 内部校验(实时校验)
+				validateField(value);
+
+				// 3. 兼容小程序inject方式
+				if (onInputInject) {
+					onInputInject(event);
+				}
+			};
+
+			const handleBlur = (event) => {
+				// 标记已交互
+				validationState.value.hasInteracted = true;
+
+				emit('blur', event);
+
+				// 失焦时再次校验
+				validateField(inputValue.value);
+
+				// 兼容小程序inject方式
+				if (onBlurInject) {
+					onBlurInject(event);
+				}
+			};
+
+			const handleFocus = (event) => {
+				emit('focus', event);
+			};
+
+			return {
+				inputValue,
+				validationState,
+				handleInput,
+				handleBlur,
+				handleFocus,
+			};
+		},
+		template: `
+		<!-- 移动端简化的错误提示 - 显示在输入框右下角 -->
+        <div v-if="validationState.hasError && validationState.hasInteracted" class="ss-input__error">
+          {{ validationState.errorMessage }}
+        </div>
+      <div class="ss-input">
+        <input
+          class="ss-input__field"
+          :name="name"
+          type="text"
+          :value="inputValue"
+          :placeholder="placeholder"
+          @input="handleInput"
+          @blur="handleBlur"
+          @focus="handleFocus"
+          autocomplete="off"
+          v-bind="$attrs"
+        />
+
+      </div>
+    `,
+	};
+
+	// icon 图标组件 - 从小程序转换
+	const Icon = {
+		name: 'Icon',
+		props: {
+			// 图标名称,对应iconfont中的类名,如'icon-home'
+			name: {
+				type: String,
+				required: true,
+			},
+			// 图标颜色
+			color: {
+				type: String,
+				default: '#000',
+			},
+			// 图标大小,单位rpx (H5中转换为px)
+			size: {
+				type: [Number, String],
+				default: 32,
+			},
+		},
+		emits: ['click'],
+		setup(props, { emit }) {
+			// 计算完整的图标类名
+			const iconClass = computed(() => {
+				return props.name;
+			});
+
+			// 计算样式
+			const iconStyle = computed(() => ({
+				color: props.color,
+				fontSize: parseInt(props.size) / 2 + 'px', // rpx转px,除以2
+				verticalAlign: 'middle',
+			}));
+
+			// 点击事件处理
+			const handleClick = (event) => {
+				emit('click', event);
+			};
+
+			return {
+				iconClass,
+				iconStyle,
+				handleClick,
+			};
+		},
+		template: `
+			<span
+				:class="['iconfont', iconClass]"
+				:style="iconStyle"
+				@click="handleClick"
+			></span>
+		`,
+	};
+
+	// ss-card 卡片组件 - 从小程序转换
+	const SsCard = {
+		name: 'SsCard',
+		props: {
+			item: {
+				type: Object,
+				default: () => ({}),
+			},
+		},
+		emits: ['click', 'buttonClick'],
+		setup(props, { emit }) {
+			// 状态
+			const showButtonMenu = ref(false);
+
+			// 计算属性
+			const hasButtons = computed(() => {
+				return (
+					props.item?.buttons &&
+					Array.isArray(props.item.buttons) &&
+					props.item.buttons.length > 0
+				);
+			});
+
+			const isMultipleButtons = computed(() => {
+				return props.item?.buttons && props.item.buttons.length > 1;
+			});
+
+			const isSingleButton = computed(() => {
+				return props.item?.buttons && props.item.buttons.length === 1;
+			});
+
+			// 处理卡片点击
+			const handleCardClick = () => {
+				// 如果菜单打开,先关闭菜单
+				if (showButtonMenu.value) {
+					showButtonMenu.value = false;
+					return;
+				}
+
+				emit('click');
+			};
+
+			// 处理设置按钮点击
+			const handleSettingClick = () => {
+				if (isSingleButton.value) {
+					// 只有一个按钮,直接执行
+					handleButtonClick(props.item.buttons[0], 0);
+				} else if (isMultipleButtons.value) {
+					// 先记录当前状态
+					const wasOpen = showButtonMenu.value;
+
+					// 关闭其他卡片的菜单
+					document.dispatchEvent(
+						new CustomEvent('closeAllCardMenus')
+					);
+
+					// 切换当前菜单状态
+					showButtonMenu.value = !wasOpen;
+				}
+			};
+
+			// 关闭菜单
+			const closeMenu = () => {
+				showButtonMenu.value = false;
+			};
+
+			// 处理按钮点击
+			const handleButtonClick = (btn, index) => {
+				showButtonMenu.value = false;
+
+				// 执行按钮的回调
+				if (btn.onclick && typeof btn.onclick === 'function') {
+					btn.onclick();
+				}
+
+				// 触发组件事件
+				emit('buttonClick', { button: btn, index, item: props.item });
+			};
+
+			// 监听全局关闭事件
+			onMounted(() => {
+				document.addEventListener('closeAllCardMenus', closeMenu);
+			});
+
+			// H5环境下的清理逻辑
+			// 注意:Vue 3的beforeUnmount在某些H5环境下可能不可用
+			// 这里暂时省略,依赖页面刷新时的自动清理
+
+			return {
+				showButtonMenu,
+				hasButtons,
+				isMultipleButtons,
+				isSingleButton,
+				handleCardClick,
+				handleSettingClick,
+				handleButtonClick,
+			};
+		},
+		template: `
+			<div class="ss-card" @click="handleCardClick">
+				<!-- 右上角设置按钮 - 只有buttons且长度>0时显示 -->
+				<div
+					v-if="hasButtons"
+					class="card-setting-header"
+					@click.stop="handleSettingClick"
+				>
+					<div class="setting-icon">
+						<Icon name="icon-chilun" size="32" color="#999"/>
+					</div>
+
+					<!-- 按钮弹窗菜单 - 只有多个按钮时显示 -->
+					<div
+						v-if="showButtonMenu && isMultipleButtons"
+						class="button-menu"
+						@click.stop
+					>
+						<div
+							v-for="(btn, index) in item.buttons"
+							:key="index"
+							class="menu-item"
+							@click="handleButtonClick(btn, index)"
+						>
+							<Icon v-if="btn.icon" :name="btn.icon" size="28" color="inherit"/>
+							<span class="menu-text">{{ btn.title }}</span>
+						</div>
+					</div>
+				</div>
+
+				<!-- 卡片内容 -->
+				<slot></slot>
+			</div>
+		`,
+	};
+
+	// ss-search-button 搜索按钮组件 - 从小程序转换
+	const SsSearchButton = {
+		name: 'SsSearchButton',
+		props: {
+			// 按钮文本
+			text: {
+				type: String,
+				default: '增加',
+			},
+			// 是否禁用
+			disabled: {
+				type: Boolean,
+				default: false,
+			},
+			// 按钮高度
+			height: {
+				type: [String, Number],
+				default: '36px',
+			},
+			// 前置图标名称
+			preIcon: {
+				type: String,
+				default: '',
+			},
+			// 后置图标名称
+			suffixIcon: {
+				type: String,
+				default: '',
+			},
+			// 图标大小
+			iconSize: {
+				type: [String, Number],
+				default: '32',
+			},
+			// 图标颜色
+			iconColor: {
+				type: String,
+				default: '#585d6e',
+			},
+			// 自定义按钮样式
+			customStyle: {
+				type: Object,
+				default: () => ({}),
+			},
+			// 跳转链接(兼容原JSP用法)
+			href: {
+				type: String,
+				default: '',
+			},
+			// 选项列表
+			options: {
+				type: Array,
+				default: () => [],
+			},
+		},
+		emits: ['click', 'optionClick'],
+		setup(props, { emit }) {
+			// 状态
+			const showOptionsMenu = ref(false);
+
+			// 计算属性
+			const hasOptions = computed(() => {
+				return (
+					props.options &&
+					Array.isArray(props.options) &&
+					props.options.length > 0
+				);
+			});
+
+			const hasMultipleOptions = computed(() => {
+				return props.options && props.options.length > 1;
+			});
+
+			const isSingleOption = computed(() => {
+				return props.options && props.options.length === 1;
+			});
+
+			// 按钮样式
+			const buttonStyle = computed(() => ({
+				height:
+					typeof props.height === 'number'
+						? `${props.height}px`
+						: props.height,
+				...props.customStyle,
+			}));
+
+			// 处理按钮点击
+			const handleClick = () => {
+				if (!hasOptions.value) {
+					// 没有选项,直接触发点击事件
+					emit('click');
+				} else if (isSingleOption.value) {
+					// 单个选项,直接执行
+					handleOptionClick(props.options[0], 0);
+				} else if (hasMultipleOptions.value) {
+					// 先记录当前状态
+					const wasOpen = showOptionsMenu.value;
+
+					// 关闭其他按钮的菜单
+					document.dispatchEvent(
+						new CustomEvent('closeAllButtonMenus')
+					);
+
+					// 切换当前菜单状态
+					showOptionsMenu.value = !wasOpen;
+				}
+			};
+
+			// 处理选项点击
+			const handleOptionClick = (option, index) => {
+				showOptionsMenu.value = false;
+
+				// 执行选项的回调
+				if (option.onclick && typeof option.onclick === 'function') {
+					option.onclick();
+				}
+
+				// 触发组件事件
+				emit('optionClick', { option, index });
+			};
+
+			// 关闭菜单
+			const closeMenu = () => {
+				showOptionsMenu.value = false;
+			};
+
+			// 监听全局关闭事件
+			onMounted(() => {
+				document.addEventListener('closeAllButtonMenus', closeMenu);
+			});
+
+			return {
+				showOptionsMenu,
+				hasOptions,
+				hasMultipleOptions,
+				isSingleOption,
+				buttonStyle,
+				handleClick,
+				handleOptionClick,
+			};
+		},
+		template: `
+			<div class="ss-search-button-container" :class="{ open: showOptionsMenu }">
+				<button
+					class="ss-search-button"
+					:style="buttonStyle"
+					@click="handleClick"
+					:disabled="disabled"
+				>
+					<!-- 前置图标插槽 -->
+					<div v-if="preIcon" class="ss-search-button__pre-icon">
+						<Icon :name="preIcon" :size="iconSize" :color="iconColor"/>
+					</div>
+
+					<!-- 按钮文本 -->
+					<span class="ss-search-button__text">{{ text }}</span>
+
+					<!-- 后置图标插槽 -->
+					<div v-if="suffixIcon" class="ss-search-button__suffix-icon">
+						<Icon :name="suffixIcon" :size="iconSize" :color="iconColor"/>
+					</div>
+				</button>
+
+				<!-- 选项弹窗菜单 -->
+				<div
+					v-if="showOptionsMenu && hasMultipleOptions"
+					class="options-menu"
+					@click.stop
+				>
+					<div
+						v-for="(option, index) in options"
+						:key="index"
+						class="option-item"
+						@click="handleOptionClick(option, index)"
+					>
+						<Icon v-if="option.icon" :name="option.icon" size="28" color="inherit"/>
+						<span class="option-text">{{ option.text }}</span>
+					</div>
+				</div>
+			</div>
+		`,
+	};
+
+	// ss-select 下拉选择组件 - 从小程序转换
+	const SsSelect = {
+		name: 'SsSelect',
+		props: {
+			// 选项数组
+			options: {
+				type: Array,
+				default: () => [],
+			},
+			// 字段映射
+			mapping: {
+				type: Object,
+				default: () => ({ text: 'n', value: 'v' }),
+			},
+			// 默认值
+			modelValue: {
+				type: [String, Number],
+				default: '',
+			},
+			// 占位符
+			placeholder: {
+				type: String,
+				default: '请选择',
+			},
+			// 校验配置
+			validation: {
+				type: Object,
+				default: () => ({ enable: false, message: '' }),
+			},
+			// 是否禁用
+			disabled: {
+				type: Boolean,
+				default: false,
+			},
+			// 是否支持搜索
+			searchable: {
+				type: Boolean,
+				default: false,
+			},
+			// 是否支持清空
+			clearable: {
+				type: Boolean,
+				default: false,
+			},
+			// 加载状态
+			loading: {
+				type: Boolean,
+				default: false,
+			},
+			// 宽度设置
+			width: {
+				type: String,
+				default: '100%',
+			},
+			minWidth: {
+				type: String,
+				default: 'unset',
+			},
+			// 功能说明:对齐PC端 ss-objp,用 codebook 在组件内部拉取下拉选项 by xu 2026-02-28
+			cb: {
+				type: String,
+				default: '',
+			},
+			url: {
+				type: String,
+				default: '/service?ssServ=loadObjpOpt&objectpickerdropdown1=1',
+			},
+			inp: {
+				type: [Boolean, String],
+				default: false,
+			},
+			filter: {
+				type: [Object, String],
+				default: null,
+			},
+			autoSelectFirst: {
+				type: Boolean,
+				default: false,
+			},
+		},
+		emits: ['update:modelValue', 'change', 'search', 'clear', 'loaded'],
+		setup(props, { emit }) {
+			// 响应式数据
+			const isOpen = ref(false);
+			const selectedValue = ref(props.modelValue);
+			const searchKeyword = ref('');
+			const remoteOptions = ref([]);
+			const remoteLoading = ref(false);
+
+			const parseFilterObj = () => {
+				if (!props.filter) return {};
+				if (typeof props.filter === 'object') return props.filter;
+				if (typeof props.filter === 'string') {
+					try {
+						const obj = JSON.parse(props.filter);
+						return obj && typeof obj === 'object' ? obj : {};
+					} catch (_) {
+						return {};
+					}
+				}
+				return {};
+			};
+
+			const normalizeResultToOptions = (respData) => {
+				const raw = respData || {};
+				if (Array.isArray(raw.resultList)) {
+					return raw.resultList.map((it) => {
+						if (it && typeof it === 'object') return it;
+						return { n: String(it || ''), v: String(it || '') };
+					});
+				}
+				if (raw.result && typeof raw.result === 'object') {
+					return Object.keys(raw.result).map((k) => ({
+						n: raw.result[k],
+						v: k,
+					}));
+				}
+				if (Array.isArray(raw.objectList)) {
+					return raw.objectList.map((it) => {
+						if (it && typeof it === 'object') return it;
+						return { n: String(it || ''), v: String(it || '') };
+					});
+				}
+				return [];
+			};
+
+			const maybeAutoSelectFirst = (opts) => {
+				if (!props.autoSelectFirst) return;
+				if (!Array.isArray(opts) || opts.length === 0) return;
+				if (selectedValue.value !== undefined && selectedValue.value !== null && selectedValue.value !== '') return;
+				const first = opts[0];
+				if (!first || typeof first !== 'object') return;
+				const value = first[props.mapping.value];
+				if (value === undefined || value === null || value === '') return;
+				selectedValue.value = value;
+				emit('update:modelValue', value);
+				emit('change', value);
+			};
+
+			const needRemoteData = computed(() => !!String(props.cb || '').trim());
+
+			const loadRemoteOptions = async () => {
+				if (!needRemoteData.value) return;
+				if (!window.request || typeof window.request.post !== 'function') return;
+
+				remoteLoading.value = true;
+				try {
+					const objpParam = {
+						input: String(props.inp === true || props.inp === 'true'),
+						codebook: String(props.cb || ''),
+						...parseFilterObj(),
+					};
+					const postData = {
+						objectpickerparam: JSON.stringify(objpParam),
+						objectpickertype: 1,
+						objectpickersearchAll: 1,
+					};
+					const resp = await window.request.post(
+						props.url || '/service?ssServ=loadObjpOpt&objectpickerdropdown1=1',
+						postData,
+						{ loading: false, formData: true }
+					);
+					const opts = normalizeResultToOptions(resp && resp.data ? resp.data : null);
+					remoteOptions.value = opts;
+					emit('loaded', opts);
+					maybeAutoSelectFirst(opts);
+				} catch (e) {
+					remoteOptions.value = [];
+					emit('loaded', []);
+					console.error('[ss-select] loadRemoteOptions failed', props.cb, e);
+				} finally {
+					remoteLoading.value = false;
+				}
+			};
+
+			// 计算属性
+			const optionsList = computed(() => {
+				if (Array.isArray(props.options) && props.options.length > 0) return props.options;
+				if (needRemoteData.value) return remoteOptions.value;
+				return [];
+			});
+			const finalLoading = computed(() => !!props.loading || remoteLoading.value);
+
+			const displayText = computed(() => {
+				if (!selectedValue.value) return props.placeholder;
+
+				const selectedOption = optionsList.value.find(
+					(option) =>
+						option[props.mapping.value] === selectedValue.value
+				);
+
+				return selectedOption
+					? selectedOption[props.mapping.text]
+					: props.placeholder;
+			});
+
+			// 监听 modelValue 变化
+			watch(
+				() => props.modelValue,
+				(newValue) => {
+					selectedValue.value = newValue;
+				}
+			);
+
+			// 切换下拉框
+			const toggleDropdown = () => {
+				if (props.disabled) return;
+				isOpen.value = !isOpen.value;
+			};
+
+			// 选择选项
+			const selectOption = (option) => {
+				const value = option[props.mapping.value];
+				selectedValue.value = value;
+				isOpen.value = false;
+
+				emit('update:modelValue', value);
+				emit('change', value);
+			};
+
+			// 点击外部关闭
+			const handleClickOutside = (event) => {
+				if (!event.target.closest('.ss-select-container')) {
+					isOpen.value = false;
+				}
+			};
+
+			onMounted(() => {
+				document.addEventListener('click', handleClickOutside);
+				loadRemoteOptions();
+				if (!needRemoteData.value && Array.isArray(props.options)) {
+					emit('loaded', props.options);
+				}
+			});
+
+			watch(
+				() => [props.cb, props.url, props.filter],
+				() => {
+					if (!needRemoteData.value) return;
+					loadRemoteOptions();
+				}
+			);
+
+			watch(
+				() => props.options,
+				(newVal) => {
+					if (needRemoteData.value) return;
+					emit('loaded', Array.isArray(newVal) ? newVal : []);
+				}
+			);
+
+			return {
+				isOpen,
+				selectedValue,
+				searchKeyword,
+				optionsList,
+				finalLoading,
+				displayText,
+				toggleDropdown,
+				selectOption,
+			};
+		},
+		template: `
+			<div class="ss-select-container" :class="{ open: isOpen }" @click.stop="toggleDropdown">
+				<!-- 显示区域 -->
+				<div class="ss-select" :class="{ disabled: disabled }">
+					<span class="select-text" :class="{ placeholder: !selectedValue }">{{ displayText }}</span>
+					<div class="select-arrow" :class="{ rotate: isOpen }">
+						<Icon name="icon-xiangxiajiantou" size="32" :color="disabled ? '#ccc' : '#999'"/>
+					</div>
+				</div>
+
+				<!-- 选项列表 -->
+				<div class="ss-options" v-show="isOpen">
+					<!-- 加载状态 -->
+					<div
+						v-if="finalLoading"
+						class="option-item loading-item"
+					>
+						<span class="loading-text">加载中...</span>
+					</div>
+					<!-- 无选项 -->
+					<div
+						v-else-if="optionsList.length === 0"
+						class="option-item no-options"
+					>
+						无选项
+					</div>
+					<!-- 选项列表 -->
+					<div
+						v-else
+						v-for="(option, index) in optionsList"
+						:key="index"
+						class="option-item"
+						:class="{ selected: option[mapping.value] === selectedValue }"
+						@click.stop="selectOption(option)"
+					>
+						{{ option[mapping.text] }}
+					</div>
+				</div>
+			</div>
+		`,
+	};
+
+	// ss-bottom 底部按钮组件
+	const SsBottom = {
+		name: 'SsBottom',
+		props: {
+			// 是否显示审核意见
+			showShyj: {
+				type: Boolean,
+				default: false,
+			},
+			// 审核意见标题
+			shyjTitle: {
+				type: String,
+				default: '审核意见',
+			},
+			// 审核意见占位符
+			shyjPlaceholder: {
+				type: String,
+				default: '请输入审核意见',
+			},
+			divider: {
+				type: Boolean,
+				default: true,
+			},
+			// 按钮配置
+			buttons: {
+				type: Array,
+				default: () => [
+					{ text: '取消', action: 'cancel' },
+					{ text: '保存并提交', action: 'submit' },
+				],
+			},
+		},
+		emits: ['button-click', 'update:shyjValue'],
+		setup(props, { emit }) {
+			const reason = ref('');
+			const activeButtonIndex = ref(-1);
+
+			// 处理按钮点击
+			const handleButtonClick = (button, index) => {
+				emit('button-click', {
+					action: button.action,
+					button: button,
+					index: index,
+					shyjValue: reason.value, // 传递审核意见
+				});
+			};
+
+			// 监听审核意见变化
+			const handleShyjInput = (event) => {
+				const value = event.target.value;
+				reason.value = value;
+				emit('update:shyjValue', value);
+			};
+
+			// 处理按钮按下
+			const handleButtonMouseDown = (index) => {
+				activeButtonIndex.value = index;
+			};
+
+			// 处理按钮释放
+			const handleButtonMouseUp = () => {
+				activeButtonIndex.value = -1;
+			};
+
+			// 处理鼠标离开
+			const handleMouseLeave = () => {
+				activeButtonIndex.value = -1;
+			};
+
+			// 计算按钮样式
+			const getButtonStyle = (button) => {
+				const styles = {};
+
+				// 如果有背景颜色配置
+				if (button.backgroundColor) {
+					styles.backgroundColor = button.backgroundColor;
+				}
+
+				// 如果有字体颜色配置
+				if (button.color) {
+					styles.color = button.color;
+				}
+
+				return styles;
+			};
+
+			// 计算按钮点击样式
+			const getButtonActiveStyle = (button) => {
+				const styles = {};
+
+				// 如果有点击背景色配置,使用点击背景色
+				if (button.clickBgColor) {
+					styles.backgroundColor = button.clickBgColor;
+				} else if (button.backgroundColor) {
+					// 如果没有点击背景色,但有背景色,点击时使用背景色
+					styles.backgroundColor = button.backgroundColor;
+				}
+
+				// 如果有点击字体色配置,使用点击字体色
+				if (button.clickColor) {
+					styles.color = button.clickColor;
+				} else if (button.color) {
+					// 如果没有点击字体色,但有字体色,点击时使用字体色
+					styles.color = button.color;
+				}
+
+				return styles;
+			};
+
+			return {
+				reason,
+				activeButtonIndex,
+				handleButtonClick,
+				handleShyjInput,
+				handleButtonMouseDown,
+				handleButtonMouseUp,
+				handleMouseLeave,
+				getButtonStyle,
+				getButtonActiveStyle,
+			};
+		},
+		template: `
+			<div class="ss-bottom">
+				<!-- 审核意见区域 -->
+				<div v-if="showShyj" class="ss-bottom__opinion">
+					<table class="ss-bottom__opinion-table">
+						<tr>
+							<th class="ss-bottom__opinion-label">{{ shyjTitle }}</th>
+							<td class="ss-bottom__opinion-input">
+								<ss-input
+									:placeholder="shyjPlaceholder"
+									v-model="reason"
+									@input="handleShyjInput"
+								/>
+							</td>
+						</tr>
+					</table>
+				</div>
+				<!-- 按钮区域 -->
+				<div class="ss-bottom__buttons" :class="{ 'ss-bottom__buttons--with-border': !showShyj }">
+					<template v-for="(button, index) in buttons" :key="index">
+						<div
+							class="ss-bottom__button"
+							:class="{
+								'ss-bottom__button--custom': button.backgroundColor || button.color || button.clickBgColor || button.clickColor,
+								'ss-bottom__button--active': activeButtonIndex === index
+							}"
+							:style="activeButtonIndex === index ? getButtonActiveStyle(button) : getButtonStyle(button)"
+							@click="handleButtonClick(button, index)"
+							@mousedown="handleButtonMouseDown(index)"
+							@mouseup="handleButtonMouseUp"
+							@mouseleave="handleMouseLeave"
+							@touchstart="handleButtonMouseDown(index)"
+							@touchend="handleButtonMouseUp"
+						>
+							{{ button.text }}
+						</div>
+						<!-- 分割线,最后一个按钮不显示 -->
+						<div v-if="index < buttons.length - 1 && divider" class="ss-bottom__divider"></div>
+					</template>
+				</div>
+			</div>
+		`,
+	};
+
+	// ===== SsVerify 审核节点链组件 =====
+	const SsVerify = {
+		name: "SsVerify",
+		props: {
+			verifyList: {
+				type: Array,
+				required: true,
+			},
+		},
+		setup(props) {
+			const toggleOpen = (item) => {
+				item.open = !item.open;
+				// 切换后重新计算连线高度
+				setTimeout(() => {
+					calculateLineHeight();
+				}, 50);
+			};
+
+			// 计算连线高度的函数
+			const calculateLineHeight = () => {
+				const lastOpenGroup = document.querySelector(".group-item-last-open");
+				console.log("lastOpenGroup", lastOpenGroup);
+				if (lastOpenGroup) {
+					// 使用原生JavaScript代替jQuery
+					const nodes = lastOpenGroup.querySelectorAll(".verify-node-container");
+					if (nodes.length) {
+						let totalHeight = 0;
+
+						if (nodes.length === 1) {
+							// 只有一个节点时,连线伸到节点的中间位置
+							const nodeHeight = nodes[0].offsetHeight;
+							const nodeTop = nodes[0].offsetTop;
+							totalHeight = nodeTop + (nodeHeight / 2) - 15; // 减去圆点半径5px
+						} else {
+							// 多个节点时,连线延伸到最后一个节点的中间位置
+							const lastNode = nodes[nodes.length - 1];
+							const lastNodeTop = lastNode.offsetTop;
+							const lastNodeHeight = lastNode.offsetHeight;
+							totalHeight = lastNodeTop + (lastNodeHeight / 2) - 15; // 减去圆点半径5px
+						}
+
+						console.log("节点信息:", {
+							节点总数: nodes.length,
+							计算后的高度: totalHeight,
+							最后节点top: nodes[nodes.length - 1]?.offsetTop,
+							最后节点高度: nodes[nodes.length - 1]?.offsetHeight,
+						});
+
+						lastOpenGroup.style.setProperty(
+							"--group-line-height",
+							`${totalHeight}px`
+						);
+					}
+				}
+			};
+
+			onMounted(() => {
+				setTimeout(() => {
+					calculateLineHeight();
+				}, 100);
+			});
+
+			return {
+				toggleOpen,
+			};
+		},
+		render() {
+			const { h } = Vue;
+			return h(
+				"div",
+				{ class: "verify-nodes" },
+				this.verifyList.map((item, i) =>
+					h(
+						"div",
+						{
+							key: i,
+							class: {
+								"group-item": true,
+								"group-item-last-open":
+									i === this.verifyList.length - 1 && item.open,
+							},
+						},
+						[
+							h(
+								"div",
+								{
+									class: "group-item-title",
+									onClick: () => this.toggleOpen(item),
+								},
+								[
+									h("div", { class: "icon" }, [
+										h("i", {
+											class: item.open ? "common-icon-folder-open common-icon" : "common-icon-folder-close common-icon"
+										}),
+										h(
+											"div",
+											{
+												class: "num",
+												style: { top: item.open ? "60%" : "55%" },
+											},
+											item.children?.length || 0
+										),
+									]),
+									h("div", { class: "name" }, item.groupName),
+								]
+							),
+							item.open && item.children?.length > 0
+								? h(
+									"div",
+									{ class: "group-item-children" },
+									item.children.map((citem, j) =>
+										h(SsVerifyNode, {
+											key: j,
+											item: citem,
+											// isGroup: i + 1 !== this.verifyList.length,
+											isGroup: true,
+										})
+									)
+								)
+								: null,
+						]
+					)
+				)
+			);
+		},
+	};
+
+	// ===== SsVerifyNode 审核节点组件 =====
+	const SsVerifyNode = {
+		name: "SsVerifyNode",
+		props: {
+			item: {
+				type: Object,
+				required: true,
+			},
+			isGroup: {
+				type: Boolean,
+				default: false,
+			},
+		},
+		render() {
+			const { h } = Vue;
+			return h("div", { class: "verify-node-container" }, [
+				h("div", { class: "info" }, [
+					h("div", { class: "avatar" }, [
+						h("img", {
+							src: this.item.thumb,
+							style: {
+								width: "50px",
+								height: "50px",
+								borderRadius: "50%",
+							},
+						}),
+					]),
+					h("div", { class: "desc" }, [
+						h("div", this.item.name),
+						h("div", this.item.role),
+					]),
+					h("div", { class: "link" }, [
+						h("div", [
+							this.item.video
+								? h("i", { class: "common-icon-video common-icon" })
+								: null,
+							this.item.link
+								? h("i", { class: "common-icon-paper-clip common-icon" })
+								: null,
+						]),
+					]),
+				]),
+				h(
+					"div",
+					{
+						class: {
+							description: true,
+							link: this.isGroup,
+						},
+						attrs: { "data-num": "3" },
+					},
+					[h("div", this.item.description)]
+				),
+				h("div", { class: "time" }, this.item.time),
+			]);
+		},
+	};
+
+	// ===== SsOnoffButton 开关按钮 =====
+	const SsOnoffButton = {
+		name: 'SsOnoffButton',
+		props: {
+			// 字段名称,用于表单校验
+			name: { type: String, required: true },
+			// 显示标签
+			label: { type: String, required: true },
+			// 按钮的值
+			value: { type: [String, Number], required: true },
+			// 宽度设置
+			width: { type: String, default: '' },
+			// v-model 绑定的值
+			modelValue: { type: [String, Number, Array], default: '' },
+			// 是否多选模式
+			multiple: { type: Boolean, default: false },
+			// 是否禁用
+			disabled: { type: Boolean, default: false }
+		},
+		emits: ['update:modelValue', 'change'],
+		setup(props, { emit }) {
+			// 解析 modelValue,支持逗号分隔的字符串和数组
+			const parseModelValue = (val) => {
+				if (!val) return [];
+
+				// 如果是数组,直接返回字符串数组
+				if (Array.isArray(val)) {
+					return val.map(v => v.toString());
+				}
+
+				// 如果是字符串,按逗号分割
+				const cleanValue = val.toString().replace(/^,+/, ''); // 去掉开头的逗号
+				if (cleanValue.includes('|')) {
+					return cleanValue.split('|');
+				}
+				if (cleanValue.includes(',')) {
+					return cleanValue.split(',');
+				}
+				return cleanValue ? [cleanValue] : [];
+			};
+
+			// 判断当前按钮是否选中
+			const isChecked = computed(() => {
+				if (props.multiple) {
+					const currentValue = parseModelValue(props.modelValue);
+					return currentValue.includes(props.value.toString());
+				}
+				return props.modelValue === props.value;
+			});
+
+			// 切换选中状态
+			const toggleSelect = () => {
+				// 如果禁用,不执行任何操作
+				if (props.disabled) return;
+
+				if (props.multiple) {
+					// 多选模式
+					const currentValue = parseModelValue(props.modelValue);
+					const index = currentValue.indexOf(props.value.toString());
+					let newValue;
+
+					if (index === -1) {
+						// 添加选项
+						newValue = [...currentValue, props.value.toString()];
+					} else {
+						// 移除选项
+						newValue = currentValue.filter(v => v !== props.value.toString());
+					}
+
+					// 发送更新事件,使用逗号分隔的字符串格式
+					const emitValue = newValue.join(',');
+					emit('update:modelValue', emitValue);
+					emit('change', emitValue, newValue);
+				} else {
+					// 单选模式
+					emit('update:modelValue', props.value);
+					emit('change', props.value);
+				}
+			};
+
+			return { isChecked, toggleSelect };
+		},
+		template: `
+			  <div class="ss-onoff-button" :class="{ checked: isChecked, disabled: disabled }" @click="toggleSelect">
+			    <span class="button-label">{{ label }}</span>
+			    <div class="button-mark">
+			      <span class="form-icon" :class="isChecked ? 'form-icon-onoffbutton-checked' : 'form-icon-onoffbutton-unchecked'"></span>
+			    </div>
+			  </div>
+			`,
+	};
+
+	// ===== SsDatetimePicker 日期时间选择(使用 Vant 4) =====
+	const SsDatetimePicker = {
+		name: 'SsDatetimePicker',
+		props: {
+			mode: { type: String, default: 'date' }, // date | time | datetime
+			placeholder: { type: String, default: '请选择日期' },
+			modelValue: { type: String, default: '' },
+			minDate: { type: String, default: '' },
+			maxDate: { type: String, default: '' },
+			// 字段名称 - 用于ssVm校验
+			name: { type: String, default: '' },
+			// 是否禁用
+			disabled: { type: Boolean, default: false },
+		},
+		emits: ['update:modelValue', 'change', 'confirm', 'cancel', 'open', 'close'],
+		setup(props, { emit }) {
+			const showPicker = ref(false);
+			const showTimePicker = ref(false);
+			const currentStep = ref('date'); // 'date' | 'time'
+
+			// 功能说明:统一在组件层处理 iframe 场景底部按钮显隐,避免每个页面重复绑定 by xu 2026-02-28
+			const notifyParentBottomVisible = (visible) => {
+				try {
+					const fn = window.parent && window.parent.__mpObjInpSetBottomVisible;
+					if (typeof fn === 'function') fn(visible !== false);
+				} catch (_) {}
+			};
+
+			// 功能说明:监听弹层显隐,点遮罩/取消关闭时也恢复父层底部按钮 by xu 2026-03-01
+			watch([showPicker, showTimePicker], ([dateOpen, timeOpen], [prevDateOpen, prevTimeOpen]) => {
+				const hasOpen = !!(dateOpen || timeOpen);
+				const hadOpen = !!(prevDateOpen || prevTimeOpen);
+
+				if (!hadOpen && hasOpen) {
+					emit('open');
+					notifyParentBottomVisible(false);
+					return;
+				}
+
+				if (hadOpen && !hasOpen) {
+					emit('close');
+					notifyParentBottomVisible(true);
+				}
+			});
+
+			// Vant DatePicker 需要数组格式 [year, month, day]
+			const currentDateArray = ref([]);
+			const currentTimeArray = ref(['12', '00']); // [hour, minute]
+			const tempDateStr = ref(''); // 临时存储选择的日期
+
+			// 格式化显示文本
+			const displayText = computed(() => {
+				if (!props.modelValue) return props.placeholder;
+				try {
+					const d = new Date(props.modelValue);
+					if (isNaN(d.getTime())) return props.modelValue;
+
+					const year = d.getFullYear();
+					const month = String(d.getMonth() + 1).padStart(2, '0');
+					const day = String(d.getDate()).padStart(2, '0');
+					const hours = String(d.getHours()).padStart(2, '0');
+					const minutes = String(d.getMinutes()).padStart(2, '0');
+
+					if (props.mode === 'time') {
+						return `${hours}:${minutes}`;
+					} else if (props.mode === 'datetime') {
+						return `${year}-${month}-${day} ${hours}:${minutes}`;
+					}
+					return `${year}-${month}-${day}`;
+				} catch (e) { return props.modelValue; }
+			});
+
+			// 监听 modelValue 变化,转换为数组格式
+			watch(() => props.modelValue, (newVal) => {
+				console.log('📅 modelValue 变化:', newVal);
+				if (newVal) {
+					try {
+						const d = new Date(newVal);
+						if (!isNaN(d.getTime())) {
+							currentDateArray.value = [d.getFullYear(), d.getMonth() + 1, d.getDate()];
+							console.log('📅 转换为数组:', currentDateArray.value);
+						}
+					} catch (e) {
+						console.warn('Invalid date:', newVal);
+					}
+				} else {
+					const today = new Date();
+					currentDateArray.value = [today.getFullYear(), today.getMonth() + 1, today.getDate()];
+				}
+			}, { immediate: true });
+
+			// 确认选择 - 根据模式处理不同的数据
+			const onConfirm = (value) => {
+				console.log('📅 Vant Picker confirm 原始值:', value, 'mode:', props.mode);
+				try {
+					// Vant 返回的是对象,包含 selectedValues 数组
+					const selectedValues = value.selectedValues || value;
+					console.log('📅 selectedValues:', selectedValues);
+
+					if (props.mode === 'time') {
+						// 时间模式:处理时分
+						if (Array.isArray(selectedValues) && selectedValues.length >= 2) {
+							const [hour, minute] = selectedValues;
+							const timeStr = `${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
+							console.log('🕐 转换后的时间字符串:', timeStr);
+							emit('update:modelValue', timeStr);
+							emit('change', timeStr);
+						emit('confirm', timeStr);
+						emit('close');
+						notifyParentBottomVisible(true);
+						showPicker.value = false;
+					}
+					} else if (Array.isArray(selectedValues) && selectedValues.length >= 3) {
+						// 日期模式:处理年月日
+						const [year, month, day] = selectedValues;
+						const dateStr = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
+
+						if (props.mode === 'datetime') {
+							// datetime 模式:先存储日期,然后打开时间选择器
+							tempDateStr.value = dateStr;
+							showPicker.value = false;
+							showTimePicker.value = true;
+							currentStep.value = 'time';
+						} else {
+						// date 模式:直接完成
+						emit('update:modelValue', dateStr);
+						emit('change', dateStr);
+						emit('confirm', dateStr);
+						emit('close');
+						notifyParentBottomVisible(true);
+						showPicker.value = false;
+					}
+					}
+				} catch (e) {
+					console.error('Picker conversion error:', e);
+				}
+			};
+
+			// 时间选择确认
+			const onTimeConfirm = (value) => {
+				try {
+					const selectedValues = value.selectedValues || value;
+					if (Array.isArray(selectedValues) && selectedValues.length >= 2) {
+						const [hour, minute] = selectedValues;
+						const datetimeStr = `${tempDateStr.value} ${hour.padStart(2, '0')}:${minute.padStart(2, '0')}`;
+						emit('update:modelValue', datetimeStr);
+						emit('change', datetimeStr);
+						emit('confirm', datetimeStr);
+						emit('close');
+						notifyParentBottomVisible(true);
+					}
+				} catch (e) {
+					console.error('Time conversion error:', e);
+				}
+				showTimePicker.value = false;
+				currentStep.value = 'date';
+			};
+
+			// 取消选择
+			const onCancel = () => {
+				emit('cancel');
+				showPicker.value = false;
+			};
+
+			const onTimeCancel = () => {
+				emit('cancel');
+				showTimePicker.value = false;
+			};
+
+			// 打开选择器
+			const openPicker = () => {
+				// 如果禁用,不打开选择器
+				if (props.disabled) return;
+				showPicker.value = true;
+			};
+
+			// 计算最小最大日期
+			const minDateObj = computed(() => {
+				return props.minDate ? new Date(props.minDate) : undefined;
+			});
+
+			const maxDateObj = computed(() => {
+				return props.maxDate ? new Date(props.maxDate) : undefined;
+			});
+
+			return {
+				showPicker,
+				showTimePicker,
+				currentDateArray,
+				currentTimeArray,
+				displayText,
+				openPicker,
+				onConfirm,
+				onTimeConfirm,
+				onTimeCancel,
+				onCancel,
+				minDateObj,
+				maxDateObj
+			};
+		},
+		template: `
+			  <div class="ss-datetime-picker ss-mobile-component" :class="{ disabled: disabled }">
+			    <!-- 隐藏的input用于ssVm校验 -->
+			    <input type="hidden" :name="name" :value="modelValue" />
+
+			    <div class="datetime-picker-display" @click="openPicker">
+			      <span class="datetime-picker-value" :class="{ placeholder: !modelValue }">{{ displayText }}</span>
+			    </div>
+
+			    <!-- 日期选择器 -->
+			    <van-popup v-model:show="showPicker" position="bottom" :style="{ zIndex: 10000 }">
+			      <van-date-picker
+			        v-if="mode === 'date' || mode === 'datetime'"
+			        v-model="currentDateArray"
+			        :min-date="minDateObj"
+			        :max-date="maxDateObj"
+			        @confirm="onConfirm"
+			        @cancel="onCancel"
+			        title="选择日期"
+			      />
+			      <van-time-picker
+			        v-if="mode === 'time'"
+			        v-model="currentTimeArray"
+			        @confirm="onConfirm"
+			        @cancel="onCancel"
+			        title="选择时间"
+			      />
+			    </van-popup>
+
+			    <!-- 时间选择器(datetime 模式的第二步) -->
+			    <van-popup v-model:show="showTimePicker" position="bottom" :style="{ zIndex: 10000 }">
+			      <van-time-picker
+			        v-model="currentTimeArray"
+			        @confirm="onTimeConfirm"
+			        @cancel="onTimeCancel"
+			        title="选择时间"
+			      />
+			    </van-popup>
+			  </div>
+			`,
+	};
+
+	const SsConfirm = {
+		name: 'SsConfirm',
+		props: {
+			modelValue: { type: Boolean, default: false },
+			title: { type: String, default: '确认' },
+			content: { type: String, default: '' },
+			maskClosable: { type: Boolean, default: true },
+		},
+		emits: ['update:modelValue', 'confirm', 'cancel', 'close'],
+		setup(props, { emit }) {
+			// 监听弹窗显示状态,控制 body 滚动
+			watch(() => props.modelValue, (newVal) => {
+				console.log('🔔 SsConfirm modelValue 变化:', newVal);
+				if (newVal) {
+					document.body.classList.add('modal-open');
+				} else {
+					document.body.classList.remove('modal-open');
+				}
+			}, { immediate: true }); // 添加 immediate 选项
+
+			const close = () => {
+				console.log('🚪 关闭确认弹窗');
+				emit('update:modelValue', false);
+				emit('close');
+			};
+			const onMask = () => {
+				console.log('👆 点击了遮罩层');
+				if (props.maskClosable) close();
+			};
+			const onCancel = () => {
+				console.log('❌ 点击了取消按钮');
+				emit('cancel');
+				close();
+			};
+			const onConfirm = () => {
+				console.log('✅ 点击了确认按钮,触发 confirm 事件');
+				emit('confirm');
+				close();
+			};
+			return { onMask, onCancel, onConfirm };
+		},
+		template: `
+			  <div v-if="modelValue" class="ss-confirm">
+			    <div class="confirm-mask" @click="onMask"></div>
+			    <div class="confirm-content">
+			      <div class="confirm-header" v-if="title">
+			        <div class="header-title">{{ title }}</div>
+			      </div>
+			      <div class="header-line" v-if="title"></div>
+			      <div class="confirm-body">
+			        <div v-if="content" class="confirm-content-text" v-html="content"></div>
+			        <div class="confirm-slot-content"><slot /></div>
+			      </div>
+			      <div class="confirm-bottom">
+			        <button class="confirm-btn confirm-btn-cancel" @click.stop="onCancel">取消</button>
+			        <button class="confirm-btn confirm-btn-confirm" @click.stop="onConfirm">确认</button>
+			      </div>
+			    </div>
+			  </div>
+			`,
+	};
+
+	// ===== SsImageCropper 纯裁剪组件 =====
+	const SsImageCropper = {
+		name: 'SsImageCropper',
+		props: {
+			// 是否显示裁剪器
+			show: { type: Boolean, default: false },
+			// 图片源(base64 或 URL)
+			src: { type: String, required: true },
+			// 图片形状:circle圆形 | square方形
+			shape: { type: String, required: true },
+			// 裁剪比例(宽/高)
+			aspectRatio: { type: Number, default: 1 },
+			// 输出图片宽度
+			outputWidth: { type: Number, default: 300 },
+			// 输出图片高度
+			outputHeight: { type: Number, default: 300 },
+		},
+		emits: ['update:show', 'confirm', 'cancel'],
+		setup(props, { emit }) {
+			const cropperInstance = ref(null);
+
+			// 监听 show 变化,初始化或销毁 Cropper
+			watch(() => props.show, (newVal) => {
+				if (newVal) {
+					// 等待 DOM 更新后初始化
+					Vue.nextTick(() => {
+						initCropper();
+					});
+				} else {
+					destroyCropper();
+				}
+			});
+
+			// 监听 src 变化,重新初始化 Cropper
+			watch(() => props.src, () => {
+				if (props.show) {
+					Vue.nextTick(() => {
+						initCropper();
+					});
+				}
+			});
+
+			// 初始化 Cropper
+			const initCropper = () => {
+				const imageElement = document.getElementById('ss-image-cropper-img');
+				if (!imageElement || !window.Cropper) return;
+
+				// 销毁旧实例
+				destroyCropper();
+
+				// 根据 shape 属性添加类名
+				const container = document.querySelector('.ss-image-cropper-container');
+				if (container) {
+					if (props.shape === 'circle') {
+						container.classList.add('crop-shape-circle');
+						container.classList.remove('crop-shape-square');
+					} else {
+						container.classList.add('crop-shape-square');
+						container.classList.remove('crop-shape-circle');
+					}
+				}
+
+				cropperInstance.value = new window.Cropper(imageElement, {
+					aspectRatio: props.aspectRatio,
+					viewMode: 1,
+					dragMode: 'move',
+					autoCropArea: 0.8,
+					restore: false,
+					guides: false,  // 关闭辅助线
+					center: false,  // 关闭中心指示器
+					highlight: false,
+					cropBoxMovable: true,
+					cropBoxResizable: true,
+					toggleDragModeOnDblclick: false,
+					minContainerWidth: window.innerWidth,
+					minContainerHeight: window.innerHeight - 50,
+				});
+			};
+
+			// 销毁 Cropper
+			const destroyCropper = () => {
+				if (cropperInstance.value) {
+					cropperInstance.value.destroy();
+					cropperInstance.value = null;
+				}
+			};
+
+			// 取消裁剪
+			const handleCancel = () => {
+				emit('update:show', false);
+				emit('cancel');
+			};
+
+			// 确认裁剪
+			const handleConfirm = () => {
+				if (!cropperInstance.value) return;
+
+				const canvas = cropperInstance.value.getCroppedCanvas({
+					width: props.outputWidth,
+					height: props.outputHeight,
+					imageSmoothingEnabled: true,
+					imageSmoothingQuality: 'high',
+					fillColor: '#fff'
+				});
+
+				canvas.toBlob((blob) => {
+					emit('update:show', false);
+					emit('confirm', blob);
+				}, 'image/jpeg', 0.9);
+			};
+
+			// 处理底部按钮事件
+			const handleCropAction = (data) => {
+				if (data.action === 'cancel') {
+					handleCancel();
+				} else if (data.action === 'confirm') {
+					handleConfirm();
+				}
+			};
+
+			return {
+				handleCropAction,
+			};
+		},
+		template: `
+			<div v-if="show" class="ss-image-cropper-container">
+				<!-- 左上角尺寸显示 -->
+				<div class="crop-size-display">
+					长: {{ outputWidth }}px<br>宽: {{ outputHeight }}px
+				</div>
+
+				<div class="ss-crop-image-container">
+					<img id="ss-image-cropper-img" :src="src" />
+				</div>
+
+				<!-- 使用 ss-bottom 组件 -->
+				<ss-bottom
+					:show-shyj="false"
+					:buttons="[
+						{ text: '取消', action: 'cancel' },
+						{ text: '保存并提交', action: 'confirm' }
+					]"
+					@button-click="handleCropAction"
+				/>
+			</div>
+		`,
+	};
+
+	// ===== SsUploadImage 图片上传裁剪组件(支持单图/多图) =====
+	const SsUploadImage = {
+		name: 'SsUploadImage',
+		props: {
+			// v-model 绑定的值(单图:String,多图:Array)
+			modelValue: { type: [String, Array], default: '' },
+			// 最大上传数量(默认1张,多图时设置大于1)
+			max: { type: Number, default: 1 },
+			// 是否禁用
+			disabled: { type: Boolean, default: false },
+			// 图片宽度(像素) - 必填
+			width: { type: [Number, String], required: true },
+			// 图片高度(像素) - 必填
+			height: { type: [Number, String], required: true },
+			// 图片形状:circle圆形 | square方形 - 必填
+			shape: { type: String, required: true },
+			// 裁剪比例(宽/高)
+			aspectRatio: { type: Number, default: undefined },
+			// 输出图片宽度
+			outputWidth: { type: Number, default: 300 },
+			// 输出图片高度
+			outputHeight: { type: Number, default: 300 },
+		},
+		emits: ['update:modelValue', 'updated'],
+		setup(props, { emit }) {
+			const showCropper = ref(false);
+			const tempImageSrc = ref('');
+
+			// 图片列表(统一用数组管理)
+			const imageList = ref([]);
+
+			// 监听 modelValue 变化,同步到 imageList
+			watch(() => props.modelValue, (newVal) => {
+				if (props.max === 1) {
+					// 单图模式
+					imageList.value = newVal ? [newVal] : [];
+				} else {
+					// 多图模式
+					imageList.value = Array.isArray(newVal) ? [...newVal] : (newVal ? [newVal] : []);
+				}
+			}, { immediate: true });
+
+			// 是否可以继续添加图片
+			const canAddMore = computed(() => {
+				return imageList.value.length < props.max;
+			});
+
+			// 容器样式
+			const itemStyle = computed(() => ({
+				width: typeof props.width === 'number' ? `${props.width}px` : props.width,
+				height: typeof props.height === 'number' ? `${props.height}px` : props.height,
+				borderRadius: props.shape === 'circle' ? '50%' : '8px',
+			}));
+
+			// 获取图片 URL(用于显示)
+			const getImageUrl = (path) => {
+				if (!path) return '/static/images/yishuzhao_nv.svg';
+				if (path.startsWith('http') || path.startsWith('blob:')) {
+					return path;
+				}
+				return window.SS.utils?.getImageUrl?.(path) || path;
+			};
+
+			// 选择图片
+			const selectImage = () => {
+				if (props.disabled || !canAddMore.value) return;
+				const input = document.createElement('input');
+				input.type = 'file';
+				input.accept = 'image/*';
+				input.onchange = (e) => {
+					const file = e.target.files[0];
+					if (file) {
+						const reader = new FileReader();
+						reader.onload = (event) => {
+							tempImageSrc.value = event.target.result;
+							showCropper.value = true;
+						};
+						reader.readAsDataURL(file);
+					}
+				};
+				input.click();
+			};
+
+			// 删除图片
+			const deleteImage = (index) => {
+				const newList = imageList.value.filter((_, i) => i !== index);
+				updateModelValue(newList);
+			};
+
+			// 确认裁剪
+			const handleCropConfirm = async (blob) => {
+				try {
+					const serverPath = await uploadFile(blob, 'image', 'image.jpg');
+					const newList = [...imageList.value, serverPath];
+					updateModelValue(newList);
+					emit('updated', serverPath);
+				} catch (error) {
+					console.error('上传失败:', error);
+				}
+			};
+
+			// 取消裁剪
+			const handleCropCancel = () => {
+				tempImageSrc.value = '';
+			};
+
+			// 更新 modelValue
+			const updateModelValue = (list) => {
+				if (props.max === 1) {
+					// 单图模式:返回 String
+					emit('update:modelValue', list[0] || '');
+				} else {
+					// 多图模式:返回 Array
+					emit('update:modelValue', list);
+				}
+			};
+
+			return {
+				imageList,
+				canAddMore,
+				itemStyle,
+				showCropper,
+				tempImageSrc,
+				getImageUrl,
+				selectImage,
+				deleteImage,
+				handleCropConfirm,
+				handleCropCancel,
+			};
+		},
+		template: `
+			<div class="ss-upload-image-multi">
+				<!-- 图片列表 -->
+				<div class="image-list">
+					<!-- 已上传的图片 -->
+					<div
+						v-for="(img, index) in imageList"
+						:key="index"
+						class="image-item"
+						:style="itemStyle"
+					>
+						<img :src="getImageUrl(img)" class="image-display" />
+						<!-- 删除按钮 -->
+						<div v-if="!disabled" class="image-delete" @click.stop="deleteImage(index)">
+							<Icon name="icon-guanbi" size="32" color="#fff" />
+						</div>
+					</div>
+
+					<!-- 添加按钮 -->
+					<div
+						v-if="canAddMore && !disabled"
+						class="image-item image-add"
+						:style="itemStyle"
+						@click="selectImage"
+					>
+						<Icon name="icon-xiangji" size="48" color="#ccc" />
+						<div class="add-text">{{ imageList.length }}/{{ max }}</div>
+					</div>
+				</div>
+
+				<!-- 裁剪组件 -->
+				<ss-image-cropper
+					v-model:show="showCropper"
+					:src="tempImageSrc"
+					:shape="shape"
+					:aspect-ratio="aspectRatio"
+					:output-width="outputWidth"
+					:output-height="outputHeight"
+					@confirm="handleCropConfirm"
+					@cancel="handleCropCancel"
+				/>
+			</div>
+		`,
+	};
+
+	// ===== SsCarCard 车辆卡片组件 =====
+	const SsCarCard = {
+		name: 'SsCarCard',
+		props: {
+			// 车辆数据
+			carData: {
+				type: Object,
+				default: () => ({})
+			},
+			// 车辆状态:'available' | 'reserved' | 'disabled'
+			status: {
+				type: String,
+				default: 'available',
+				validator: (value) => ['available', 'reserved', 'disabled'].includes(value)
+			}
+		},
+		emits: ['click', 'select'],
+		setup(props, { emit }) {
+			// 计算状态样式类
+			const statusClass = computed(() => {
+				return `status-${props.status}`;
+			});
+
+			// 获取图片URL
+			const getImageUrl = (path) => {
+				if (!path) return '/static/images/default-car.png';
+				if (path.startsWith('http') || path.startsWith('blob:')) {
+					return path;
+				}
+				return window.SS.utils?.getImageUrl?.(path) || path;
+			};
+
+			// 处理卡片点击
+			const handleCardClick = () => {
+				if (props.status === 'disabled') {
+					return; // 禁用状态不响应点击
+				}
+
+				emit('click', props.carData);
+				emit('select', props.carData);
+			};
+
+			return {
+				statusClass,
+				getImageUrl,
+				handleCardClick,
+			};
+		},
+		template: `
+			<div class="car-card" :class="statusClass" @click="handleCardClick">
+				<!-- 第一行:车辆名称 -->
+				<div class="car-title">
+					{{ carData.name || '别克GL8' }}
+				</div>
+
+				<!-- 第二行:左右结构 -->
+				<div class="car-info">
+					<!-- 左边:车辆图片 -->
+					<div class="car-image-container">
+						<img class="car-image" :src="getImageUrl(carData.image)" />
+					</div>
+
+					<!-- 右边:车辆信息 -->
+					<div class="car-details">
+						<div class="detail-item car-name" v-if="carData.wph">
+							{{ carData.wph }}
+						</div>
+						<div class="detail-item seats" v-for="(wp, index) in carData.wpcsList" :key="index">
+							{{ wp.mc }} : {{ wp.sz || wp.zf }}
+						</div>
+						<div class="detail-item car-type">
+							{{ carData.type || '商务车' }}
+						</div>
+					</div>
+				</div>
+			</div>
+		`,
+	};
+
+	// ===== SsSubTab 移动端Tab组件 =====
+	const SsSubTab = {
+		name: 'SsSubTab',
+		props: {
+			// Tab列表数据
+			tabList: {
+				type: Array,
+				required: true,
+			},
+			// 当前激活的Tab索引
+			activeIndex: {
+				type: Number,
+				default: 0,
+			},
+			// 基础URL参数(会传递给每个iframe)
+			baseParams: {
+				type: Object,
+				default: () => ({}),
+			},
+		},
+		emits: ['tab-change'],
+		setup(props, { emit }) {
+			const currentTab = ref(props.activeIndex);
+			const currentTabUrl = ref('');
+
+			// 加载Tab对应的URL
+			const loadTabUrl = (index) => {
+				const tab = props.tabList[index];
+				if (!tab || !tab.dest) return;
+
+				// 构建iframe URL:mp_ + dest + .html
+				const fileName = `mp_${tab.dest}.html`;
+
+				// 构建完整URL,包含所有参数
+				const params = new URLSearchParams({
+					...props.baseParams,
+					service: tab.service || '',
+					param: tab.param || '',
+				});
+
+				currentTabUrl.value = `/page/${fileName}?${params.toString()}`;
+				console.log('🔄 切换到Tab:', tab.desc || tab.title, '加载页面:', currentTabUrl.value);
+			};
+
+			// 监听 activeIndex 变化
+			watch(() => props.activeIndex, (newIndex) => {
+				currentTab.value = newIndex;
+				loadTabUrl(newIndex);
+			}, { immediate: true });
+
+			// 监听 tabList 变化
+			watch(() => props.tabList, () => {
+				if (props.tabList.length > 0 && currentTab.value === 0) {
+					loadTabUrl(0);
+				}
+			}, { immediate: true });
+
+			// 切换Tab
+			const handleTabClick = (index) => {
+				if (currentTab.value === index) return;
+
+				currentTab.value = index;
+				loadTabUrl(index);
+				emit('tab-change', { index, tab: props.tabList[index] });
+			};
+
+			return {
+				currentTab,
+				currentTabUrl,
+				handleTabClick,
+			};
+		},
+		template: `
+			<div class="ss-sub-tab">
+				<!-- Tab 栏 -->
+				<div class="ss-sub-tab__bar" v-if="tabList.length > 0">
+					<div
+						v-for="(tab, index) in tabList"
+						:key="index"
+						class="ss-sub-tab__item"
+						:class="{ 'ss-sub-tab__item--active': currentTab === index }"
+						@click="handleTabClick(index)"
+					>
+						{{ tab.desc || tab.title }}
+					</div>
+				</div>
+
+				<!-- 内容区域 -->
+				<div class="ss-sub-tab__content" v-if="currentTabUrl">
+					<iframe :src="currentTabUrl" frameborder="0"></iframe>
+				</div>
+			</div>
+		`,
+	};
+
+	// ===== SsUploadFile 文件上传组件(支持单文件/多文件) =====
+	const SsUploadFile = {
+		name: 'SsUploadFile',
+		props: {
+			// v-model 绑定的值(单文件:String,多文件:Array)
+			modelValue: { type: [String, Array], default: '' },
+			// 最大上传数量(默认1个)
+			max: { type: Number, default: 1 },
+			// 是否禁用
+			disabled: { type: Boolean, default: false },
+			// 允许的文件类型(默认常见文件类型)
+			accept: { type: String, default: '.pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar' },
+			// 最大文件大小(MB,默认5MB)
+			maxSize: { type: Number, default: 5 },
+		},
+		emits: ['update:modelValue', 'uploaded'],
+		setup(props, { emit }) {
+			// 文件列表(统一用数组管理)
+			const fileList = ref([]);
+
+			// 监听 modelValue 变化,同步到 fileList
+			watch(() => props.modelValue, (newVal) => {
+				if (props.max === 1) {
+					// 单文件模式
+					fileList.value = newVal ? [{ path: newVal, name: getFileName(newVal) }] : [];
+				} else {
+					// 多文件模式
+					if (Array.isArray(newVal)) {
+						fileList.value = newVal.map(path => ({ path, name: getFileName(path) }));
+					} else {
+						fileList.value = newVal ? [{ path: newVal, name: getFileName(newVal) }] : [];
+					}
+				}
+			}, { immediate: true });
+
+			// 是否可以继续添加文件
+			const canAddMore = computed(() => {
+				return fileList.value.length < props.max;
+			});
+
+			// 从路径中提取文件名
+			const getFileName = (path) => {
+				if (!path) return '';
+				const parts = path.split('/');
+				return parts[parts.length - 1];
+			};
+
+			// 获取文件图标
+			const getFileIcon = (fileName) => {
+				const ext = fileName.split('.').pop().toLowerCase();
+				const iconMap = {
+					pdf: 'icon-pdf',
+					doc: 'icon-word',
+					docx: 'icon-word',
+					xls: 'icon-excel',
+					xlsx: 'icon-excel',
+					txt: 'icon-txt',
+					zip: 'icon-zip',
+					rar: 'icon-zip',
+				};
+				return iconMap[ext] || 'icon-wenjian';
+			};
+
+			// 选择文件
+			const selectFile = () => {
+				if (props.disabled || !canAddMore.value) return;
+				const input = document.createElement('input');
+				input.type = 'file';
+				input.accept = props.accept;
+				input.onchange = async (e) => {
+					const file = e.target.files[0];
+					if (!file) return;
+
+					// 检查文件大小
+					const fileSizeMB = file.size / 1024 / 1024;
+					if (fileSizeMB > props.maxSize) {
+						window.showToast?.(`文件大小不能超过${props.maxSize}MB`, 'error');
+						return;
+					}
+
+					// 上传文件
+					try {
+						const serverPath = await uploadFile(file, 'file', file.name);
+						const newList = [...fileList.value, { path: serverPath, name: file.name }];
+						updateModelValue(newList);
+						emit('uploaded', serverPath);
+					} catch (error) {
+						console.error('文件上传失败:', error);
+					}
+				};
+				input.click();
+			};
+
+			// 删除文件
+			const deleteFile = (index) => {
+				const newList = fileList.value.filter((_, i) => i !== index);
+				updateModelValue(newList);
+			};
+
+			// 下载文件
+			const downloadFile = (file) => {
+				const url = window.SS.utils?.getFileUrl?.(file.path) || window.getFileUrl?.(file.path) || file.path;
+				window.open(url, '_blank');
+			};
+
+			// 更新 modelValue
+			const updateModelValue = (list) => {
+				const paths = list.map(f => f.path);
+				if (props.max === 1) {
+					// 单文件模式:返回 String
+					emit('update:modelValue', paths[0] || '');
+				} else {
+					// 多文件模式:返回 Array
+					emit('update:modelValue', paths);
+				}
+			};
+
+			return {
+				fileList,
+				canAddMore,
+				getFileIcon,
+				selectFile,
+				deleteFile,
+				downloadFile,
+			};
+		},
+		template: `
+			<div class="ss-upload-file">
+				<!-- 文件列表 -->
+				<div v-if="fileList.length > 0" class="file-list">
+					<!-- 已上传的文件 -->
+					<div
+						v-for="(file, index) in fileList"
+						:key="index"
+						class="file-item"
+					>
+						<div class="file-icon">
+							<Icon :name="getFileIcon(file.name)" size="40" color="#40ac6d" />
+						</div>
+						<div class="file-info" @click="downloadFile(file)">
+							<span class="file-name">{{ file.name }}</span>
+						</div>
+						<!-- 文件操作按钮 -->
+						<div class="file-actions">
+							<div class="file-download" @click="downloadFile(file)">
+								<Icon name="icon-xiazai" size="32" color="#40ac6d" />
+							</div>
+							<div v-if="!disabled" class="file-delete" @click.stop="deleteFile(index)">
+								<Icon name="icon-guanbi" size="32" color="#ff3b30" />
+							</div>
+						</div>
+					</div>
+				</div>
+
+				<!-- 添加按钮 -->
+				<div
+					v-if="canAddMore && !disabled"
+					class="file-add-button"
+					@click="selectFile"
+				>
+					<div class="add-icon">
+						<Icon name="icon-tianjia" size="64" color="#999" />
+					</div>
+					<span class="add-text">上传文件 ({{ fileList.length }}/{{ max }})</span>
+					<span class="file-tip">支持格式: {{ accept }},最大{{ maxSize }}MB</span>
+				</div>
+			</div>
+		`,
+	};
+
+	/**
+	 * 功能说明:H5移动端富文本组件(对齐PC字段协议 mswj/fjid/path 回显) by xu 2026-03-01
+	 *
+	 * 约定:
+	 * - v-model 绑定文件路径字段(如 mswj)
+	 * - 组件内部维护编辑内容,并输出隐藏字段:xxEdit / xxwj / ueditorpath / fjid
+	 * - 回显通过 url + path 拉取 HTML 内容,不直接依赖 modelValue 的 HTML 字符串
+	 *
+	 * @component SsEditor
+	 * @prop {String} modelValue 文件路径(如 mswj)
+	 * @prop {String} name 字段名(默认 mswj)
+	 * @prop {String} url 回显读取接口地址
+	 * @prop {Number|String} height 编辑器高度
+	 * @prop {String} placeholder 占位文案
+	 * @prop {Boolean} readonly 是否只读
+	 * @prop {String} uploadUrl 上传接口地址
+	 * @prop {Object} param 附件参数(button.cmsAddUrl / button.cmsUpdUrl / mode)
+	 * @emits update:modelValue 更新文件路径
+	 * @emits ready 编辑器就绪
+	 * @emits change 内容变化
+	 */
+	const SsEditor = {
+		name: 'SsEditor',
+		props: {
+			modelValue: { type: String, default: '' },
+			name: { type: String, default: 'mswj' },
+			url: { type: String, default: '' },
+			height: { type: [Number, String], default: 280 },
+			placeholder: { type: String, default: '请输入内容' },
+			readonly: { type: Boolean, default: false },
+			uploadUrl: { type: String, default: '/service?ssServ=ulByHttp' },
+			param: { type: Object, default: () => ({}) },
+		},
+		emits: ['update:modelValue', 'ready', 'change'],
+		setup(props, { emit }) {
+			const editorElementId = `ss-editor-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
+			const editorContent = ref('');
+			const editorInstance = ref(null);
+			const fjid = ref(props.param?.button?.val || '');
+			const fjName = props.param?.button?.desc || '附件';
+			const mode = props.param?.mode;
+
+			/**
+			 * 功能说明:确保附件 fjid 存在,不存在时先通过 cmsAddUrl 创建 by xu 2026-03-01
+			 * @returns {Promise<string>} fjid
+			 */
+			const ensureFjid = async () => {
+				if (fjid.value) return fjid.value;
+				if (!props.param?.button?.cmsAddUrl) return '';
+				return new Promise((resolve) => {
+					$.ajax({
+						type: 'post',
+						url: props.param.button.cmsAddUrl,
+						async: false,
+						data: {
+							name: 'fjid',
+							ssNrObjName: 'sh',
+							ssNrObjId: '',
+						},
+						success: (_fjid) => {
+							fjid.value = _fjid || '';
+							resolve(fjid.value);
+						},
+						error: () => resolve(''),
+					});
+				});
+			};
+
+			/**
+			 * 功能说明:打开附件管理弹窗(与PC端行为一致) by xu 2026-03-01
+			 * @returns {Promise<void>}
+			 */
+			const openAttachmentDialog = async () => {
+				if (!props.param?.button?.cmsUpdUrl) {
+					console.warn('未配置附件编辑地址 cmsUpdUrl');
+					return;
+				}
+				const currentFjid = await ensureFjid();
+				if (!currentFjid) return;
+
+				const query =
+					`&nrid=T-${currentFjid}` +
+					`&objectId=${currentFjid}` +
+					`&objectName=${encodeURIComponent(fjName)}` +
+					`&callback=${window['fjidCallbackName'] || ''}`;
+
+				if (window.SS && typeof window.SS.openDialog === 'function') {
+					window.SS.openDialog({
+						src: props.param.button.cmsUpdUrl + query,
+						headerTitle: '编辑',
+						width: 900,
+						high: 664,
+						zIndex: 51,
+					});
+				}
+			};
+
+			/**
+			 * 功能说明:初始化 Jodit 编辑器并绑定工具栏/上传/change 事件 by xu 2026-03-01
+			 * @returns {void}
+			 */
+			const buildEditor = () => {
+				if (!window.Jodit || !window.Jodit.make) {
+					console.error('Jodit 未加载,无法初始化 ss-editor');
+					return;
+				}
+
+				const editorUploadUrl = props.uploadUrl.includes('?')
+					? `${props.uploadUrl}&type=img`
+					: `${props.uploadUrl}?type=img`;
+
+				const instance = window.Jodit.make(`#${editorElementId}`, {
+					height: props.height,
+					placeholder: props.placeholder,
+					readonly: props.readonly,
+					language: 'zh_cn',
+					showXPathInStatusbar: false,
+					showCharsCounter: false,
+					showWordsCounter: false,
+					allowResizeY: false,
+					toolbarSticky: false,
+					statusbar: false,
+					uploader: {
+						url: editorUploadUrl,
+						format: 'json',
+						method: 'POST',
+						filesVariableName: (i) => `imgs[${i}]`,
+						isSuccess: (resp) => resp?.code === 0 || !!resp?.data,
+						getMessage: (resp) => resp?.msg || '上传失败',
+						process: (resp) => resp?.data?.url || resp?.data?.path || '',
+						contentType: () => false,
+					},
+					controls: {
+						customLinkButton: {
+							name: 'link',
+							tooltip: '附件',
+							exec: () => {
+								openAttachmentDialog();
+							},
+						},
+					},
+					buttons: [
+						'fullsize',
+						'bold',
+						'italic',
+						'underline',
+						'|',
+						'font',
+						'fontsize',
+						'|',
+						'left',
+						'center',
+						'right',
+						'|',
+						'ul',
+						'ol',
+						'|',
+						'image',
+						'table',
+						'customLinkButton',
+						'|',
+						'undo',
+						'redo',
+					],
+					buttonsMD: ['bold', 'italic', 'underline', '|', 'image', 'customLinkButton', '|', 'dots'],
+					buttonsSM: ['bold', 'italic', '|', 'image', 'customLinkButton', '|', 'dots'],
+					buttonsXS: ['bold', '|', 'dots'],
+				});
+
+				instance.value = editorContent.value || '';
+				instance.events.on('change', () => {
+					editorContent.value = instance.value || '';
+					emit('change', editorContent.value);
+				});
+
+				editorInstance.value = instance;
+				emit('ready', instance);
+			};
+
+			/**
+			 * 功能说明:按路径加载富文本HTML内容并回填编辑器 by xu 2026-03-01
+			 * @returns {Promise<void>}
+			 */
+			const loadContentByPath = async () => {
+				if (!props.url || !props.modelValue) return;
+				try {
+					const params = new URLSearchParams();
+					if (mode) params.append('mode', mode);
+					params.append('path', props.modelValue);
+
+					const response = await window.axios.post(props.url, params, {
+						headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+					});
+
+					const content = response?.data?.content || '';
+					if (content) {
+						editorContent.value = content;
+						if (editorInstance.value) {
+							editorInstance.value.value = content;
+						}
+					}
+
+					const filePath = response?.data?.path;
+					if (filePath) {
+						emit('update:modelValue', filePath);
+					}
+				} catch (error) {
+					console.error('ss-editor 回显内容加载失败:', error);
+				}
+			};
+
+			onMounted(async () => {
+				buildEditor();
+				await loadContentByPath();
+			});
+
+			watch(
+				() => props.readonly,
+				(newVal) => {
+					if (editorInstance.value && typeof editorInstance.value.setReadOnly === 'function') {
+						editorInstance.value.setReadOnly(newVal);
+					}
+				}
+			);
+
+			onBeforeUnmount(() => {
+				if (editorInstance.value && typeof editorInstance.value.destruct === 'function') {
+					editorInstance.value.destruct();
+				}
+			});
+
+			return {
+				editorElementId,
+				editorContent,
+				fjid,
+			};
+		},
+		template: `
+			<div class="ss-editor-container">
+				<input v-if="fjid" type="hidden" name="fjid" :value="fjid" />
+				<input type="hidden" :name="name.replace(/wj$/, '') + 'Edit'" :value="editorContent" />
+				<input type="hidden" :name="name.replace(/wj$/, '') + 'wj'" :value="modelValue" />
+				<input type="hidden" name="ueditorpath" value="mswj" />
+				<textarea :id="editorElementId"></textarea>
+			</div>
+		`,
+	};
+
+	window.SS.dom.initializeFormApp = function (config) {
+		const { el, ...vueOptions } = config;
+		const app = createApp({
+			...vueOptions,
+		});
+
+		// 注册组件
+		// app.component("SsLoginIcon", SsLoginIcon);
+		// app.component("SsMark", SsMark);
+		// app.component("SsFullStyleHeader", SsFullStyleHeader);
+		// app.component("SsDialog", SsDialog);
+		app.component('SsInput', SsInput);
+		app.component('SsBottom', SsBottom);
+		app.component('SsCard', SsCard);
+		app.component('SsSearchButton', SsSearchButton);
+		app.component('SsSelect', SsSelect);
+		app.component('Icon', Icon);
+		// 注册 Vant 组件
+		const vantLib = window.vant || window.Vant;
+		console.log('🔍 检查 Vant:', {
+			hasVant: !!window.vant,
+			hasVantCap: !!window.Vant,
+			vantLib: !!vantLib,
+			vantKeys: vantLib ? Object.keys(vantLib).slice(0, 20) : [],
+			hasPopup: vantLib?.Popup,
+			hasDatetimePicker: vantLib?.DatetimePicker,
+			hasDatePicker: vantLib?.DatePicker,
+			hasTimePicker: vantLib?.TimePicker,
+			allKeys: vantLib ? Object.keys(vantLib) : []
+		});
+
+		if (vantLib) {
+			try {
+				// 使用 Vant 的 use 方法注册所有组件
+				app.use(vantLib);
+				console.log('✅ Vant 全部组件注册成功');
+			} catch (error) {
+				console.error('❌ Vant 组件注册失败:', error);
+				// 降级方案:手动注册具体组件
+				try {
+					app.component('van-popup', vantLib.Popup);
+					app.component('van-datetime-picker', vantLib.DatetimePicker);
+					console.log('✅ Vant 手动注册成功');
+				} catch (e) {
+					console.error('❌ Vant 手动注册也失败:', e);
+				}
+			}
+		} else {
+			console.warn('⚠️ Vant 未加载');
+		}
+
+		// 注册组件 - 统一使用 kebab-case
+		app.component('ss-verify', SsVerify);
+		app.component('ss-verify-node', SsVerifyNode);
+
+		
+		app.component('ss-common-icon', SsCommonIcon);
+		app.component('ss-onoff-button', SsOnoffButton);
+		app.component('ss-datetime-picker', SsDatetimePicker);
+		app.component('ss-confirm', SsConfirm);
+		app.component('ss-image-cropper', SsImageCropper);
+		app.component('ss-upload-image', SsUploadImage);
+		app.component('ss-upload-file', SsUploadFile);
+		app.component('ss-car-card', SsCarCard);
+		app.component('ss-sub-tab', SsSubTab);
+		app.component('ss-editor', SsEditor);
+		app.component('SsEditor', SsEditor);
+
+
+		// app.component("SsObjp", SsObjp);
+		// app.component("SsHidden", SsHidden);
+		// app.component("SsCcp", SsCcp);
+		// app.component("SsDatePicker", SsDatePicker);
+		// app.component("SsIcon", SsIcon);
+		// app.component("SsCommonIcon", SsCommonIcon);
+		// app.component("SsBreadcrumb", SsBreadcrumb);
+		// app.component("SsEditor", SsEditor);
+		// app.component("SsDialogIcon", SsDialogIcon);
+		// app.component("SsBottomButton", SsBottomButton);
+		// app.component("SsNavIcon", SsNavIcon);
+		// app.component("SsHeaderIcon", SsHeaderIcon);
+		// app.component("SsGolbalMenuIcon", SsGolbalMenuIcon);
+		// app.component("SsCartListIcon", SsCartListIcon);
+		// app.component("SsQuickIcon", SsQuickIcon);
+		// app.component("SsFormIcon", SsFormIcon);
+		// app.component("SsBottomDivIcon", SsBottomDivIcon);
+		// app.component("SsEditorIcon", SsEditorIcon);
+		// app.component("SsValidate", SsValidate);
+		// app.component("SsOnoffbutton", SsOnoffbutton);
+		// app.component("SsOnoffbuttonArray", SsOnoffbuttonArray);
+		// app.component("SsTextarea", SsTextarea);
+		// app.component("SsLoginInput", SsLoginInput);
+		// app.component("SsLoginButton", SsLoginButton);
+		// app.component("SsSearch", SsSearch);
+		// app.component("SsCartItem", SsCartItem);
+		// app.component("SsCartItem2", SsCartItem2);
+		// app.component("SsListCard", SsListCard);
+		// app.component("SsFolderCard", SsFolderCard);
+		// app.component("SsFolderCartView", SsFolderCartView);
+		// app.component("SsPage", SsPage);
+		// app.component("SsRightInfo", SSRightInfo);
+		// app.component("SsSuccessPopup", SsSuccessPopup);
+		// app.component("SsErrorDialog", SsErrorDialog);
+		// app.component("SsVerify", SsVerify);
+		// app.component("SsVerifyNode", SsVerifyNode);
+		// app.component("SsOrcImgBox", SsOrcImgBox);
+		// app.component("ss-search-input", SsSearchInput);
+		// app.component("ss-search-date-picker", SsSearchDatePicker);
+
+		// app.component("ss-search-button", SsSearchButton);
+		// app.component("ss-drop-button", SsDropButton);
+		// app.component("ss-sub-tab", SsSubTab);
+		// app.component("ss-img", SsImgUpload);
+		// 设置为中文
+		// app.use(ElementPlus, {
+		//   locale: ElementPlusLocaleZhCn,
+		// });
+		// console.log(ElementPlus);
+		// 确保 ElementPlusIconsVue
+		// if (window.ElementPlusIconsVue) {
+		//   // 注册 Element Plus 图标组件
+		//   for (const [key, component] of Object.entries(
+		//     window.ElementPlusIconsVue
+		//   )) {
+		//     console.log(key, component);
+		//     app.component(key, component);
+		//   }
+		// }
+		// 挂载首页的组件
+		// for (const componentName in IndexComponents) {
+		//   app.component(componentName, IndexComponents[componentName]);
+		// }
+		// 挂载echarts的组件
+		// for (const componentName in EchartComponents) {
+		//   app.component(componentName, EchartComponents[componentName]);
+		// }
+		// 挂载 Vue 应用
+		const vm = app.mount(el);
+		vm.data = vueOptions.data();
+
+		return vm;
+	};
+})();

+ 104 - 0
js/mp_toast/toast.js

@@ -0,0 +1,104 @@
+/**
+ * H5版本的Toast提示工具
+ */
+
+// Toast管理器
+const toastManager = {
+  toastElement: null,
+  toastTimer: null,
+
+  // 显示Toast
+  show(message, duration = 3000, type = 'info') {
+    // 清除之前的Toast
+    this.hide()
+
+    // 创建Toast元素
+    this.toastElement = document.createElement('div')
+    this.toastElement.className = `h5-toast h5-toast-${type}`
+    this.toastElement.textContent = message
+
+    // 添加样式(如果还没有添加)
+    if (!document.getElementById('h5-toast-style')) {
+      const style = document.createElement('style')
+      style.id = 'h5-toast-style'
+      style.textContent = `
+        .h5-toast {
+          position: fixed;
+          top: 50%;
+          left: 50%;
+          transform: translate(-50%, -50%);
+          padding: 12px 20px;
+          border-radius: 6px;
+          font-size: 14px;
+          z-index: 10001;
+          max-width: 80%;
+          text-align: center;
+          opacity: 0;
+          transition: opacity 0.3s ease;
+        }
+        .h5-toast.show {
+          opacity: 1;
+        }
+        .h5-toast-info {
+          background: rgba(0, 0, 0, 0.8);
+          color: white;
+        }
+        .h5-toast-success {
+          background: rgba(40, 167, 69, 0.9);
+          color: white;
+        }
+        .h5-toast-warning {
+          background: rgba(255, 193, 7, 0.9);
+          color: #212529;
+        }
+        .h5-toast-error {
+          background: rgba(220, 53, 69, 0.9);
+          color: white;
+        }
+      `
+      document.head.appendChild(style)
+    }
+
+    // 添加到页面
+    document.body.appendChild(this.toastElement)
+
+    // 显示动画
+    setTimeout(() => {
+      this.toastElement.classList.add('show')
+    }, 10)
+
+    // 自动隐藏
+    this.toastTimer = setTimeout(() => {
+      this.hide()
+    }, duration)
+  },
+
+  // 隐藏Toast
+  hide() {
+    if (this.toastTimer) {
+      clearTimeout(this.toastTimer)
+      this.toastTimer = null
+    }
+
+    if (this.toastElement) {
+      this.toastElement.classList.remove('show')
+      setTimeout(() => {
+        if (this.toastElement && this.toastElement.parentNode) {
+          document.body.removeChild(this.toastElement)
+        }
+        this.toastElement = null
+      }, 300)
+    }
+  }
+}
+
+// 全局Toast函数
+function showToastEffect(message, duration = 3000, type = 'info') {
+  toastManager.show(message, duration, type)
+}
+
+// 导出到全局
+window.showToastEffect = showToastEffect
+window.toastManager = toastManager
+
+console.log('✅ H5 Toast工具已加载')

+ 182 - 0
js/mp_user_api/user-api.js

@@ -0,0 +1,182 @@
+/**
+ * H5版本的用户API
+ * 基于request,兼容小程序的userApi接口
+ * 依赖:common.js (提供公共工具函数)
+ */
+
+// 注意:getUrlParams, getDeviceInfo, getMockWechatCode, userManager
+// 这些函数已在 common.js 中定义,这里直接使用全局函数
+
+const h5UserApi = {
+  // 账号密码登录 - H5版本
+  async accountLogin(data, onComplete) {
+    try {
+      console.log('🔐 H5账号登录开始:', data)
+
+      // 获取设备信息并更新
+      const deviceInfo = getDeviceInfo()
+      const newDeviceInfo = {
+        ...deviceInfo,
+        deviceId: `(${data.yhm})${deviceInfo.model}`,
+      }
+      
+      // H5环境下保存到localStorage
+      localStorage.setItem('deviceInfo', JSON.stringify(newDeviceInfo))
+
+      // 获取微信授权码 (H5环境下的处理)
+      const wechatCode = data.wechatCode || getMockWechatCode()
+
+      // 构建请求参数
+      const requestData = {
+        ...data,
+        wechatCode: wechatCode
+      }
+
+      console.log('📤 发送登录请求:', requestData)
+
+      // 发送登录请求
+      const promise = request.post(
+        `/service?ssServ=ssLogin&yhm=${data.yhm}&mm=${data.mm}&wdConfirmationCaptchaService=0&wechatCode=${wechatCode}`,
+        requestData,
+        {
+          loading: false, // 禁用全局 loading,由登录页面自己控制
+          request: {
+            timeout: 15000 // 登录请求 15 秒超时
+          }
+        }
+      )
+
+      // 处理请求结果
+      const result = await promise
+
+      console.log('✅ H5登录请求成功:', result)
+
+      // 无论成功还是失败,都调用完成回调
+      if (onComplete) {
+        onComplete()
+      }
+
+      return result
+
+    } catch (error) {
+      console.error('❌ H5登录请求失败:', error)
+
+      // 确保调用完成回调
+      if (onComplete) {
+        onComplete()
+      }
+
+      throw error
+    }
+  },
+
+  // 自动登录 - H5版本
+  async autoLogin(data) {
+    console.log('🔄 H5自动登录:', data)
+
+    return request.post(
+      `/service?ssServ=ssLogin&wdConfirmationCaptchaService=0&mdToken=${data.mdToken}`,
+      data,
+      {
+        loading: false // 自动登录不显示 loading,避免打扰用户
+      }
+    )
+  },
+
+  // 微信登录 - H5版本
+  async wechatLogin(data, onComplete) {
+    try {
+      console.log('🔐 H5微信登录开始:', data)
+
+      // 在H5环境下,微信登录需要特殊处理
+      // 这里先使用模拟的code
+      const wechatCode = data.code || getMockWechatCode()
+
+      const requestData = {
+        ...data,
+        code: wechatCode
+      }
+
+      const promise = request.post(
+        `/service?ssServ=ssLogin&wdConfirmationCaptchaService=0`,
+        requestData,
+        {
+          loading: false, // 禁用全局 loading,由登录页面自己控制
+          request: {
+            timeout: 15000 // 微信登录请求 15 秒超时
+          }
+        }
+      )
+
+      const result = await promise
+
+      console.log('✅ H5微信登录成功:', result)
+
+      // 无论成功还是失败,都调用完成回调
+      if (onComplete) {
+        onComplete()
+      }
+
+      return result
+
+    } catch (error) {
+      console.error('❌ H5微信登录失败:', error)
+
+      // 确保调用完成回调
+      if (onComplete) {
+        onComplete()
+      }
+
+      throw error
+    }
+  },
+
+  // 获取用户信息
+  async getUserInfo(userId) {
+    console.log('👤 H5获取用户信息:', userId)
+
+    return request.get(`/service?ssServ=user.getUserInfo&userId=${userId}`, {}, {
+      loading: false
+    })
+  },
+
+  // 登出 (使用 common.js 中的 userManager)
+  async logout() {
+    console.log('🚪 H5用户登出')
+
+    try {
+      // 使用公共的用户管理器清除登录信息
+      userManager.clearLoginInfo()
+
+      // 可以调用服务端登出接口
+      // await request.post('/service?ssServ=ss.logout', {}, { loading: false })
+
+      console.log('✅ H5登出成功')
+      return { success: true }
+
+    } catch (error) {
+      console.error('❌ H5登出失败:', error)
+      throw error
+    }
+  },
+
+  // 检查登录状态 (使用 common.js 中的 userManager)
+  checkLoginStatus() {
+    return userManager.checkLoginStatus()
+  },
+
+  // 保存用户信息 (使用 common.js 中的 userManager)
+  saveUserInfo(userInfo) {
+    return userManager.saveUserInfo(userInfo)
+  },
+
+  // 获取保存的用户信息 (使用 common.js 中的 userManager)
+  getSavedUserInfo() {
+    return userManager.getSavedUserInfo()
+  }
+}
+
+// 导出到全局
+window.h5UserApi = h5UserApi
+
+console.log('✅ H5 UserApi已加载')

+ 469 - 0
js/mp_utils/field-formatter.js

@@ -0,0 +1,469 @@
+/**
+ * H5版本的字段格式化工具函数
+ * 基于小程序版本改造,适配H5环境
+ */
+
+/**
+ * 获取字典翻译 - H5版本
+ * @param {string} cbName - 字典名称
+ * @param {string|number} value - 值
+ * @param {string} cacheKey - 缓存键
+ * @param {Map} dictCache - 字典缓存
+ * @param {Function} updateCallback - 更新回调函数
+ */
+const getDictTranslation = async (cbName, value, cacheKey, dictCache, updateCallback) => {
+  try {
+    console.log(`🔍 获取字典翻译 [${cbName}:${value}]`);
+
+    // 使用 loadObjpOpt 接口调用字典
+    const result = await request.post(
+      '/service?ssServ=loadObjpOpt&objectpickerdropdown1=1',
+      {
+        objectpickerparam: JSON.stringify({
+          input: "false",
+          codebook: cbName
+        }),
+        objectpickertype: 2,      // 2表示获取要回显的一项
+        objectpickervalue: value  // 需回显的值
+      },
+      { loading: false, formData: true }
+    );
+
+    console.log(`字典翻译 [${cbName}:${value}] API返回:`, result);
+
+    // 处理返回数据格式:{"result":{"622182":"会议室A"}}
+    let translatedValue = value; // 默认返回原值
+    if (result && result.data && result.data.result) {
+      // 从result对象中根据value查找对应的翻译
+      translatedValue = result.data.result[value] || value;
+      console.log(`字典翻译结果: ${value} -> ${translatedValue}`);
+    }
+
+    dictCache.set(cacheKey, translatedValue);
+
+    // 触发页面更新
+    if (updateCallback) {
+      updateCallback();
+    }
+  } catch (error) {
+    console.error(`字典翻译失败 [${cbName}:${value}]:`, error);
+    dictCache.set(cacheKey, value);
+  }
+};
+
+/**
+ * 格式化字段值(处理字典翻译、日期格式等)- H5版本
+ * @param {Object} fieldObj - 字段对象 {field, value}
+ * @param {Map} dictCache - 字典缓存
+ * @param {Function} getDictTranslationFn - 字典翻译函数
+ * @returns {string} 格式化后的值
+ */
+const formatFieldValue = (fieldObj, dictCache, getDictTranslationFn) => {
+  if (!fieldObj || !fieldObj.field || fieldObj.value === undefined) {
+    return '';
+  }
+
+  const { field, value } = fieldObj;
+  
+  // 空值处理
+  if (value === null || value === undefined || value === '') {
+    return '';
+  }
+
+  // 如果字段有字典配置,进行字典翻译
+  if (field.cbName) {
+    const cacheKey = `${field.cbName}_${value}`;
+    
+    // 先从缓存中查找
+    if (dictCache.has(cacheKey)) {
+      return dictCache.get(cacheKey);
+    }
+    
+    // 缓存中没有,异步获取翻译
+    if (getDictTranslationFn) {
+      getDictTranslationFn(field.cbName, value, cacheKey, dictCache);
+    }
+    
+    // 返回原值作为临时显示
+    return value;
+  }
+
+  // 时间格式化(优先检查fmt字段)
+  if (field.fmt) {
+    return h5FormatDate(value, field.fmt);
+  }
+
+  // 日期格式化(兼容旧逻辑)
+  if (field.type === 'date' || field.name.toLowerCase().includes('date') ||
+      field.name.toLowerCase().includes('time')) {
+    return h5FormatDate(value);
+  }
+
+  // 数字格式化
+  if (field.type === 'number' && typeof value === 'number') {
+    return value.toLocaleString();
+  }
+
+  // 布尔值格式化
+  if (field.type === 'boolean' || typeof value === 'boolean') {
+    return value ? '是' : '否';
+  }
+
+  // 默认返回字符串
+  return String(value);
+};
+
+/**
+ * 解析特殊日期格式:Jul 4, 2025, 6:00:00 AM
+ * @param {string} dateStr - 日期字符串
+ * @returns {object|null} 解析后的日期对象或null
+ */
+const parseSpecialDateFormat = (dateStr) => {
+  try {
+    // 匹配格式:Jul 4, 2025, 6:00:00 AM
+    const regex = /^(\w{3})\s+(\d{1,2}),\s+(\d{4}),\s+(\d{1,2}):(\d{2}):(\d{2})\s+(AM|PM)$/;
+    const match = dateStr.match(regex);
+
+    if (!match) return null;
+
+    const [, monthStr, day, year, hour, minute, second, ampm] = match;
+
+    // 月份映射
+    const months = {
+      'Jan': 0, 'Feb': 1, 'Mar': 2, 'Apr': 3, 'May': 4, 'Jun': 5,
+      'Jul': 6, 'Aug': 7, 'Sep': 8, 'Oct': 9, 'Nov': 10, 'Dec': 11
+    };
+
+    const month = months[monthStr];
+    if (month === undefined) return null;
+
+    // 处理12小时制
+    let hour24 = parseInt(hour);
+    if (ampm === 'PM' && hour24 !== 12) {
+      hour24 += 12;
+    } else if (ampm === 'AM' && hour24 === 12) {
+      hour24 = 0;
+    }
+
+    return {
+      year: parseInt(year),
+      month: month,
+      day: parseInt(day),
+      hour: hour24,
+      minute: parseInt(minute),
+      second: parseInt(second)
+    };
+  } catch (error) {
+    console.warn('解析特殊日期格式失败:', dateStr, error);
+    return null;
+  }
+};
+
+/**
+ * 格式化日期对象
+ * @param {object} dateObj - 日期对象 {year, month, day, hour, minute, second}
+ * @param {string} format - 格式字符串
+ * @returns {string} 格式化后的字符串
+ */
+const formatDateObject = (dateObj, format) => {
+  const { year, month, day, hour, minute, second } = dateObj;
+
+  return format
+    .replace(/YYYY/g, year)
+    .replace(/yyyy/g, year)
+    .replace(/MM/g, String(month + 1).padStart(2, '0'))
+    .replace(/DD/g, String(day).padStart(2, '0'))
+    .replace(/dd/g, String(day).padStart(2, '0'))
+    .replace(/HH/g, String(hour).padStart(2, '0'))
+    .replace(/hh/g, String(hour).padStart(2, '0'))
+    .replace(/mm/g, String(minute).padStart(2, '0'))
+    .replace(/ss/g, String(second).padStart(2, '0'));
+};
+
+/**
+ * 增强的日期格式化函数 - H5版本
+ * @param {string|Date} date - 日期,支持多种格式如 "Jul 4, 2025, 6:00:00 AM"
+ * @param {string} format - 格式,如 'YYYY-MM-DD HH:mm:ss'
+ * @returns {string} 格式化后的日期
+ */
+const h5FormatDate = (date, format = 'YYYY-MM-DD HH:mm:ss') => {
+  if (!date) return '';
+
+  try {
+    console.log(`🔧 h5FormatDate 开始处理:`, date, '目标格式:', format);
+
+    // 手动解析特殊格式:Jul 4, 2025, 6:00:00 AM
+    const parseResult = parseSpecialDateFormat(date);
+    if (parseResult) {
+      console.log(`🔧 手动解析成功:`, parseResult);
+      const result = formatDateObject(parseResult, format);
+      console.log(`✅ 手动格式化结果:`, result);
+      return result;
+    }
+
+    // 使用 Day.js 解析日期(更强大的日期解析能力)
+    if (typeof window !== 'undefined' && window.dayjs) {
+      const dayObj = window.dayjs(date);
+      console.log(`🔧 使用 Day.js 解析:`, dayObj.isValid(), dayObj.toString());
+
+      if (dayObj.isValid()) {
+        const result = dayObj.format(format);
+        console.log(`✅ Day.js 格式化结果:`, result);
+        return result;
+      }
+    }
+
+    // 降级到原生 Date(如果 Day.js 未加载)
+    console.log(`🔧 Day.js 未加载,使用原生 Date`);
+    const d = new Date(date);
+
+    console.log(`🔧 解析后的日期对象:`, d, '是否有效:', !isNaN(d.getTime()));
+
+    // 检查日期是否有效
+    if (isNaN(d.getTime())) {
+      console.warn('❌ 无效日期格式:', date);
+      return date; // 无效日期返回原值
+    }
+
+    const year = d.getFullYear();
+    const month = String(d.getMonth() + 1).padStart(2, '0');
+    const day = String(d.getDate()).padStart(2, '0');
+    const hours = String(d.getHours()).padStart(2, '0');
+    const minutes = String(d.getMinutes()).padStart(2, '0');
+    const seconds = String(d.getSeconds()).padStart(2, '0');
+
+    // 支持多种格式模式
+    let result = format
+      .replace(/YYYY/g, year)
+      .replace(/yyyy/g, year)
+      .replace(/MM/g, month)
+      .replace(/DD/g, day)
+      .replace(/dd/g, day)
+      .replace(/HH/g, hours)
+      .replace(/hh/g, hours)
+      .replace(/mm/g, minutes)
+      .replace(/ss/g, seconds);
+
+    console.log(`日期格式化: ${date} -> ${result} (格式: ${format})`);
+    return result;
+
+  } catch (error) {
+    console.error('日期格式化异常:', date, format, error);
+    return date; // 异常时返回原值
+  }
+};
+
+/**
+ * 异步格式化字段值 - 支持字典转换
+ * @param {object} fieldData - 字段数据 {field, value}
+ * @param {Map} dictCache - 字典缓存
+ * @returns {Promise<string>} 格式化后的值
+ */
+const formatFieldValueAsync = async (fieldData, dictCache) => {
+  if (!fieldData || !fieldData.field) {
+    return fieldData?.value || '';
+  }
+
+  const { field, value } = fieldData;
+
+  // 空值处理
+  if (value === null || value === undefined || value === '') {
+    return '';
+  }
+
+  // 优先处理时间格式化(如果字段有fmt属性)
+  if (field.fmt) {
+    try {
+      console.log(`🕐 格式化时间字段 [${field.name}]:`, value, '格式:', field.fmt);
+      console.log(`🔧 调用 h5FormatDate 前...`);
+      const formattedTime = h5FormatDate(value, field.fmt);
+      console.log(`🔧 调用 h5FormatDate 后,结果:`, formattedTime);
+      console.log(`✅ 时间格式化结果:`, formattedTime);
+      return formattedTime;
+    } catch (error) {
+      console.error('❌ 时间格式化失败:', field.name, value, error);
+      // 格式化失败,返回原值
+      return value;
+    }
+  }
+
+  // 如果字段有字典配置,进行字典转换
+  if (field.cbName) {
+    const cacheKey = `${field.cbName}_${value}`;
+
+    // 先从缓存中查找
+    if (dictCache && dictCache.has(cacheKey)) {
+      return dictCache.get(cacheKey);
+    }
+
+    // 缓存中没有,异步获取翻译
+    try {
+      const result = await request.post(
+        `/service?ssServ=loadObjpOpt&objectpickerdropdown1=1`,
+        {
+          objectpickerparam: JSON.stringify({
+            input: "false",
+            codebook: field.cbName
+          }),
+          objectpickertype: 2,
+          objectpickervalue: value
+        },
+        {
+          loading: false,
+          formData: true
+        }
+      );
+
+      if (result && result.data && result.data.result && result.data.result[value]) {
+        const translatedValue = result.data.result[value];
+        // 缓存结果
+        if (dictCache) {
+          dictCache.set(cacheKey, translatedValue);
+        }
+        return translatedValue;
+      }
+    } catch (error) {
+      console.warn('字典转换失败:', field.cbName, value, error);
+    }
+
+    // 转换失败,返回原值
+    return value;
+  }
+
+  // 日期格式化
+  if (field.type === 3 && field.fmt) {
+    return h5FormatDate(value, field.fmt);
+  }
+
+  // 数字格式化
+  if (field.type === 2 && typeof value === 'number') {
+    return value.toLocaleString();
+  }
+
+  // 默认返回字符串
+  return value.toString();
+};
+
+/**
+ * 获取字典所有选项 - 用于下拉菜单
+ * @param {string} cbName - 字典名称
+ * @param {Map} dictCache - 字典缓存
+ * @returns {Promise<Array>} 选项列表 [{n: '显示名', v: '值'}]
+ */
+const getDictOptions = async (cbName, dictCache) => {
+  const cacheKey = `options_${cbName}`;
+
+  // 检查缓存
+  if (dictCache && dictCache.has(cacheKey)) {
+    return dictCache.get(cacheKey);
+  }
+
+  try {
+    console.log(`🔍 获取字典选项 [${cbName}]`);
+
+    const result = await request.post(
+      `/service?ssServ=loadObjpOpt&objectpickerdropdown1=1`,
+      {
+        objectpickerparam: JSON.stringify({
+          input: "false",
+          codebook: cbName
+        }),
+        objectpickertype: 1,
+        objectpickersearchAll: 1
+      },
+      {
+        loading: false,
+        formData: true
+      }
+    );
+
+    console.log(`字典选项 [${cbName}] API返回:`, result);
+
+    if (result && result.data && result.data.result) {
+      const options = Object.entries(result.data.result).map(([value, label]) => ({
+        n: label,
+        v: value
+      }));
+
+      // 缓存结果
+      if (dictCache) {
+        dictCache.set(cacheKey, options);
+      }
+
+      return options;
+    }
+
+    return [];
+  } catch (error) {
+    console.error(`❌ 获取字典选项失败 [${cbName}]:`, error);
+    return [];
+  }
+};
+
+/**
+ * 格式化对象列表数据 - 处理API返回的objectList
+ * @param {Array} objectList - 原始对象列表
+ * @param {Map} dictCache - 字典缓存
+ * @returns {Promise<Array>} 格式化后的列表
+ */
+const formatObjectList = async (objectList, dictCache) => {
+  if (!Array.isArray(objectList)) return [];
+
+  const formattedList = [];
+
+  for (const item of objectList) {
+    const formattedItem = { ...item };
+
+    // 格式化 first 字段
+    if (item.first) {
+      formattedItem.firstDisplay = await formatFieldValueAsync(item.first, dictCache);
+    }
+
+    // 格式化 second 字段
+    if (item.second) {
+      formattedItem.secondDisplay = await formatFieldValueAsync(item.second, dictCache);
+    }
+
+    // 格式化 third 字段组
+    if (item.third && Array.isArray(item.third)) {
+      formattedItem.thirdDisplay = [];
+
+      for (const group of item.third) {
+        const formattedGroup = [];
+
+        for (const fieldData of group) {
+          const displayValue = await formatFieldValueAsync(fieldData, dictCache);
+          formattedGroup.push({
+            ...fieldData,
+            displayValue
+          });
+        }
+
+        formattedItem.thirdDisplay.push(formattedGroup);
+      }
+    }
+
+    formattedList.push(formattedItem);
+  }
+
+  return formattedList;
+};
+
+// 导出到全局
+window.H5FieldFormatter = {
+  getDictTranslation,
+  formatFieldValue,
+  formatFieldValueAsync,
+  formatDate: h5FormatDate,
+  getDictOptions,
+  formatObjectList
+};
+
+// 兼容性:也导出为全局函数
+window.getDictTranslation = getDictTranslation;
+window.formatFieldValue = formatFieldValue;
+window.formatFieldValueAsync = formatFieldValueAsync;
+window.getDictOptions = getDictOptions;
+window.formatObjectList = formatObjectList;
+
+console.log('✅ H5字段格式化工具加载完成');

+ 221 - 0
js/mp_utils/navigation.js

@@ -0,0 +1,221 @@
+/**
+ * H5版本的页面导航工具
+ * 提供统一的页面跳转、返回、刷新等功能
+ */
+
+/**
+ * 页面导航管理器
+ */
+const NavigationManager = {
+  
+  /**
+   * 跳转到目标页面
+   * @param {string} destPage - 目标页面名称(不含.html)
+   * @param {object} params - 传递的参数
+   * @param {object} options - 跳转选项
+   */
+  goTo(destPage, params = {}, options = {}) {
+    try {
+      const urlParams = new URLSearchParams(window.location.search);
+      
+      // 添加新参数
+      Object.keys(params).forEach(key => {
+        if (params[key] !== undefined && params[key] !== null) {
+          urlParams.set(key, encodeURIComponent(params[key]));
+        }
+      });
+      
+      // 添加导航标记,用于返回时刷新
+      if (options.needRefresh) {
+        urlParams.set('_needRefresh', '1');
+      }
+      
+      // 构建目标URL
+      const newUrl = `${destPage}.html?${urlParams.toString()}`;
+      
+      console.log('🔗 导航到:', newUrl);
+      console.log('📋 传递参数:', Object.fromEntries(urlParams));
+      
+      // 跳转页面
+      window.location.href = newUrl;
+      
+    } catch (error) {
+      console.error('❌ 页面导航失败:', error);
+      if (typeof showToast !== 'undefined') {
+        showToast('页面跳转失败', 'error');
+      } else {
+        alert('页面跳转失败');
+      }
+    }
+  },
+
+  /**
+   * 处理buttonList按钮跳转
+   * @param {object} button - 按钮配置
+   */
+  goToFromButton(button) {
+    // 优先检查function.dest,然后是button.dest
+    const rawDestPage = button.function?.dest || button.dest;
+
+    // 功能说明:后端返回 dest(如 objInp)时自动补齐 mp_ 前缀,统一跳转到移动端页面 by xu 2026-02-28
+    const destPage = (() => {
+      const target = String(rawDestPage || '').trim();
+      if (!target) return '';
+      if (target.startsWith('mp_')) return target;
+      return `mp_${target}`;
+    })();
+
+    if (!destPage) {
+      console.warn('按钮没有配置跳转页面:', button);
+      return false;
+    }
+
+    const params = {
+      dest: destPage,
+      title: button.function?.desc || button.function?.title || button.desc || button.title || button.buttonName || button.name || '',
+      service: button.function?.servName || button.servName || button.service || ''
+    };
+
+    // 跳转页面,标记需要刷新
+    this.goTo(destPage, params, { needRefresh: true });
+    return true;
+  },
+
+  /**
+   * 返回上一页
+   * @param {object} options - 返回选项
+   */
+  goBack(options = {}) {
+    try {
+      // 检查是否需要刷新父页面
+      const urlParams = new URLSearchParams(window.location.search);
+      const needRefresh = urlParams.get('_needRefresh') === '1';
+      
+      if (needRefresh && options.refreshParent !== false) {
+        // 通知父页面刷新
+        this.notifyParentRefresh();
+      }
+      
+      // 返回上一页
+      if (window.history.length > 1) {
+        window.history.back();
+      } else {
+        NavigationManager.goBack()
+      }
+      
+    } catch (error) {
+      console.error('❌ 页面返回失败:', error);
+      if (typeof showToast !== 'undefined') {
+        showToast('页面返回失败', 'error');
+      }
+    }
+  },
+
+  /**
+   * 通知父页面刷新(通过localStorage)
+   */
+  notifyParentRefresh() {
+    try {
+      const refreshData = {
+        timestamp: Date.now(),
+        action: 'refresh',
+        source: window.location.pathname
+      };
+      
+      localStorage.setItem('_pageRefreshNotify', JSON.stringify(refreshData));
+      
+      // 触发storage事件
+      window.dispatchEvent(new StorageEvent('storage', {
+        key: '_pageRefreshNotify',
+        newValue: JSON.stringify(refreshData)
+      }));
+      
+      console.log('📢 通知父页面刷新');
+      
+    } catch (error) {
+      console.error('❌ 通知父页面刷新失败:', error);
+    }
+  },
+
+  /**
+   * 监听页面刷新通知
+   * @param {function} callback - 刷新回调函数
+   */
+  onRefreshNotify(callback) {
+    if (typeof callback !== 'function') {
+      console.warn('刷新回调必须是函数');
+      return;
+    }
+    
+    // 监听localStorage变化
+    const handleStorageChange = (event) => {
+      if (event.key === '_pageRefreshNotify' && event.newValue) {
+        try {
+          const refreshData = JSON.parse(event.newValue);
+          console.log('📢 收到页面刷新通知:', refreshData);
+          
+          // 执行刷新回调
+          callback(refreshData);
+          
+          // 清除通知
+          localStorage.removeItem('_pageRefreshNotify');
+          
+        } catch (error) {
+          console.error('❌ 处理刷新通知失败:', error);
+        }
+      }
+    };
+    
+    window.addEventListener('storage', handleStorageChange);
+    
+    // 返回清理函数
+    return () => {
+      window.removeEventListener('storage', handleStorageChange);
+    };
+  },
+
+  /**
+   * 刷新当前页面数据
+   * @param {function} refreshCallback - 刷新数据的回调函数
+   */
+  refreshCurrentPage(refreshCallback) {
+    if (typeof refreshCallback === 'function') {
+      console.log('🔄 刷新当前页面数据');
+      refreshCallback();
+    } else {
+      // 默认刷新整个页面
+      window.location.reload();
+    }
+  },
+
+  /**
+   * 获取URL参数
+   * @param {string} key - 参数名,不传则返回所有参数
+   * @returns {string|object} 参数值或参数对象
+   */
+  getUrlParam(key) {
+    const urlParams = new URLSearchParams(window.location.search);
+    
+    if (key) {
+      const value = urlParams.get(key);
+      return value ? decodeURIComponent(value) : null;
+    }
+    
+    // 返回所有参数
+    const params = {};
+    for (const [paramKey, paramValue] of urlParams) {
+      params[paramKey] = decodeURIComponent(paramValue);
+    }
+    return params;
+  }
+};
+
+// 导出到全局
+window.NavigationManager = NavigationManager;
+
+// 兼容性:也导出为全局函数
+window.goTo = NavigationManager.goTo.bind(NavigationManager);
+window.goBack = NavigationManager.goBack.bind(NavigationManager);
+window.getUrlParam = NavigationManager.getUrlParam.bind(NavigationManager);
+
+console.log('✅ H5页面导航工具加载完成');

+ 375 - 0
js/mp_validation/validation-manager.js

@@ -0,0 +1,375 @@
+// 工具方法
+const findParentTd = (element) => {
+    let parent = element.parentElement;
+    while (parent && parent.tagName !== 'TD') {
+        parent = parent.parentElement;
+    }
+    return parent;
+};
+
+const isSameTd = (element1, element2) => {
+    const td1 = findParentTd(element1);
+    const td2 = findParentTd(element2);
+    return td1 && td1 === td2;
+};
+
+class ValidationManager {
+    constructor() {
+        this.validations = new Map();
+        this.dependRules = new Map(); // 存储依赖关系
+    }
+
+    // 添加验证规则
+    add(ruleName, fields, options = {}, priority = 1) {
+        if (!Array.isArray(fields)) {
+            fields = [fields];
+        }
+
+        const rule = this._parseRule(ruleName);
+        if (!rule) return;
+
+        fields.forEach(field => {
+            if (!this.validations.has(field)) {
+                this.validations.set(field, []);
+            }
+            
+            this.validations.get(field).push({
+                rule: rule,
+                options: {
+                    msgPrfx: options.msgPrfx || field,
+                    ...options
+                },
+                priority: priority
+            });
+        });
+
+        if (options.relField) {
+            const relElement = document.querySelector(`[name="${options.relField}"]`);
+            if (relElement) {
+                relElement.addEventListener('change', () => {
+                    this.validateField(field);
+                });
+            }
+        }
+
+        this.initRequiredMarks();
+
+        // 通知移动端组件重新检查必填状态
+        this.notifyComponentsUpdate();
+
+        ssVm.bindForm(".form-container");
+    }
+
+    // 验证单个字段
+    validateField(field) {
+        const element = document.querySelector(`[name="${field}"]`);
+        if (!element) return { valid: true };
+
+        const td = findParentTd(element);
+        if (!td) return { valid: true };
+
+        // 获取同一个 td 内的所有需要验证的字段
+        const tdFields = Array.from(td.querySelectorAll('[name]'))
+            .map(el => el.getAttribute('name'))
+            .filter(name => this.validations.has(name));
+        
+        // 验证所有字段并合并错误信息
+        let errors = [];
+        let hasEmptyRequired = false; // 是否有空的必填字段
+
+        for (const f of tdFields) {
+            
+            const element = document.querySelector(`[name="${f}"]`);
+            const value = element.value;
+            
+            // 检查是否为空
+            if (!value || value.trim() === '') {
+                hasEmptyRequired = true;
+            }
+
+            const result = this._doValidate(f);
+            if (!result.valid && errors.indexOf(result.message) === -1) {
+               
+                errors.push(result.message);
+            }
+        }
+         // 检查是否有依赖规则
+         const dependRule = this.dependRules.get(field);
+         if (dependRule) {
+            const { dependField, rules } = dependRule;
+            const dependElement = document.querySelector(`[name="${dependField}"]`);
+            if (dependElement) {
+                const dependValue = dependElement.value;
+                const validatorName = rules[dependValue];
+                if (validatorName) {
+                    // 更新验证规则
+                    this.validations.set(field, [{
+                        rule: window.ValidatorRules[validatorName],
+                        options: {
+                            msgPrfx: field,
+                            required: true
+                        }
+                    }]);
+                }
+            }
+        }
+        // 管理必填标记
+        let requiredMark = td.querySelector('.required');
+        // console.log(hasEmptyRequired,errors.length,requiredMark)
+        if ((hasEmptyRequired || errors.length > 0) && !requiredMark) {
+            requiredMark = document.createElement('div');
+            requiredMark.className = 'required';
+            td.appendChild(requiredMark);
+        } else if (!hasEmptyRequired && errors.length === 0 && requiredMark) {
+            requiredMark.remove();
+        }
+
+        // 动态管理错误提示 - 仅PC端使用,移动端通过组件内部显示
+        // 检查是否为移动端环境(通过检查是否有ss-input或ss-mobile-component组件)
+        const isMobileEnv = td.querySelector('.ss-input') !== null || td.querySelector('.ss-mobile-component') !== null;
+
+        if (!isMobileEnv) {
+            // PC端:使用.err-tip元素显示错误
+            let errTip = td.querySelector('.err-tip');
+            if (!errTip && errors.length > 0) {
+                errTip = document.createElement('div');
+                errTip.className = 'err-tip';
+                errTip.style.position = 'absolute';
+                errTip.style.zIndex = '1';
+                errTip.innerHTML = `
+                    <div class="tip">${errors.join(';')}</div>
+                    <div class="tip-more">${errors.join(';')}</div>
+                `;
+                td.appendChild(errTip);
+            } else if (errTip && errors.length === 0) {
+                errTip.remove();
+            } else if (errTip && errors.length > 0) {
+                errTip.querySelector('.tip').textContent = errors.join(';');
+                errTip.querySelector('.tip-more').textContent = errors.join(';');
+            }
+        }
+        // 移动端:错误提示由ss-input组件内部处理,这里不生成.err-tip元素
+        
+        return {
+            valid: errors.length === 0,
+            message: errors.join(';')
+        };
+    }
+
+    // 验证所有字段
+    validateAll() {
+        const errors = [];
+        for (const field of this.validations.keys()) {
+            const result = this.validateField(field);
+            if (!result.valid) {
+                errors.push({
+                    field: field,
+                    message: result.message
+                });
+            }
+        }
+        
+        return {
+            valid: errors.length === 0,
+            errors: errors
+        };
+    }
+
+    // 解析规则名称并获取对应的验证规则
+    _parseRule(ruleName) {
+        const parts = ruleName.split('.');
+        const actualRuleName = parts[parts.length - 1];
+        return ValidatorRules[actualRuleName];
+    }
+
+    // 绑定到表单提交
+    bindForm(formClass) {
+        // 创建一个观察器实例
+        const observer = new MutationObserver((mutations) => {
+            const form = document.querySelector(formClass);
+            if (form && !form._bound) {
+                // console.log("Found form, binding submit event");
+                
+                $(form).on('submit', (e) => {
+                    e.preventDefault();
+
+                    const result = this.validateAll();
+                    if (!result.valid) {
+                        // 触发所有字段的校验,让移动端组件显示错误
+                        result.errors.forEach(error => {
+                            const element = document.querySelector(`[name="${error.field}"]`);
+                            if (element) {
+                                // 触发input事件,让移动端组件更新状态
+                                const inputEvent = new Event('input', { bubbles: true });
+                                element.dispatchEvent(inputEvent);
+
+                                // PC端的错误提示处理
+                                const validateComponent = element.closest('.input-container')?.querySelector('.err-tip');
+                                if (validateComponent) {
+                                    validateComponent.querySelector('.tip').textContent = error.message;
+                                    validateComponent.querySelector('.tip-more').textContent = error.message;
+                                }
+                            }
+                        });
+                        return false;
+                    } else {
+                        alert('表单校验通过!');
+                    }
+                    return true;
+                });
+
+                form._bound = true;  // 标记已绑定
+                observer.disconnect();  // 停止观察
+            }
+        });
+
+        // 开始观察
+        observer.observe(document.body, {
+            childList: true,
+            subtree: true
+        });
+    }
+    remove(fields) {
+        // 如果传入单个字段,转换为数组
+        if (!Array.isArray(fields)) {
+            fields = [fields];
+        }
+
+        // 遍历字段并移除验证规则
+        fields.forEach(field => {
+            if (this.validations.has(field)) {
+                this.validations.delete(field);
+            }
+        });
+    }
+
+    // 私有验证方法
+    _doValidate(field) {
+        const rules = this.validations.get(field);
+        if (!rules) return { valid: true };
+
+        const element = document.querySelector(`[name="${field}"]`);
+        
+        if (!element) return { valid: true };
+        
+        const value = element.value;
+        // console.log(element,value)
+        for (const {rule, options} of rules) {
+            // console.log(rule,options)
+            // 如果设置了 required 且值为空,进行必填验证
+            if (options.required && (!value || value.trim() === '')) {
+                return {
+                    valid: false,
+                    message: `${options.msgPrfx}不能为空`
+                };
+            }
+
+            // 执行规则验证,传入完整的 options 以支持关联字段验证
+            const isValid = rule.validate(value, options);
+            if (!isValid) {
+                let message = rule.message;
+                message = message.replace('{field}', options.msgPrfx);
+                Object.keys(options).forEach(key => {
+                    message = message.replace(`{${key}}`, options[key]);
+                });
+                return {
+                    valid: false,
+                    message: message
+                };
+            }
+        }
+
+        return { valid: true };
+    }
+
+    // 初始化必填标记
+    initRequiredMarks() {
+        
+        // 先收集所有需要处理的 td
+        const processedTds = new Set();
+        
+        // 遍历所有带验证规则的字段
+        for (const [field, rules] of this.validations.entries()) {
+            const element = document.querySelector(`[name="${field}"]`);
+            if (!element) continue;
+
+            const td = findParentTd(element);
+            if (!td || processedTds.has(td)) continue;  // 跳过已处理的 td
+            
+            processedTds.add(td);  // 标记该 td 已处理
+
+            // 获取同一个 td 内的所有需要验证的字段
+            const tdFields = Array.from(td.querySelectorAll('[name]'))
+                .map(el => el.getAttribute('name'))
+                .filter(name => this.validations.has(name));
+
+            // 只要有验证规则就添加必填标记
+            if (tdFields.length > 0) {
+                let requiredMark = td.querySelector('.required');
+                if (!requiredMark) {
+                    requiredMark = document.createElement('div');
+                    requiredMark.className = 'required';
+                    td.appendChild(requiredMark);
+                }
+            }
+        }
+    }
+
+    // 验证字段但不显示错误信息
+    validateFieldSilent(field) {
+        const rules = this.validations.get(field);
+        if (!rules) return { valid: true };
+
+        const element = document.querySelector(`[name="${field}"]`);
+        if (!element) return { valid: true };
+
+        const value = element.value;
+
+        for (const {rule, options} of rules) {
+            if (options.required && (!value || value.trim() === '')) {
+                return { valid: false };
+            }
+            if (!value && !options.required) {
+                return { valid: true };
+            }
+            const isValid = rule.validate(value, options);
+            if (!isValid) {
+                return { valid: false };
+            }
+        }
+
+        return { valid: true };
+    }
+
+    // 检查字段是否为必填项 - 为移动端组件提供支持
+    isRequired(field) {
+        const rules = this.validations.get(field);
+        if (!rules) return false;
+
+        // 检查是否有任何规则设置了required或者是notNull规则
+        return rules.some(({rule, options}) => {
+            return options.required || rule === ValidatorRules.notNull;
+        });
+    }
+
+    // 通知移动端组件更新状态
+    notifyComponentsUpdate() {
+        // 触发所有已挂载的ss-input组件重新检查状态
+        const inputs = document.querySelectorAll('input[name]');
+        inputs.forEach(input => {
+            // 触发一个自定义事件,让组件重新检查必填状态
+            const event = new CustomEvent('ssvm-rules-updated', {
+                bubbles: true,
+                detail: { fieldName: input.getAttribute('name') }
+            });
+            input.dispatchEvent(event);
+        });
+    }
+}
+
+
+
+ // 创建全局实例
+ window.ssVm = window.ssVm || new ValidationManager(); 
+ // 在 DOM 加载完成后初始化必填标记
+

+ 137 - 0
js/mp_validator/validator-rules.js

@@ -0,0 +1,137 @@
+window.ValidatorRules = {
+    // 非空验证
+    'notNull': {
+        validate: (value) => {
+            return value !== null && value !== undefined && value !== '';
+        },
+        message: '{field}不能为空'
+    },
+    
+    // 邮箱验证
+    'email': {
+        validate: (value) => {
+            if (!value) return true; // 如果为空则跳过验证
+            return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
+        },
+        message: '{field}格式不正确'
+    },
+    
+    // 数字验证
+    'number': {
+        validate: (value) => {
+            if (!value) return true;
+            return !isNaN(value) && typeof Number(value) === 'number';
+        },
+        message: '{field}必须是数字'
+    },
+    
+    // 最小长度
+    'minLength': {
+        validate: (value, min) => {
+            if (!value) return true;
+            return String(value).length >= min;
+        },
+        message: '{field}长度不能小于{min}个字符'
+    },
+    
+    // 最大长度
+    'maxLength': {
+        validate: (value, max) => {
+            if (!value) return true;
+            return String(value).length <= max;
+        },
+        message: '{field}长度不能超过{max}个字符'
+    },
+    
+    // 证件号验证
+    'idCard': {
+        validate: function(value, options) {
+            // 如果有关联字段,按关联字段的值选择验证规则
+            if (options.relField) {
+                const typeElement = document.querySelector(`[name="${options.relField}"]`);
+                const type = typeElement?.value;
+
+                // 内置规则映射
+                const rules = {
+                    '1': this.validateIdCard,           // 居民身份证
+                    '6': this.validateHkIdCard,         // 香港特区护照或身份证
+                    '7': this.validateMacauIdCard,      // 澳门特区护照或身份证
+                    '8': this.validateTaiwanPass,       // 台湾居民来往大陆通行证
+                    '10': this.validateForeignerCard    // 外国人永久居留证
+                };
+
+                // 如果是空值或"请选择",则跳过验证
+                if (!type || type === '') return true;
+
+                const validator = rules[type];
+                if (!validator) return true;
+
+                return validator(value);
+            }
+            
+            // 如果没有关联字段,默认使用身份证验证
+            return this.validateIdCard(value);
+        },
+        message: '{field}格式不正确',
+
+        // 居民身份证:18位数字,最后一位可能是X
+        validateIdCard(value) {
+            if (!value) return true;
+            return /^[1-9]\d{5}(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dX]$/.test(value);
+        },
+
+        // 香港特区护照或身份证:字母开头+6位数字+(数字或A)
+        validateHkIdCard(value) {
+            if (!value) return true;
+            // 支持两种格式:
+            // 1. 香港身份证:1-2个字母 + 6位数字 + (数字或A)
+            // 2. 香港特区护照:H + 8位数字
+            return /^[A-Z]{1,2}[0-9]{6}\([0-9A]\)$/.test(value) || /^H[0-9]{8}$/.test(value);
+        },
+
+        // 澳门特区护照或身份证:1/5/7 + 6位数字 + (校验位)
+        validateMacauIdCard(value) {
+            if (!value) return true;
+            // 支持两种格式:
+            // 1. 澳门身份证:1/5/7 + 6位数字 + (数字)
+            // 2. 澳门特区护照:MA + 8位数字
+            return /^[157][0-9]{6}\([0-9]\)$/.test(value) || /^MA[0-9]{8}$/.test(value);
+        },
+
+        // 台湾居民来往大陆通行证:8位数字
+        validateTaiwanPass(value) {
+            if (!value) return true;
+            return /^[0-9]{8}$/.test(value);
+        },
+        validateForeignerCard(value) {
+            if (!value) return true;
+            return /^[A-Z]{3}\d{6}$/.test(value);
+        }
+    },
+    'custom': {
+        validate: function(value, options) {
+            // 如果没有自定义验证函数,直接返回 true
+            if (!options.validate) return true;
+
+            // 获取关联字段的值(如果有)
+            let relValue;
+            if (options.relField) {
+                const relElement = document.querySelector(`[name="${options.relField}"]`);
+                relValue = relElement?.value;
+            }
+
+            // 执行自定义验证
+            const result = options.validate(value, relValue);
+            
+            // 如果返回的是对象,设置自定义错误消息
+            if (result && typeof result === 'object') {
+                this.message = result.message;
+                return result.valid;
+            }
+
+            // 如果返回的是 boolean,使用默认错误消息
+            return result;
+        },
+        message: '{field}验证失败'
+    }
+}; 

File diff suppressed because it is too large
+ 0 - 0
js/mp_vant/vant.min.js


File diff suppressed because it is too large
+ 282 - 0
js/mp_vue/vue.global.js


+ 138 - 0
js/vue/EventBus.js

@@ -0,0 +1,138 @@
+class EventBus {
+    constructor() {
+        this.eventObject = {};
+        this.callbackId = 0;
+        this.state = {}
+        // console.log("eventBus初始化了")
+    }
+    // 发布事件
+    publish(eventName, ...args) {
+        // console.log("发布事件", eventName, ...args);
+        this.state[eventName] = args.length === 1 ? args[0] : args;
+
+        // 取出当前事件所有的回调函数
+        const callbackObject = this.eventObject[eventName];
+        if (!callbackObject) return console.warn(eventName + " not found!");
+        // 执行每一个回调函数
+        for (let id in callbackObject) {
+            // 执行时传入参数
+            // console.log("准备执行回调", id); // 确认回调即将执行
+            callbackObject[id](...args);
+            // console.log("回调执行完毕", id); // 确认回调执行完毕
+
+            // 只订阅一次的回调函数需要删除
+            if (id[0] === "d") {
+                delete callbackObject[id];
+            }
+        }
+    }
+    // 订阅事件
+    subscribe(eventName, callback) {
+        // console.log("订阅事件", eventName, callback);
+        // 初始化这个事件
+        if (!this.eventObject[eventName]) {
+            // 使用对象存储,注销回调函数的时候提高删除的效率
+            this.eventObject[eventName] = {};
+        }
+
+        const id = this.callbackId;
+        this.callbackId++;  // 确保每次调用后递增
+        // 存储订阅者的回调函数
+        // callbackId使用后需要自增,供下一个回调函数使用
+        this.eventObject[eventName][id] = callback;
+        // console.log("回调ID分配:", id); // 查看ID
+
+        // 每一次订阅事件,都生成唯一一个取消订阅的函数
+        const unSubscribe = () => {
+            // 清除这个订阅者的回调函数
+            delete this.eventObject[eventName][id];
+
+            // 如果这个事件没有订阅者了,也把整个事件对象清除
+            if (Object.keys(this.eventObject[eventName]).length === 0) {
+                delete this.eventObject[eventName];
+            }
+        };
+
+        return { unSubscribe };
+    }
+
+    // 只订阅一次
+    subscribeOnce(eventName, callback) {
+        // 初始化这个事件
+        if (!this.eventObject[eventName]) {
+            // 使用对象存储,注销回调函数的时候提高删除的效率
+            this.eventObject[eventName] = {};
+        }
+
+        // 标示为只订阅一次的回调函数
+        const id = "d" + this.callbackId++;
+
+        // 存储订阅者的回调函数
+        // callbackId使用后需要自增,供下一个回调函数使用
+        this.eventObject[eventName][id] = callback;
+
+        // 每一次订阅事件,都生成唯一一个取消订阅的函数
+        const unSubscribe = () => {
+            // 清除这个订阅者的回调函数
+            delete this.eventObject[eventName][id];
+
+            // 如果这个事件没有订阅者了,也把整个事件对象清除
+            if (Object.keys(this.eventObject[eventName]).length === 0) {
+                delete this.eventObject[eventName];
+            }
+        };
+
+        return { unSubscribe };
+    }
+    // 获取状态的新方法
+    getState(eventName) {
+        return this.state[eventName];
+    }
+    // 清除事件
+    clear(eventName) {
+        // 未提供事件名称,默认清除所有事件
+        if (!eventName) {
+            this.eventObject = {};
+            this.state = {};
+            return;
+        }
+        // 清除指定事件
+        delete this.eventObject[eventName];
+        delete this.state[eventName];
+    }
+}
+if (!window.sharedEventBus) {
+    window.sharedEventBus = new EventBus();
+    window.sharedEventBus.publish('folderPath', []);
+}
+
+export const eventBus = window.sharedEventBus;
+
+export const EVEN_VAR = {
+    // 系统编辑模式变化
+    systemEditModelChange: "systemEditModelChange",
+    // 显示项目弹窗
+    showProjectDialog: "showProjectDialog",
+    showKnowledgeDialog: "KnowledgeDialog",
+    showOrcDialog: "showOrcDialog",
+    showOrcListDialog: "showOrcListDialog",
+    showZhibiaoNodeDialog: "showZhibiaoNodeDialog",
+    showVerifyDialog: "showVerifyDialog",
+    showGlobalSearchDialog: "showGlobalSearchDialog",
+    // 1.2.2录入页校验
+    showAddUserInfoDialog: "showAddUserInfoDialog",
+    // 1.5.2.新增工作节点
+    showAddWorkNodeDialog: "showAddWorkNodeDialog",
+    // 知识库变动01
+    showKnowlegeDbViewerDialog: "showKnowlegeDbViewerDialog",
+    showKnowlegeDbChangeDialog: "showKnowlegeDbChangeDialog",
+    // 编辑器
+    showEditorDialog: "showEditorDialog",
+    // 添加预警窗口
+    showAddWaringDialog: "showAddWaringDialog",
+    // 空窗口
+    showEmptyDialog: "showEmptyDialog",
+    currentPage:"index.html",
+};
+
+

+ 233 - 0
js/vue/icon-config.js

@@ -0,0 +1,233 @@
+// 公共图标
+export const commonIcon = {
+    name: "common",
+    title: "公共图标",
+    prefix: "common-icon-",
+    family: "common-iconfont",
+    types: {
+      close: "close",
+      "close-circle": "close-circle",
+      "menu-square": "menu-square",
+      menu: "menu",
+      empty: "empty",
+      add: "add",
+      slant: "slant",
+      home: "home",
+      "double-arrow-right": "double-arrow-right",
+      layer: "layer",
+      activity: "activity",
+      love: "love",
+      "full-screen": "full-screen",
+      print: "print",
+      question: "question",
+      card: "card",
+      site: "site",
+      notice: "notice",
+      smile: "smile",
+      fontSize: "fontSize",
+      paperClip: "paperClip",
+      edit: "edit",
+      video: "video",
+      "arrow-double-left": "arrow-double-left",
+      "arrow-double-right": "arrow-double-right",
+      "arrow-double-up": "arrow-double-up",
+      "arrow-double-down": "arrow-double-down",
+      photo: "photo",
+    },
+  };
+  
+  // 项目图标
+  export const ssIcon = {
+    name: "ss",
+    prefix: "ss-icon-",
+    family: "ss-iconfont",
+    types: {
+      menu: "sanheng",
+      setting: "shezhi",
+      message: "duihua2",
+      refresh: "shaugxin",
+      reset: "chongzuo",
+      back: "tuihui",
+      "question-thin": "bangzhu",
+      question: "zaixianbangzhu",
+      "full-screen": "quanping",
+      "arrow-left": "shangyizhang",
+      "arrow-up": "xiangshangjiantou",
+      "arrow-right": "xiayizhang",
+      "arrow-down": "xiangxiajiantou",
+      close: "X",
+      "close-thin": "guanbi",
+      fontSize: "changyongyu",
+      "user-project": "xiuxue",
+      user: "yonghuming",
+      userGroup: "huiyi",
+      file: "wenben",
+      "arrow-left-line": "fanhui",
+      edit: "shouxie",
+      remark: "fuzhi",
+      love: "shoucang",
+      link: "a-lianjie1",
+      search: "a-fangda1",
+      check: "baocun-youshangjiao",
+      "check-thin": "baocun",
+      focus: "laiyuan",
+      "scale-out": "fangda",
+      "scale-in": "suoxiao",
+      "user-group": "renyuan-",
+      "search-result": "quanwenjiansuo",
+      send: "fasong",
+      "db-maintenance": "duixiang",
+      quit: "tuichu",
+      word: "word",
+      cloud: "xiazai",
+      todo: "daiban",
+      print: "dayin",
+      "edit-font-family": "ziti",
+      "edit-font-size": "zihao",
+      "alarm-clock": "cuiban",
+      lightning: "kuaijiefangwen",
+      qingjia: "qingjia",
+      shoufukuan: "shoufukuan",
+      kaoqin: "kaoqin",
+      "close-mark-fill": "guanbi_jiaobiao",
+      lock: "a-lock1",
+      "un-lock": "a-mima1",
+      "lock-fill": "suoping",
+      "topic-fill": "huanfu",
+      "list-fill": "lieshu",
+      "setting-fill": "shouyeshezhi",
+      "folder-collapse-fill": "folderShare",
+      "folder-expand-fill": "olderOpen",
+      "checked-fill-circle": "a-chenggong1",
+      "info-circle": "i",
+      "forbid-circle": "tingyong",
+      "minus-circle": "jianshao",
+      "mark-bottom-success": "gouxuan",
+      "mark-bottom-error": "bugouxuan",
+      "mark-bottom-warn": "bugouxuan",
+      notice: "gongshigonggao",
+      layer: "leibie",
+      scan: "saoyisao",
+      "check-code": "yanzhengma",
+      time: "shijian",
+      cancal: "chexiao",
+      "re-cancal": "fanhuishangyibu",
+      resize: "guangbiao-tuozhuai",
+      yaoyiyao: "yaoyiyaocu",
+  
+      anjian: "anjian",
+      renwu: "renwu",
+      shanchubiaoge: "shanchubiaoge",
+      duomeiti: "duomeiti",
+      shengqing: "shengqing",
+      baocunbintijiao: "baocunbintijiao",
+      bianqian: "bianqian",
+      shanchuhang: "shanchuhang",
+      shangjiaobiao: "shangjiaobiao",
+      charulie: "charulie",
+      shanchulie: "shanchulie",
+      buhuanrao: "buhuanrao",
+      biaoqing: "biaoqing",
+      excl: "excl",
+      home: "home",
+      baoguandan: "baoguandan",
+      jinyong: "jinyong",
+      biyanjing: "biyanjing",
+      qian: "qian",
+      piliang: "piliang",
+      dianji_02: "dianji-02",
+      biaoge: "biaoge",
+      guangbiaoBianji: "guangbiao-bianji",
+      bingtu: "bingtu",
+      gerenxuanzhi: "gerenxuanzhi",
+      suofang: "suofang",
+      xieti: "xieti",
+      fukuan: "fukuan",
+      kehuguanli: "kehuguanli",
+      faxian: "faxian",
+      dianzan: "dianzan",
+      fanxuan: "fanxuan",
+      gongwen: "gongwen",
+      neirongku: "neirongku",
+      xuanzhongJiaobiao: "xuanzhong_jiaobiao",
+      cangdan: "cangdan",
+      zuoduiqi: "zuoduiqi",
+      shouqi: "shouqi",
+      charuhang: "charuhang",
+      hebing: "hebing",
+      guangbiao: "guangbiao",
+      huodong: "huodong",
+      danwei: "danwei",
+      guanwang: "guanwang",
+      canshuxiang: "canshuxiang",
+      fenlei: "fenlei",
+      zuanshi: "zuanshi",
+      tuozhuai: "tuozhuai",
+      jiacu: "jiacu",
+      denglu: "denglu",
+      shubiaojiantou: "shubiaojiantou",
+      tupian: "tupian",
+      yanjing: "yanjing",
+      xiangmushezhi: "xiangmushezhi",
+      xiajiaobiao: "xiajiaobiao",
+      wupin: "wupin",
+      qiandaizi: "qiandaizi",
+      shunpai: "shunpai",
+      fujian: "fujian",
+      zhenghe: "zhenghe",
+      zhifangtu: "zhifangtu",
+      dingwei: "dingwei",
+      xietong: "xietong",
+      diannao: "diannao",
+      guanli: "guanli",
+      tupu: "tupu",
+      daochu: "daochu",
+      yewu: "yewu",
+      zhengshu: "zhengshu",
+      zitiyangshi: "zitiyangshi",
+      zuopaihuanrao: "zuopaihuanrao",
+      youpaihuanrao: "youpaihuanrao",
+      zitishanchu: "zitishanchu",
+      zihao2: "zihao2",
+      zhognjianduiqi: "zhognjianduiqi",
+      zitiyanse: "zitiyanse",
+      yiban: "yiban",
+      wangzhan: "wangzhan",
+      caogaoxiang: "caogaoxiang",
+      chanyeleibie: "chanyeleibie",
+      chaifen: "chaifen",
+      xiahuaxian: "xiahuaxian",
+      xiangyouhebing: "xiangyouhebing",
+      xueyuan: "xueyuan",
+      yikatong: "yikatong",
+      xiangxiahebing: "xiangxiahebing",
+      wentiliebiao: "wentiliebiao",
+      wenzhang: "wenzhang",
+      youduiqi: "youduiqi",
+      yujing: "yujing",
+      caozuoshibai1: "caozuoshibai1",
+      "anjian-1": "anjian-1",
+      duihua1: "duihua1",
+      jiahao: "jiahao",
+      "a-liucheng1": "a-liucheng1",
+      "a-liangduanduiqi1": "a-liangduanduiqi1",
+      "a-ljt1": "a-ljt1",
+      wenjianjia_zhankai: "wenjianjia_zhankai",
+      "wenjianjia_shouqi": "wenjianjia_shouqi",
+      grxzFuyongshanggezhi: "grxzFuyongshanggezhi",
+      grxzMorenzhi: "grxzMorenzhi",
+      grxzBuxuan: "grxzBuxuan",
+      yaoyiyao: "yaoyiyao",
+      PE: "PE",
+      fenzhi: "fenzhi",
+      xiangliang: "xiangliang",
+      huihe: "huihe",
+      linggan: "linggan",
+      kaishi: "kaishi",
+      jieshu: "jieshu",
+      quanxuan: "quanxuan",
+      quanbuguoqu: "quanbuguoqu",
+      biaoqian: "biaoqian",
+    },
+  };
+  

+ 6142 - 0
js/vue/ss-components.js

@@ -0,0 +1,6142 @@
+import { ssIcon, commonIcon } from "./icon-config.js";
+import * as IndexComponents from "./ss-index-components.js";
+import * as EchartComponents from "./ss-echarts-compnents.js";
+import { isNum, toStyleStr } from "./tools.js";
+import { EVEN_VAR } from "./EventBus.js";
+
+// import * as elements from "../lib/element-plus.js";
+(function () {
+  const {
+    createApp,
+    ref,
+    reactive,
+    watch,
+    onMounted,
+    onBeforeUnmount,
+    h,
+    computed,
+    resolveComponent,
+    watchEffect,
+    nextTick,
+    onVnodeMounted,
+    Teleport,
+    inject,
+    provide,
+  } = Vue;
+
+  // 弹窗默认遮罩z-index
+  let currentZIndex = 100;
+  //   目前已存在的弹窗
+  const topWindow = window.top;
+  topWindow.dialogInstances = topWindow.dialogInstances || [];
+  // 新建弹窗
+  function createSsDialogInstance(setting, callbackEvent) {
+    currentZIndex += 10; // 动态提升 z-index
+    const container = document.createElement("div");
+    document.body.appendChild(container);
+    const app = Vue.createApp({
+      render() {
+        return h(SsDialog, {
+          ...setting,
+          zIndex: currentZIndex,
+          onClose() {
+            document.body.removeChild(container); // 仅移除弹窗容器
+            const index = topWindow.dialogInstances.indexOf(app);
+            if (index > -1) {
+              topWindow.dialogInstances.splice(index, 1); // 移除实例
+            }
+            // 关闭后的回调
+            if (callbackEvent && typeof callbackEvent === "function") {
+              callbackEvent();
+            }
+            app.unmount(); // 仅卸载弹窗实例
+            if (container.parentNode) {
+              container.parentNode.removeChild(container); // 确保移除容器
+            }
+          },
+        });
+      },
+    });
+    topWindow.dialogInstances.push({ app, callbackEvent, container });
+    app.component("ss-mark", SsMark); // 注册 ss-mark 组件
+    app.component("ss-icon", SsIcon);
+    app.component("ss-full-style-header", SsFullStyleHeader); // 注册 ss-full-style-header 组件
+    app.mount(container);
+  }
+  // ss-breadcrumb 一级菜单页面面包屑
+  const SsBreadcrumb = {
+    name: "SsBreadcrumb",
+    props: {
+      level: {
+        type: Object,
+        default: null,
+      },
+    },
+    setup(props) {
+      const currentMenu = ref(null);
+      const folderPath = ref([]);
+      const eventBus = window.parent.sharedEventBus;
+
+      // 监听页面变化
+      onMounted(() => {
+        // 获取初始页面
+        currentMenu.value = eventBus.getState(EVEN_VAR.currentPage);
+        folderPath.value = eventBus.getState("folderPath") || [];
+
+        // 订阅页面变化
+        eventBus.subscribe(EVEN_VAR.currentPage, (page) => {
+          currentMenu.value = page;
+        });
+        eventBus.subscribe("folderPath", (path) => {
+          folderPath.value = path || [];
+        });
+      });
+      // 修改点击处理函数
+      const handlePathClick = (index) => {
+        if (props.level?.onBack) {
+          // 截取到点击的位置,后面的路径会被销毁
+          const newPath = folderPath.value.slice(0, index + 1);
+          eventBus.publish("folderPath", newPath);
+          // 返回到对应层级
+          const targetFolder = newPath[newPath.length - 1]?.folder || null;
+          props.level.onBack(targetFolder);
+        }
+      };
+      const SsCommonIcon = resolveComponent("SsCommonIcon");
+      return () =>
+          h("div", { class: "bread-crumb" }, [
+            currentMenu.value &&
+            h(
+                "div",
+                {
+                  onClick: () => {
+                    if (props.level?.onBack) {
+                      eventBus.publish("folderPath", []);
+                      props.level.onBack(null); // 返回到根目录
+                    } else {
+                      eventBus.publish(EVEN_VAR.currentPage, currentMenu.value);
+                    }
+                  },
+                },
+                currentMenu.value.label || currentMenu.value.name
+            ),
+
+            ...(folderPath.value || [])
+                .map((folder, index) => [
+                  h(SsCommonIcon, { class: "common-icon-arrow-right" }),
+                  h(
+                      "div",
+                      {
+                        class: "bread-crumb-item",
+                        onClick: () => handlePathClick(index),
+                        style: { cursor: "pointer" },
+                      },
+                      folder.title
+                  ),
+                ])
+                .flat(),
+          ]);
+    },
+  };
+
+  // ss-input form表单的输入
+  const SsInput = {
+    name: "SsInput",  
+    inheritAttrs: false, // 不直接继承属性到组件根元素
+    props: {
+      name: {
+        type: String,
+        required: true,
+        default: "",
+      },
+      // 接收 v-model 绑定的值
+      errTip: {
+        type: String,
+      },
+      required: {
+        type: Boolean,
+        default: false,
+      },
+      placeholder: {
+        type: String,
+        default: "请输入",
+      },
+      defaultValue: [String, Number],
+      modelValue: [String, Number],
+      // 新增:附件配置
+      fj: {
+        type: Object,
+        default: null,
+      },
+      // 新增:param 配置(用于附件功能)
+      param: {
+        type: Object,
+        default: null,
+      },
+      // 新增:高度配置
+      height: {
+        type: String,
+        default: "",
+      },
+    },
+    emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值
+    setup(props, { emit }) {
+
+      const errMsg = ref("");
+      const inputRef = ref(null);
+      const textareaRef = ref(null);
+      const inputValue = ref(props.modelValue || props.defaultValue || "");
+      const contentFloatingDiv = ref(false); // 控制浮动 DIV 的显示
+      const floatingDivPosition = ref("bottom"); // 'bottom' 或 'top'
+      const isFocused = ref(false); // 跟踪焦点状态
+
+      // 附件相关变量(仅在传入 param 时初始化)
+      let fjid = ref(null);
+      let fjName = null;
+      let mode = null;
+      if (props.param && props.param.button) {
+        fjid = ref(props.param.button.val);
+        fjName = props.param.button.desc;
+        mode = props.param.mode;
+      }
+
+      const showRequired = computed(() => {
+        // 检查是否有验证规则(通过 window.ssVm 判断)
+        const hasValidationRule = window.ssVm?.validations?.has(props.name);
+
+        if (!hasValidationRule) return false;
+        if (errMsg.value) return true;
+        if (!inputValue.value) return true;
+        return false;
+      });
+
+      // 计算floatdiv应该向上还是向下展开
+      const calculateFloatingDivPosition = () => {
+        nextTick(() => {
+          const textarea = inputRef.value;
+          if (!textarea) return;
+
+          const rect = textarea.getBoundingClientRect();
+          const viewportHeight = window.innerHeight;
+
+          // 预估floatdiv的高度(最多5行 * 20px + 上下padding + border)
+          const estimatedFloatDivHeight = 20 * 5 + 10 + 2; // 5行 + padding + border = 112px
+
+          // 检查下方空间
+          const spaceBelow = viewportHeight - rect.bottom;
+
+          // 如果下方空间不足,且上方空间足够,则向上展开
+          if (spaceBelow < estimatedFloatDivHeight && rect.top > estimatedFloatDivHeight) {
+            floatingDivPosition.value = "top";
+          } else {
+            floatingDivPosition.value = "bottom";
+          }
+        });
+      };
+
+      // 计算floatdiv的top偏移量
+      const getFloatingDivTop = computed(() => {
+        if (props.height) {
+          // 有height时,padding是5px
+          return '5px';
+        } else {
+          // 没有height时是单行居中,需要计算居中位置
+          // 假设input-container高度是32px(或者从CSS读取),单行20px
+          // 居中偏移 = (容器高度 - 行高) / 2 = (32 - 20) / 2 = 6px
+          return '6px';
+        }
+      });
+
+      const validate = () => {
+        if (window.ssVm) {
+          const result = window.ssVm.validateField(props.name);
+          console.log(result);
+
+          errMsg.value = result.valid ? "" : result.message;
+        }
+      };
+
+      // 使用 watch 监听 props.errTip 和 props.modelValue 的变化
+      watch(
+        () => props.errTip,
+        (newVal) => {
+          errMsg.value = newVal;
+        },
+        { immediate: true }
+      );
+      watch(
+        () => props.modelValue,
+        (newVal) => {
+          inputValue.value = newVal;
+        }
+      );
+
+      // 挂载时的逻辑
+      onMounted(() => {
+        errMsg.value = props.errTip;
+        inputValue.value = props.modelValue || props.defaultValue || "";
+      });
+      // 计算并调整textarea的高度
+      const adjustHeight = () => {
+        nextTick(() => {
+          const textarea = textareaRef.value;
+          if (!textarea) return;
+
+          // floatDiv的textarea始终自动计算高度,不受props.height影响
+          // 重置高度以获得正确的scrollHeight
+          textarea.style.height = "auto";
+          // 计算新高度 - 统一限制为5行
+          const lineHeight = parseInt(getComputedStyle(textarea).lineHeight, 10);
+          const maxHeight = lineHeight * 5; // 统一为5行
+          const newHeight = Math.min(textarea.scrollHeight, maxHeight);
+          textarea.style.height = `${newHeight}px`;
+        });
+      };
+
+      // 检查是否应该显示浮动窗口(需要同时满足:有焦点 + 内容超出)
+      const checkShouldShowFloatingDiv = () => {
+        const textarea = inputRef.value;
+        if (!textarea) return false;
+
+        // 判断内容是否超出:scrollHeight > clientHeight
+        // scrollHeight 是内容的完整高度(包括不可见部分)
+        // clientHeight 是可视区域高度(不包括滚动的部分)
+        const isOverflow = textarea.scrollHeight > textarea.clientHeight;
+
+        // 需要同时满足:有焦点 + 内容超出
+        return isFocused.value && isOverflow;
+      };
+
+      // 定义事件处理函数
+      const onInput = (event) => {
+        const newValue = event.target.value;
+        inputValue.value = newValue;
+        emit("update:modelValue", newValue);
+        validate(); // 输入时验证
+
+        nextTick(() => {
+          // 检查是否需要显示浮动div
+          contentFloatingDiv.value = checkShouldShowFloatingDiv();
+
+          // 如果需要显示floatdiv,计算其位置
+          if (contentFloatingDiv.value) {
+            calculateFloatingDivPosition();
+          }
+        });
+        adjustHeight();
+      };
+      const onFocus = (event) => {
+        // 设置焦点状态为true
+        isFocused.value = true;
+        adjustHeight();
+
+        // 检查是否应该显示浮动窗口
+        nextTick(() => {
+          contentFloatingDiv.value = checkShouldShowFloatingDiv();
+          if (contentFloatingDiv.value) {
+            calculateFloatingDivPosition();
+          }
+        });
+      };
+
+      // 失去焦点时进行验证
+      const onBlur = (event) => {
+        emit("blur", event.target);
+        validate(); // 失焦时验证
+
+        nextTick(() => {
+          // 如果焦点不在 textarea 上,则隐藏浮动 div
+          if (!document.activeElement.classList.contains("input-control")) {
+            isFocused.value = false;
+            contentFloatingDiv.value = false;
+          }
+        });
+      };
+      const onChange = (event) => {
+        inputValue.value = event.target.value || "";
+        emit("change", inputValue.value);
+      };
+      const onMouseover = (event) => {
+        nextTick(() => {
+          // setTimeout(contentFloatingDiv.value = true, 500)
+        });
+      };
+      const onMouseleave = (event) => {
+        // contentFloatingDiv.value = false
+      };
+
+      // 附件按钮点击处理(从 SsEditor 搬运)
+      const onAttachmentClick = (e) => {
+        e.preventDefault();
+
+        if (!props.param || !props.param.button) {
+          console.warn("未配置 param 参数");
+          return;
+        }
+
+        console.log("附件点击了");
+        console.log("param", props.param);
+        console.log("cmsAddUrl", props.param.button.cmsAddUrl);
+
+        // 如果 fjid 为空,先调用 cmsAddUrl 创建
+        if (fjid.value == null || fjid.value == "") {
+          $.ajax({
+            type: "post",
+            url: props.param.button.cmsAddUrl,
+            async: false,
+            data: {
+              name: "fjid",
+              ssNrObjName: "sh",
+              ssNrObjId: "",
+            },
+            success: function (_fjid) {
+              console.log("cmsAddUrl success", _fjid);
+              fjid.value = _fjid;
+            },
+          });
+        }
+
+        // 构建参数字符串
+        var str =
+          "&nrid=T-" +
+          fjid.value +
+          "&objectId=" +
+          fjid.value +
+          "&objectName=" +
+          fjName +
+          "&callback=" +
+          (window["fjidCallbackName"] || "");
+
+        console.log("str", str);
+
+        // 打开附件编辑对话框
+        SS.openDialog({
+          src: props.param.button.cmsUpdUrl + str,
+          headerTitle: "编辑",
+          width: 900,
+          high: 664,
+          zIndex: 51,
+        });
+      };
+
+      return {
+        errMsg,
+        inputValue,
+        showRequired,
+        onInput,
+        onBlur,
+        onChange,
+        onMouseover,
+        onMouseleave,
+        contentFloatingDiv,
+        floatingDivPosition,
+        getFloatingDivTop,
+        inputRef,
+        textareaRef,
+        onFocus,
+        onAttachmentClick,
+        fjid, // 附件 ID,用于隐藏字段
+      };
+    },
+    render() {
+      const { resolveComponent, h } = Vue;
+      const SsIcon = resolveComponent("ss-icon");
+      const SsEditorIcon = resolveComponent("SsEditorIcon");
+
+
+      // 构建主textarea的样式
+      const mainTextareaStyle = {};
+      if (this.height) {
+        mainTextareaStyle.height = "auto"
+        // mainTextareaStyle.paddingTop = '5px'; // 有高度时加上padding-top
+        // mainTextareaStyle.paddingBottom = '5px'; // 有高度时加上padding-bottom
+      } else {
+        // 没有指定height时,固定为单行高度
+        mainTextareaStyle.height = '20px'; // 行高20px
+        mainTextareaStyle.lineHeight = '20px'; // 确保单行垂直居中
+        mainTextareaStyle.display = 'flex';
+        mainTextareaStyle.marginBottom  = '5px';
+      }
+
+      // 如果有附件按钮,为按钮留出空间
+      if (this.fj || this.param) {
+        mainTextareaStyle.paddingRight = '75px';
+        
+      }
+
+      const mainTextareaRows = this.height ? Math.floor(parseFloat('80px') / 20) : 1
+
+      return h("div", { class: "input" }, [
+        h("div", { class: "input-container", }, [
+          h("div", { class: "input",style:"padding:5px 0"  }, [
+            h("textarea", {
+              ref: "inputRef",
+              class: "input-control",
+              name: this.name,
+              value: this.inputValue,
+              onInput: this.onInput,
+              onFocus: this.onFocus,
+              onBlur: this.onBlur,
+              onChange: this.onChange,
+              placeholder: this.placeholder,
+              onMouseover: this.onMouseover, // 监听鼠标悬停
+              onMouseleave: this.onMouseleave, // 监听鼠标离开
+              rows:mainTextareaRows,
+              ...this.$attrs,
+              style: mainTextareaStyle,
+              autocomplete: "off",
+            }),
+            // 附件按钮(优先使用 param,兼容旧的 fj)
+            this.param || this.fj
+              ? h(
+                  "button",
+                  {
+                    type: "button",
+                    class: "fj-button",
+                    onClick: this.param ? this.onAttachmentClick : (e) => {
+                      e.preventDefault();
+                      console.log("附件配置:", this.fj);
+                    },
+                  },
+                  [
+                   
+                      h(SsEditorIcon, {
+                        class: "editor-icon-link",
+                      }),
+                      h("span", { class: "fj-button-text" }, "附件")
+                    
+                  ]
+                )
+              : null,
+            // this.showRequired ? h("div", { class: "required" }) : null,
+          ]),
+          this.contentFloatingDiv || ""
+            ? h("div", {
+                class: "floating-div",
+                style: this.floatingDivPosition === "bottom"
+                  ? {
+                      // 向下展开: 覆盖原输入框,top对齐首行
+                      top: this.getFloatingDivTop,
+                      bottom: 'auto',
+                    }
+                  : {
+                      // 向上展开: 同样覆盖原输入框,但从底部开始计算
+                      top: 'auto',
+                      bottom: this.height ? '5px' : '6px', // 对齐到原textarea的底部padding位置
+                    }
+              }, [
+                h("textarea", {
+                  ref: "textareaRef",
+                  class: "input-control",
+                  value: this.inputValue,
+                  onInput: this.onInput,
+                  onBlur: this.onBlur,
+                  onFocus: this.onFocus,
+                  onMouseover: this.onMouseover, // 监听鼠标悬停
+                  onMouseleave: this.onMouseleave, // 监听鼠标离开
+                  autocomplete: "off",
+                  onVnodeMounted: (vnode) => {
+                    vnode.el.focus();
+                  },
+                }),
+              ])
+            : null,
+          // this.errMsg ? h(SsValidate, { errMsg: this.errMsg }) : null,
+        ]),
+        // 附件相关的隐藏字段(仅在有 param 时才渲染)
+        this.param && [
+          // fjid 隐藏字段(只有当 fjid 有值时才渲染)
+          this.fjid && this.fjid.value &&
+          h("input", {
+            type: "hidden",
+            name: "fjid",
+            value: this.fjid.value,
+          }),
+          // 其他隐藏字段根据 name 生成
+          /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205
+          h("input", {
+          type: "hidden",
+          name: this.name.replace(/wj$/, "") + "Edit",
+          value: this.inputValue
+          }),
+          */
+          // h("input", {
+          //   type: "hidden",
+          //   name: this.name.replace(/wj$/, "") + "wj",
+          //   value: this.inputValue
+          // }),
+          /* 去掉。文本框不需要(这是富文本才有的) Ben 20251205
+          h("input", {
+          type: "hidden",
+          name: "ueditorpath",
+          value: this.name
+          }),
+          */
+        ],
+      ]);
+    },
+  };
+  // ss-normal-input 登录输入
+  const SsLoginInput = {
+    name: "SsLoginInput",
+    inheritAttrs: false,
+    props: {
+      errTip: {
+        type: String,
+      },
+
+      type: {
+        type: String,
+        default: "text",
+      },
+      required: {
+        type: Boolean,
+        default: false,
+      },
+      placeholder: {
+        type: String,
+        default: "请输入",
+      },
+      name: {
+        type: String,
+        default: "",
+      },
+      defaultValue: [String, Number],
+      modelValue: [String, Number],
+    },
+    emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值
+    setup(props, { emit }) {
+      const errMsg = ref("");
+      const inputRef = ref(null);
+      const textareaRef = ref(null);
+      const inputValue = ref(props.modelValue || props.defaultValue || "");
+
+      // 使用 watch 监听 props.errTip 和 props.modelValue 的变化
+      watch(
+          () => props.errTip,
+          (newVal) => {
+            errMsg.value = newVal;
+          },
+          { immediate: true }
+      );
+      watch(
+          () => props.modelValue,
+          (newVal) => {
+            inputValue.value = newVal;
+          }
+      );
+
+      // 挂载时的逻辑
+      onMounted(() => {
+        errMsg.value = props.errTip;
+        inputValue.value = props.modelValue || props.defaultValue || "";
+      });
+
+      // 定义事件处理函数
+      const onInput = (event) => {
+        const newValue = event.target.value;
+        inputValue.value = newValue;
+        emit("update:modelValue", newValue);
+      };
+      return { inputValue, onInput, inputRef, textareaRef };
+    },
+    render() {
+      return h("div", { class: "input" }, [
+        h("div", { class: "input-container" }, [
+          h("div", { class: "input" }, [
+            h("input", {
+              ref: "inputRef",
+              class: "input-control",
+              name: this.name,
+              value: this.inputValue,
+              onInput: this.onInput,
+              type: this.type,
+              placeholder: this.placeholder,
+              required: this.required,
+              ...this.$attrs,
+              autocomplete: "off",
+            }),
+            this.required ? h("div", { class: "required" }) : null,
+          ]),
+        ]),
+      ]);
+    },
+  };
+  // ss-login-button
+  const SsLoginButton = {
+    name: "SsLoginButton",
+    inheritAttrs: false,
+    props: {
+      class: {
+        type: String,
+        default: "",
+      },
+      text: {
+        type: String,
+        default: "",
+      },
+      type: {
+        type: String,
+        default: "button",
+      },
+    },
+    emits: ["click"],
+    setup(props, { emit }) {
+      // 定义事件处理函数
+      const onClick = (event) => {
+        // 发射一个 'click' 事件,你可以传递所需的参数
+        emit("click", event);
+      };
+
+      return { props, onClick };
+    },
+    render() {
+      const SsIcon = resolveComponent("ss-icon");
+      const SsLoginIcon = resolveComponent("ss-login-icon");
+      return h(
+          "button",
+          { class: "login-button", type: this.type, onClick: this.onClick },
+          [
+            h("span", [h(SsLoginIcon, { class: this.class })]),
+            h("span", {}, this.text),
+          ]
+      );
+    },
+  };
+  // ss-objp 下拉选择
+  const SsObjp = {
+    name: "SsObjp",
+    inheritAttrs: false,
+    props: {
+      filter: {
+        type: String,
+        required: false,
+      },
+      cb: {
+        type: String,
+        required: true,
+      },
+      url: {
+        type: String,
+        required: true,
+      },
+      name: {
+        type: String,
+        required: true,
+      },
+      width: {
+        type: String,
+        default: "100%",
+      },
+      placeholder: {
+        type: String,
+        default: "请选择",
+      },
+      inp: {
+        type: Boolean,
+        default: false,
+      },
+      opt: {
+        type: Array,
+        default: () => [],
+      },
+      errTip: String,
+      defaultValue: [String, Number],
+      modelValue: [String, Number],
+      direction: {
+        type: String,
+        default: "bottom",
+      },
+    },
+    emits: ["update:modelValue", "input", "blur", "change"],
+    setup(props, { emit }) {
+      const canInput = props.inp;
+      const errMsg = Vue.ref(props.errTip);
+      const selectItem = Vue.ref({});
+      let inputText = Vue.ref(""); // 用于存储输入框的文本
+      const popupWinVisible = Vue.ref(false);
+
+      const filteredOptions = Vue.ref(props.opt);
+      const popupDirection = Vue.ref("bottom");
+
+      // const showRequired = Vue.computed(() => {
+      //   const hasValidationRule = window.ssVm?.validations?.has(props.name);
+      //   if (!hasValidationRule) return false;
+      //   if (errMsg.value) return true;
+      //   if (!selectItem.value?.value) return true;
+      //   return false;
+      // });
+
+      const validate = () => {
+        if (window.ssVm) {
+          const result = window.ssVm.validateField(props.name);
+          // console.log("validate", window.ssVm.validateField(props.name));
+          errMsg.value = result.valid ? "" : result.message;
+        }
+      };
+
+      //在objPicker界面,选中value对应的项
+      const updateSelectItem = () => {
+        // console.log(props.opt);
+        const item = props.opt.find((it) => it.value === props.modelValue);
+        if (item) {
+          selectItem.value = item;
+          inputText.value = item.label;
+        } else {
+          selectItem.value = { label: "", value: "" };
+          inputText.value = "";
+        }
+        // validate();
+      };
+
+      Vue.watch(
+          () => props.errTip,
+          (newVal) => {
+            errMsg.value = newVal;
+          }
+      );
+
+      Vue.watch(() => props.modelValue, updateSelectItem, { immediate: true });
+      Vue.watch(
+          () => props.opt,
+          (newVal) => {
+            updateSelectItem();
+            filteredOptions.value = [...newVal];
+            // console.log("filteredOptions", filteredOptions.value);
+          }
+      );
+
+      //初始化objPicker在页面刚打开时的默认值
+      async function initDefaultValue() {
+        try {
+          if (props.url && props.cb && props.modelValue) {
+
+            let objectPickerParam;
+            let url = props.url;
+
+            //如果有定义过滤器
+            if (props.filter) {
+              //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{&quot;dwid&quot;:&quot;88&quot;},注意key也必需用单引号包着
+              // const decodedString = props.filter.replace(/&quot;/g, '"'); // 转换为: {"dwid":"88"}
+              // objectPickerParam = JSON.parse(decodedString); // 转为json对象
+
+              const filterObj = props.filter; // 转为json对象
+              for (let k in filterObj) {
+                let v = filterObj[k];
+                url += "&" + k + "=" + v;
+              }
+
+              objectPickerParam = props.filter; // 转为json对象
+              objectPickerParam["input"] = props.inp;
+              objectPickerParam["codebook"] = props.cb;
+
+
+              // alert(url);
+            } else {
+              objectPickerParam = { input: props.inp, codebook: props.cb };
+            }
+
+            const objectPickerParamStr = JSON.stringify(objectPickerParam);
+
+            const params = new URLSearchParams();
+            params.append("objectpickerparam", objectPickerParamStr);
+            params.append("objectpickertype", "2");
+            params.append("objectpickervalue", props.modelValue); //需回显的值
+
+            // alert("1params:"+JSON.stringify(params));
+
+            axios
+                .post(props.url, params, {
+                  headers: {
+                    "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
+                  },
+                })
+                .then((response) => {
+                  // alert(JSON.stringify(response.data));
+
+                  if ("timeout" == response.data.statusText) {
+                    alert("网络超时!");
+                    return;
+                  }
+
+                  if (response.data.result) {
+                    const keys = Object.keys(response.data.result);
+                    if (keys.length === 1) {
+                      let code = keys[0];
+                      let desc = response.data.result[keys[0]];
+                      if (props.opt)
+                        props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
+                      else {
+                        props.opt = [];
+                      }
+                      props.opt.push({ label: desc, value: code });
+                      updateSelectItem();
+
+                      // alert('props.opt:'+JSON.stringify(props.opt));
+                    }
+                  }
+                });
+          }
+        } catch (error) {
+          // callback(null, error.message); // 失败回调,传递错误
+        }
+      }
+
+      // Vue.onMounted(updateSelectItem);
+
+      const doSelectItem = (item) => {
+        emit("update:modelValue", item.value);
+        selectItem.value = item;
+        inputText.value = item.label;
+        hidePopup();
+        nextTick(() => {
+          console.log(item.value + "@@@props.modelValue:" + props.modelValue);
+          validate();
+          if (window.ssVm) {
+            // 遍历所有验证规则,找到依赖当前字段的规则
+            for (const [field, rules] of window.ssVm.validations.entries()) {
+              for (const rule of rules) {
+                if (rule.opt?.relField === props.name) {
+                  // console.log("Found dependent field:", field); // 调试日志
+                  window.ssVm.validateField(field);
+                }
+              }
+            }
+          }
+        });
+      };
+
+      //可录入的objPicker,更新下拉菜单选项
+      async function updateOptionBYInputText(inpTxt) {
+        try {
+          let objectPickerParam;
+          let url = props.url;
+
+          if (props.url && props.cb) {
+            //如果有定义过滤器
+            if (props.filter) {
+
+              const filterObj = props.filter; // 转为json对象
+              for (let k in filterObj) {
+                let v = filterObj[k];
+                url += "&" + k + "=" + v;
+              }
+
+              //包含HTML实体的JSON字符串转为JSON对象,如原字符串是{&quot;dwid&quot;:&quot;88&quot;},注意key也必需用单引号包着
+              // const decodedString = props.filter.replace(/&quot;/g, '"'); // 转换为: {"dwid":"88"}
+              // objectPickerParam = JSON.parse(decodedString); // 转为json对象
+              objectPickerParam = props.filter;
+              objectPickerParam["input"] = props.inp;
+              objectPickerParam["codebook"] = props.cb;
+
+
+              // alert(url);
+            } else {
+              objectPickerParam = { input: props.inp, codebook: props.cb };
+            }
+
+            const objectPickerParamStr = JSON.stringify(objectPickerParam);
+            const params = new URLSearchParams();
+            params.append("objectpickerparam", objectPickerParamStr);
+            params.append("objectpickertype", "1");
+            if (props.inp && props.inp === "true") {
+              params.append("objectpickersearchAll", 0); //只查录入的值
+              params.append("objectpickerinput", inpTxt); //录入的值
+            } else {
+              params.append("objectpickersearchAll", 1);
+            }
+
+            axios
+                .post(url, params, {
+                  headers: {
+                    "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
+                  },
+                })
+                .then((response) => {
+                  if ("timeout" == response.data.statusText) {
+                    alert("网络超时!");
+                    return;
+                  }
+
+                  if (response.data.result) {
+                    const keys = Object.keys(response.data.result);
+                    // console.log("params:"+params+"@@response.data:"+JSON.stringify(response.data));
+                    if (keys.length > 0) {
+                      if (props.opt)
+                        props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
+                      else {
+                        props.opt = [];
+                      }
+                      for (let k in response.data.result) {
+                        props.opt.push({
+                          label: response.data.result[k],
+                          value: k,
+                        });
+                      }
+
+                      // console.log('###inpTxt:'+inpTxt+';');
+                      if (
+                          props.inp &&
+                          props.inp === "true" &&
+                          inpTxt.length > 0
+                      ) {
+                        //对于可录入的,用已录入的值作过滤
+
+                        filteredOptions.value = props.opt.filter((option) =>
+                            option.label
+                                .toLowerCase()
+                                .includes(inputText.value.toLowerCase())
+                        );
+                        filteredOptions.value.unshift({ label: "", value: "" });
+
+                        // console.log('###做了过滤:'+inputText.value.toLowerCase()+';');
+                      } else {
+                        filteredOptions.value = props.opt;
+                        filteredOptions.value.unshift({ label: "", value: "" });
+                      }
+
+                      if (!popupWinVisible.value) {
+                        popupWinVisible.value = true; // 确保下拉框在输入时打开
+                      }
+
+                      console.log("props.opt11:" + JSON.stringify(props.opt));
+                    }
+                  }
+                });
+          }
+        } catch (error) {
+          // callback(null, error.message); // 失败回调,传递错误
+        }
+      }
+
+      // 计算弹出方向的方法
+      const calculatePopupDirection = () => {
+        // 1. 获取select容器元素
+        const selectEl = document
+            .querySelector(`[name="${props.name}"]`)
+            ?.closest(".select-container");
+        if (!selectEl) return;
+
+        // 2. 获取位置信息
+        const selectRect = selectEl.getBoundingClientRect();
+        const viewportHeight = window.innerHeight;
+
+        // 3. 简单判断:如果下方剩余空间小于300px就向上弹出
+        const spaceBelow = viewportHeight - selectRect.bottom;
+
+        // 调试信息
+        // console.log('空间判断:', {
+        //   elementBottom: selectRect.bottom,
+        //   viewportHeight,
+        //   spaceBelow,
+        //   willPopUp: spaceBelow < 300,
+        //   popupDirection: popupDirection.value
+        // });
+
+        // 4. 设置方向
+        popupDirection.value = spaceBelow < 300 ? "top" : "bottom";
+      };
+
+      //点击下拉菜单的文本区域时,会触发的方法
+      function togglePopup() {
+        //可录入的objPicker,更新下拉菜单选项
+        updateOptionBYInputText(inputText.value);
+        // popupWinVisible.value = !popupWinVisible.value;
+        Vue.nextTick(() => {
+          calculatePopupDirection();
+        });
+      }
+
+      const hidePopup = () => {
+        popupWinVisible.value = false;
+      };
+
+      //点击下拉菜单的三角形时,会触发的方法
+      const suffixClick = () => {
+        //可录入的objPicker,更新下拉菜单选项
+        updateOptionBYInputText("");
+        // popupWinVisible.value = !popupWinVisible.value;
+        Vue.nextTick(() => {
+          calculatePopupDirection();
+        });
+        console.log("点三角");
+      };
+
+      //可录入的objPicker,录入项变化时,会触发
+      async function handleInputChange(event) {
+        inputText.value = event.target.value;
+        if (!inputText.value) {
+          inputText.value = "";
+        }
+
+        //可录入的objPicker,更新下拉菜单选项
+        updateOptionBYInputText(inputText.value);
+
+        // filteredOptions.value = props.opt.filter((option) =>
+        //     option.label.toLowerCase().includes(inputText.value.toLowerCase())
+        // );
+        // if (!popupWinVisible.value) {
+        //   popupWinVisible.value = true; // 确保下拉框在输入时打开
+        // }
+      }
+      Vue.onMounted(() => {
+        initDefaultValue();
+        window.addEventListener("resize", calculatePopupDirection);
+      });
+      Vue.onUnmounted(() => {
+        window.removeEventListener("resize", calculatePopupDirection);
+      });
+
+      return {
+        errMsg,
+        selectItem,
+        inputText,
+        canInput,
+        filteredOptions,
+        popupWinVisible,
+        popupDirection,
+        suffixClick,
+        togglePopup,
+        hidePopup,
+        doSelectItem,
+        handleInputChange,
+      };
+    },
+
+    template: `
+      <div class="input" style="position: relative" :style="{width: width}">
+        <div class="select-container" @mouseleave="hidePopup">
+          <div class="input" @click="togglePopup">
+            <input 
+              type="hidden"
+              :name="name"
+              :value="selectItem.value"
+              .value="selectItem.value" 
+            />
+            <input
+              v-model="inputText"
+              @input="handleInputChange"
+              v-if="canInput"
+              :placeholder="placeholder"
+              
+            />
+            
+            <input
+              v-else
+              :placeholder="placeholder"
+              :value="selectItem.label"
+              
+              disabled
+              style="pointer-events: none;"
+            />
+           
+            <div class="suffix" @click.stop="suffixClick">
+              <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
+              
+            </div>
+          </div>
+         
+            
+          <div v-show="popupWinVisible" class="popup-win" :class="popupDirection">
+            <div v-if="opt && opt.length && filteredOptions.length > 0" class="popup-content">
+              <div class="content-area">
+                <div v-for="(item, index) in filteredOptions" :key="index" @click="doSelectItem(item)" :class="{ active: item.value === selectItem.value }">
+                  <span class="check-icon">
+                   
+                    <ss-form-icon class="form-icon-select-checked" />
+                  </span>
+                  <span>{{ item.label }}</span>
+                </div>
+              </div>
+            </div>
+            <div v-else class="popup-content"><div class="content-area"><div class="content-area"> <span>无选项</span></div></div></div>
+          </div>
+        </div>
+      </div>
+
+    `,
+  };
+  // ss-hidden 隐藏字段组件
+  const SsHidden = {
+    name: "SsHidden",
+    props: {
+      modelValue: String,
+      name: {
+        type: String,
+        required: true,
+      },
+      rule: {
+        type: String,
+        required: true,
+      },
+
+      param: {
+        type: String,
+        required: true,
+      },
+      url: {
+        type: String,
+        required: true,
+      },
+    },
+    emits: ["update:modelValue"],
+
+    setup(props, { emit }) {
+      const errMsg = Vue.ref("");
+      const validate = () => {
+        if (window.ssVm) {
+          const result = window.ssVm.validateField(props.name);
+          console.log("validate", window.ssVm.validateField(props.name));
+          errMsg.value = result.valid ? "" : result.message;
+        }
+      };
+      Vue.onMounted(() => {
+        /**
+         * 初始化级联菜单值初始值思路:
+         * 1. 带隐藏字段(即带编码规则)的级联菜单
+         *  在隐藏字段这,可以取到要回显的值和编码规则,从而计算出各级下拉菜单要回显的值。
+         *  然后通过ajax取各级级联菜单的值回显。
+         *  2. 不带隐藏字段的级联,只能在各个下拉菜单的setup事件中通过ajax取回显值回显
+         */
+
+        // 当同组级联下拉菜单选中值变化时,会调用本隐藏字段下面这方法设置隐藏字段值
+        window.addEventListener(
+            "cascader-setHiddenVal-" + props.name,
+            (event) => {
+              const { value } = event.detail;
+              emit("update:modelValue", value);
+              console.log(value);
+              setTimeout(() => {
+                validate();
+              }, 50);
+            }
+        );
+
+        // 如果有初始值,触发回显过程
+        if (props.modelValue) {
+          console.log("级联隐藏字段,开始回显,初始值:", props.modelValue);
+          triggerCascaderEcho(props.modelValue);
+          validate();
+        }
+      });
+
+      // 触发级联回显
+      const triggerCascaderEcho = (code) => {
+        /**
+         * 开始回显,初始值: 440304
+         * 解析后的所有值: Array(3)0: "440000"1: "440300"2: "440304"length: 3[[Prototype]]: Array(0)
+         */
+        const values = parseHiddenCodeForAll(code, props.rule);
+        console.log("解析后的所有值:", values);
+
+        // 转换为 JSON 对象
+        // const paramObj = JSON.parse(props.param);
+        const paramObj = props.param;
+        let selectArr = paramObj.fieldOrd; //保存本组级联菜单项的数组,如:['hksheng','hkshi','hkxian']
+        if (selectArr.length != values.length) {
+          // alert('属性'+props.name+'的值'+code+'与级联菜单中下拉菜单的数目不匹配!');
+          return;
+        }
+
+        // 按顺序触发回显,并增加延迟确保数据加载
+        /**
+         * 通过隐藏字段的setup事件,
+         * 循环遍历各级下拉菜单,并触发定义在下拉菜单中的'cascader-echo'事件,
+         * 在此事件中完成每个下拉菜单回显值操作(只取当前要回显的键值对显示,
+         * 下拉菜单所有的值,在点击下拉菜单时,才通过ajax取)。
+         */
+        values.forEach((value, index) => {
+          if (value) {
+            setTimeout(() => {
+              let upperVal = undefined;
+              if (index != 0) {
+                upperVal = values[index - 1];
+              }
+
+              const echoEvent = new CustomEvent(
+                  "cascader-echo-" + selectArr[index],
+                  {
+                    detail: {
+                      name: props.name,
+                      value: value,
+                      // level: index + 1,
+                      isAuto: true, // 标记为自动回显
+                      upperVal: upperVal,
+                    },
+                  }
+              );
+
+              console.log(props.name + "--upperValue:" + upperVal);
+
+              window.dispatchEvent(echoEvent);
+            }, index * 500); // 每级增加500ms延迟
+          }
+        });
+      };
+
+      // 解析所有级别的代码
+      const parseHiddenCodeForAll = (code, rule) => {
+        if (!code || !rule) return [];
+
+        // 获取规则中每段的长度
+        const segments = [];
+        let currentChar = rule[0];
+        let currentLength = 1;
+
+        for (let i = 1; i < rule.length; i++) {
+          if (rule[i] === currentChar) {
+            currentLength++;
+          } else {
+            segments.push(currentLength);
+            currentChar = rule[i];
+            currentLength = 1;
+          }
+        }
+        segments.push(currentLength);
+
+        // 解析每一级的值
+        const values = [];
+        let position = 0;
+
+        segments.forEach((length, index) => {
+          const value = code
+              .substring(0, position + length)
+              .padEnd(rule.length, "0");
+          values.push(value);
+          position += length;
+        });
+
+        return values;
+      };
+      watchEffect(() => {});
+      return {};
+    },
+    template: `<input type="hidden" :name="name" :value="modelValue">`,
+  };
+  // ss-cascader 级联选择器
+  const SsCcp = {
+    name: "SsCcp",
+    inheritAttrs: false,
+    props: {
+      modelValue: String,
+
+      name: {
+        type: String,
+        required: true,
+      },
+      level: {
+        type: Number,
+        required: true,
+      },
+      opt: {
+        type: Array,
+        default: () => [],
+      },
+      placeholder: {
+        type: String,
+        default: "请选择",
+      },
+      width: {
+        type: String,
+        default: "150px",
+      },
+      direction: {
+        type: String,
+        default: "bottom",
+      },
+      mode: {
+        type: String,
+        default: "1",
+      },
+
+      //级联菜单配置参数,如果是数组,则代表本下拉菜单是多套级联菜单共用的第一级菜单。如果是对象,则只有一套级联菜单用此下拉菜单。
+      param: {
+        type: String,
+        required: true,
+      },
+      //向后台拿数据的url
+      url: {
+        type: String,
+        required: true,
+      },
+    },
+    emits: ["update:modelValue", "change"],
+
+    setup(props, { emit }) {
+      // alert('级联菜单初始化:'+props.name+':--:'+props.modelValue);
+      const selectItem = Vue.ref({ label: props.placeholder, value: "" });
+      const popupWinVisible = Vue.ref(false);
+      const isAutoEcho = Vue.ref(false); // 用于标记是否是自动回显
+      const upperValue = Vue.ref(""); //上级下拉菜单当前值,在初始化下拉菜单默认值时,和上级下拉菜单的值变化时,修改此upperValue变量
+      const popupDirection = Vue.ref("bottom");
+
+      //有隐藏字段的下拉菜单,加载菜单项并展开事件
+      // 被上级下拉菜单选中值后,触发本下拉菜单刷新菜单项并弹出显示
+      window.addEventListener("cascader-open-" + props.name, async (event) => {
+        const { upperVal } = event.detail;
+        upperValue.value = upperVal;
+        console.log(
+            "22props.name:" +
+            props.name +
+            ",22props.upperValue:" +
+            upperValue.value
+        );
+
+        selectItem.value = ""; //清除本下拉菜单当前选中的值
+        emit("update:modelValue", ""); //通知父级
+
+        //清空下拉菜单,并设置第一项的值为placeholder
+        clearAndInit1stOpt();
+
+        //下个下拉菜单名
+        let nextSelName = getNextSel(props.name, props.param.fieldOrd);
+        if (nextSelName) {
+          //清下个下拉菜单选中值和选项
+          event = new CustomEvent("cascader-cleanOpt-" + nextSelName, {
+            detail: {},
+          });
+          window.dispatchEvent(event);
+        }
+
+        showPopup();
+      });
+
+      //设置mode2的下级下拉菜单的上级菜单当前值
+      function setNextSelectUpperValue() {
+        //设置下级菜单的上级菜单当前值upperValue
+        let paramArr = undefined;
+        if (Array.isArray(props.param)) {
+          paramArr = props.param;
+        } else {
+          paramArr = [];
+          paramArr.push(props.param);
+        }
+
+        for (const oneParam of paramArr) {
+          //下个下拉菜单名
+          const nextSelName = getNextSel(props.name, oneParam.fieldOrd);
+          if (nextSelName) {
+            setTimeout(() => {
+              const openNextEvent = new CustomEvent(
+                  "cascade-setUpperVal-" + nextSelName,
+                  {
+                    detail: {
+                      upperVal: props.modelValue,
+                    },
+                  }
+              );
+              window.dispatchEvent(openNextEvent);
+            }, 100);
+          }
+        } // end for
+      }
+
+      // 把上级 级联下拉菜单的值,设置进本组件的事件
+      window.addEventListener("cascade-setUpperVal-" + props.name, (event) => {
+        // alert('props.name:'+props.name+',props.upperValue:'+event.detail.upperVal);
+        const { upperVal } = event.detail;
+        upperValue.value = upperVal;
+
+        // console.log('props.name:'+props.name+',props.upperValue:'+upperValue.value);
+      });
+
+      //清空下拉菜单,并设置第一项的值为空
+      function clearAndInit1stOpt() {
+        if (props.opt)
+          props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
+        else {
+          props.opt = [];
+        }
+        props.opt.push({ label: "", value: "" });
+      }
+
+      //获取下一级下拉菜单,如果下一级下拉菜单不存在,则返回undefined
+      function getNextSel(selName, selNameArr) {
+        // 检查参数有效性
+        if (!Array.isArray(selNameArr) || selNameArr.length === 0) {
+          return undefined;
+        }
+
+        // 查找当前元素的索引
+        const currentIndex = selNameArr.indexOf(selName);
+
+        // 如果元素不存在或已经是最后一个元素,返回undefined
+        if (currentIndex === -1 || currentIndex === selNameArr.length - 1) {
+          return undefined;
+        }
+
+        // 返回下一个元素
+        return selNameArr[currentIndex + 1];
+      }
+
+      // 处理选择事件
+      const doSelectItem = (item) => {
+        selectItem.value = item;
+        emit("update:modelValue", item.value); //修改本下拉菜单在vue中保存的值
+
+        // alert('item.value:'+item.value);
+        if (props.mode === "1") {
+          // mode 1 模式:修改隐藏字段值
+
+          let event = new CustomEvent(
+              "cascader-setHiddenVal-" + props.param.combField,
+              {
+                detail: {
+                  value: item.value,
+                },
+              }
+          );
+          window.dispatchEvent(event);
+        }
+        emit("change", item.value); //触发配置的change方法
+
+        //设置下级菜单的上级菜单当前值upperValue
+        let paramArr = undefined;
+        if (Array.isArray(props.param)) {
+          paramArr = props.param;
+        } else {
+          paramArr = [];
+          paramArr.push(props.param);
+        }
+
+        for (const oneParam of paramArr) {
+          //下个下拉菜单名
+          const nextSelName = getNextSel(props.name, oneParam.fieldOrd);
+          if (nextSelName) {
+            setTimeout(() => {
+              const openNextEvent = new CustomEvent(
+                  "cascader-open-" + nextSelName,
+                  {
+                    detail: {
+                      upperVal: item.value,
+                    },
+                  }
+              );
+              window.dispatchEvent(openNextEvent);
+            }, 100);
+          }
+        } // end for
+
+        hidePopup();
+
+        //下个下拉菜单名
+        // let nextSelName = getNextSel(props.name, props.param.fieldOrd);
+        // if(nextSelName){
+        //   // //设置下一级下拉菜单中保存的本下拉菜单值(upperValue)
+        //   // event = new CustomEvent('cascade-setUpperVal-'+nextSelName, {
+        //   //   detail: {
+        //   //     value: item.value
+        //   //   }
+        //   // });
+        //   // window.dispatchEvent(event);
+        //
+        //   //触发下一级下拉菜单,重新初始化下拉菜单项并弹出显示
+        //   event = new CustomEvent('cascader-open-' +nextSelName, {
+        //     detail: {
+        //       upperVal: item.value
+        //     }
+        //   });
+        //   window.dispatchEvent(event);
+        // }
+
+        // 只在手动选择时自动展开下一级
+        // if (!isAutoEcho.value) {
+        //   const nextLevel = props.level + 1;
+        //   setTimeout(() => {
+        //     const openNextEvent = new CustomEvent('open-next-cascader', {
+        //       detail: {
+        //         name: props.name,
+        //         level: nextLevel
+        //       }
+        //     });
+        //     window.dispatchEvent(openNextEvent);
+        //   }, 100);
+        // }
+      };
+
+      // 监听下一级展开事件 (仅 mode 2)
+      window.addEventListener("cascade-open", (event) => {
+        if (props.mode === "2") {
+          const { level } = event.detail;
+          if (level === props.level) {
+            popupWinVisible.value = true;
+          }
+        }
+      });
+
+      if (props.mode === "1") {
+        //如果是有隐藏字段的下拉菜单
+        // 监听回显事件
+        window.addEventListener(
+            "cascader-echo-" + props.name,
+            async (event) => {
+              const { name, value, isAuto, upperVal } = event.detail;
+              // level,
+              if (upperVal) {
+                upperValue.value = upperVal;
+                console.log(
+                    "value:" +
+                    value +
+                    ",upperValue:" +
+                    upperValue +
+                    ",初始化级联组件时props.name:" +
+                    props.name
+                );
+              }
+
+              // if (name === props.name && level === props.level) {
+              // 设置自动回显标记
+              isAutoEcho.value = true;
+
+              // if (props.opt.length === 0) {
+              // const loadDataEvent = new CustomEvent('cascader-load-data', {
+              //   detail: {
+              //     name: props.name,
+              //     level: props.level,
+              //     value: value
+              //   }
+              // });
+              // window.dispatchEvent(loadDataEvent);
+
+              //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。
+
+              //Vue.watch用于监听数据的变化,并在数据变化时执行特定的回调函数。
+              //这段代码使用了 Vue.js 的 watch API 来监听 props.opt 的变化,如果props.opt有变化,则自动
+
+              //   const unwatch = Vue.watch(
+              //       () => props.opt,                // 监听的数据源(props.opt)
+              //       (newOptions) => {              // 回调函数
+              //         if (newOptions.length > 0) { // 条件判断
+              //           matchAndSelect(value);     // 执行逻辑
+              //           unwatch();                 // 停止监听
+              //         }
+              //       },
+              //       { immediate: true }            // 配置:立即触发一次
+              //   );
+              // } else {
+              //   matchAndSelect(value);
+              // }
+
+              // 初始化级联菜单在页面刚打开时的默认值
+              async function initDefaultValue(value) {
+                try {
+                  // alert(1);
+                  if (props.url && props.param
+                      // && props.modelValue 对于有rule编码规则的级联菜单(即mode=1),modelValue一定是空的,所以注释掉,修复mode=1的级联菜单无法回显问题。Ben(20251124)
+                  ) {
+                    // alert(2);
+                    /**
+                     *         let objectPickerParam=
+                     *             {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," +
+                     *                     "\"name\":\"hksheng\"," +
+                     *                     "\"cascadingName\":\"dq\",\"cascadingInputsName\":\"hkdqm\"," +
+                     *                     "\"codebook\":\"sheng\"}",
+                     *                 "objectpickertype":2,
+                     *                 "objectpickervalue":"440000"
+                     *             };
+                     */
+
+                    const objectPickerParam = {
+                      input: "false",
+                      cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
+                      name: props.name, //本下拉菜单名
+                      cascadingName: props.param.name, //级联菜单名
+                      cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm
+                      codebook: props.param.codebook,
+                    };
+
+                    const objectPickerParamStr =
+                        JSON.stringify(objectPickerParam);
+
+                    const params = new URLSearchParams();
+                    params.append("objectpickerparam", objectPickerParamStr);
+                    params.append("objectpickertype", "2");
+                    params.append("objectpickervalue", value); //需回显的值
+
+                    axios
+                        .post(props.url, params, {
+                          headers: {
+                            "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
+                          },
+                        })
+                        .then((response) => {
+                          // alert(JSON.stringify(response.data));
+
+                          if ("timeout" == response.data.statusText) {
+                            alert("网络超时!");
+                            return;
+                          }
+
+                          if (response.data.result) {
+                            const keys = Object.keys(response.data.result);
+                            if (keys.length === 1) {
+                              let code = keys[0];
+                              let desc = response.data.result[keys[0]];
+                              clearAndInit1stOpt();
+                              props.opt.push({ label: desc, value: code });
+
+                              if (value) matchAndSelect(value);
+                              // updateSelectItem();
+
+                              // alert('props.opt:'+JSON.stringify(props.opt));
+                            }
+                          }
+                        });
+                  }
+                } catch (error) {
+                  alert(error);
+                  // callback(null, error.message); // 失败回调,传递错误
+                }
+              }
+
+              //下面的代码只用于页面刚打开时,初始化级联菜单的回显值。
+              initDefaultValue(value);
+
+              // 延迟重置自动回显标记
+              setTimeout(() => {
+                isAutoEcho.value = false;
+              }, 500);
+            }
+        );
+
+        // 被上级下拉菜单触发的,清除选中值和下拉菜单选项
+        window.addEventListener(
+            "cascader-cleanOpt-" + props.name,
+            async (event) => {
+              upperValue.value = "";
+
+              selectItem.value = ""; //清除本下拉菜单当前选中的值
+              emit("update:modelValue", ""); //通知父级
+
+              //清空所有下拉菜单项
+              if (props.opt) {
+                props.opt.length = 0;
+              } else {
+                props.opt = [];
+              }
+
+              //下个下拉菜单名
+              let nextSelName = getNextSel(props.name, props.param.fieldOrd);
+              // alert('nextSelName:'+nextSelName+'--,props.name:'+props.name);
+              if (nextSelName) {
+                //清下个下拉菜单选中值和选项
+                event = new CustomEvent("cascader-cleanOpt-" + nextSelName, {
+                  detail: {},
+                });
+                window.dispatchEvent(event);
+              }
+            }
+        );
+      } else if (props.mode === "2") {
+        //没隐藏字段的下拉菜单,在这初始化默认值
+
+        let needInitParam = undefined;
+        if (Array.isArray(props.param)) {
+          needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项
+          console.log("needInitParam最后一项:" + JSON.stringify(needInitParam));
+        } else {
+          needInitParam = props.param;
+        }
+
+        // 初始化级联菜单在页面刚打开时的默认值
+        async function initDefaultValue(value, param) {
+          try {
+            // alert(1);
+            if (props.url && param && props.modelValue) {
+              // alert(2);
+              /**
+               *        let param=
+               *             {"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"rylbm,gwid\"," +
+               *                     "\"name\":\"gwid\",\"cascadingName\":\"rylb_gw\"," +
+               *                     "\"codebook\":\"gwByRylb\"}",
+               *                 "objectpickertype":2,
+               *                 "objectpickervalue":"102121"};
+               */
+
+                  // alert('props.name:'+props.name+',props.param.fieldOrd:'+props.param.fieldOrd);
+
+              const objectPickerParam = {
+                    input: "false",
+                    cascadingLevel: param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
+                    name: props.name, //本下拉菜单名
+                    cascadingName: param.name, //级联菜单名
+                    codebook: param.codebook,
+                  };
+
+              const objectPickerParamStr = JSON.stringify(objectPickerParam);
+
+              const sendParams = new URLSearchParams();
+              sendParams.append("objectpickerparam", objectPickerParamStr);
+              sendParams.append("objectpickertype", "2");
+              sendParams.append("objectpickervalue", value); //需回显的值
+
+              axios
+                  .post(props.url, sendParams, {
+                    headers: {
+                      "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
+                    },
+                  })
+                  .then((response) => {
+                    // alert(JSON.stringify(response.data));
+
+                    if ("timeout" == response.data.statusText) {
+                      alert("网络超时!");
+                      return;
+                    }
+
+                    if (response.data.result) {
+                      const keys = Object.keys(response.data.result);
+
+                      console.log(
+                          "name:" +
+                          props.name +
+                          ",@@级联初始化默认值value:" +
+                          value +
+                          "--param:" +
+                          JSON.stringify(param) +
+                          "--objectPickerParamStr:" +
+                          objectPickerParamStr +
+                          "--response.data:" +
+                          JSON.stringify(response.data)
+                      );
+
+                      if (keys.length === 1) {
+                        let code = keys[0];
+                        let desc = response.data.result[keys[0]];
+
+                        if (props.opt)
+                          props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
+                        else {
+                          props.opt = [];
+                        }
+                        props.opt.push({ label: desc, value: code });
+
+                        if (value) matchAndSelect(value);
+
+                        console.log(
+                            "GOOD mode2回显的默认值:" +
+                            JSON.stringify({ label: desc, value: code }) +
+                            "--props.param:" +
+                            JSON.stringify(param)
+                        );
+
+                        // updateSelectItem();
+
+                        // alert('props.opt:'+JSON.stringify(props.opt));
+                      }
+                    }
+                  });
+            }
+          } catch (error) {
+            alert(error);
+            // callback(null, error.message); // 失败回调,传递错误
+          }
+
+          // 重置自动回显标记
+          isAutoEcho.value = false;
+        }
+
+        // 初始化级联菜单在页面刚打开时的默认值
+        initDefaultValue(props.modelValue, needInitParam);
+
+        //设置mode2的下级下拉菜单的上级菜单当前值
+        setNextSelectUpperValue();
+      }
+
+      //选中要回显的默认值
+      const matchAndSelect = (value) => {
+        const matchedOption = props.opt.find((opt) => opt.value === value);
+        if (matchedOption) {
+          selectItem.value = matchedOption;
+          emit("update:modelValue", value);
+          emit("change", value);
+        }
+      };
+
+      const calculatePopupDirection = () => {
+        // 1. 获取select容器元素
+        const selectEl = document.querySelector(
+            `[name="${props.name}"]`
+        )?.nextElementSibling;
+        console.log("selectEl:" + selectEl, props.name);
+        if (!selectEl) return;
+
+        // 2. 获取位置信息
+        const selectRect = selectEl.getBoundingClientRect();
+        const viewportHeight = window.innerHeight;
+
+        // 3. 简单判断:如果下方剩余空间小于300px就向上弹出
+        const spaceBelow = viewportHeight - selectRect.bottom;
+
+        // 调试信息
+        console.log("空间判断:", {
+          elementBottom: selectRect.bottom,
+          viewportHeight,
+          spaceBelow,
+          willPopUp: spaceBelow < 300,
+          popupDirection: popupDirection.value,
+        });
+
+        // 4. 设置方向
+        popupDirection.value = spaceBelow < 300 ? "top" : "bottom";
+      };
+      //级联菜单点击事件
+      const togglePopup = () => {
+        if (!popupWinVisible.value) {
+          //如果当前下拉菜单是隐藏的,先ajax重新加载下拉菜单项,再显示。
+
+          showPopup();
+        } else {
+          hidePopup();
+        }
+      };
+
+      //显示下拉菜单,在此之前先清除下拉菜单项
+      const showPopup = () => {
+        //清空下拉菜单,并设置第一项的值为空
+        clearAndInit1stOpt();
+        Vue.nextTick(() => {
+          calculatePopupDirection();
+        });
+        let url = props.url;
+        let filterObj = props.param.filter;
+
+        if (filterObj) {
+          for (let k in filterObj) {
+            let v = filterObj[k];
+            url += "&" + k + "=" + v;
+          }
+        }
+
+        if (props.mode === "1") {
+          //如果是有隐藏字段的下拉菜单
+
+          console.log("666url:" + url);
+
+          // alert('url:'+url);
+
+          // 获取级联菜单所有下拉菜单项
+          async function getSelectItems(value) {
+            try {
+              // alert(1);
+              if (props.url && props.param) {
+                // alert(2);
+                /**
+                 *            param={"objectpickerparam":"{\"input\":\"false\",\"cascadingLevel\":\"hksheng,hkshi,hkxian\"," +
+                 *                     "\"name\":\"hksheng\",\"cascadingName\":\"dq\"," +
+                 *                     "\"cascadingInputsName\":\"hkdqm\",\"codebook\":\"sheng\"}",
+                 *                 "objectpickertype":1,//2表示获取要回显的一项,1表示获取所有下拉菜单项
+                 *                 "upperValue":"440000"
+                 *             };
+                 */
+
+                const objectPickerParam = {
+                  input: "false",
+                  cascadingLevel: props.param.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
+                  name: props.name, //本下拉菜单名
+                  cascadingName: props.param.name, //级联菜单名
+                  cascadingInputsName: props.param.combField, //对象属性,即隐藏字段名,如:hkdqm
+                  codebook: props.param.codebook,
+                };
+
+                console.log("mode1 upperValue.value:" + upperValue.value);
+
+                const objectPickerParamStr = JSON.stringify(objectPickerParam);
+
+                const params = new URLSearchParams();
+                params.append("objectpickerparam", objectPickerParamStr);
+                params.append("objectpickertype", "1");
+                if (upperValue.value) {
+                  params.append("upperValue", upperValue.value);
+                }
+                // params.append('objectpickervalue', value); //需回显的值
+
+                axios
+                    .post(url, params, {
+                      headers: {
+                        "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
+                      },
+                    })
+                    .then((response) => {
+                      if ("timeout" == response.data.statusText) {
+                        alert("网络超时!");
+                        return;
+                      }
+
+                      if (response.data.result) {
+                        const keys = Object.keys(response.data.result);
+                        console.log(
+                            "params:" +
+                            params +
+                            "@@response.data:" +
+                            JSON.stringify(response.data)
+                        );
+
+                        if (keys.length > 0) {
+                          for (let k in response.data.result) {
+                            props.opt.push({
+                              label: response.data.result[k],
+                              value: k,
+                            });
+                          }
+
+                          if (!popupWinVisible.value) {
+                            popupWinVisible.value = true; // 确保下拉框打开
+                          }
+
+                          console.log("props.opt11:" + JSON.stringify(props.opt));
+                        }
+                      }
+                    });
+              }
+            } catch (error) {
+              alert(error);
+              // callback(null, error.message); // 失败回调,传递错误
+            }
+          }
+
+          getSelectItems(props.modelValue);
+        } else if (props.mode === "2") {
+          //没隐藏字段的下拉菜单
+
+          let needInitParam = undefined;
+          if (Array.isArray(props.param)) {
+            needInitParam = props.param[props.param.length - 1]; //只初始化数组最后一项
+            console.log(
+                "needInitParam最后一项:" + JSON.stringify(needInitParam)
+            );
+          } else {
+            needInitParam = props.param;
+          }
+
+          // 获取级联菜单所有下拉菜单项
+          async function getSelectItems(value, sendParam) {
+            try {
+              // alert(1);
+              if (props.url && sendParam) {
+                // alert(2);
+                /**
+                 *            param="{\"input\":\"false\",\"cascadingLevel\":\"dwid,sjryid\",
+                 *            \"ryid\":\"111121\",\"name\":\"sjryid\",
+                 *            \"cascadingName\":\"dw_sjry\",\"codebook\":\"sjryByDw\"}"
+                 */
+
+                const objectPickerParam = {
+                  input: "false",
+                  cascadingLevel: sendParam.fieldOrd.join(","), //如:"hksheng,hkshi,hkxian"
+                  name: props.name, //本下拉菜单名
+                  cascadingName: sendParam.name, //级联菜单名
+                  codebook: sendParam.codebook,
+                };
+
+                console.log("mode2 upperValue.value:" + upperValue.value);
+
+                const objectPickerParamStr = JSON.stringify(objectPickerParam);
+
+                const params = new URLSearchParams();
+                params.append("objectpickerparam", objectPickerParamStr);
+                params.append("objectpickertype", "1");
+                if (upperValue.value) {
+                  params.append("upperValue", upperValue.value);
+                }
+                // params.append('objectpickervalue', value); //需回显的值
+
+                axios
+                    .post(url, params, {
+                      headers: {
+                        "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
+                      },
+                    })
+                    .then((response) => {
+                      if ("timeout" == response.data.statusText) {
+                        alert("网络超时!");
+                        return;
+                      }
+
+                      if (response.data.result) {
+                        const keys = Object.keys(response.data.result);
+                        console.log(
+                            "params:" +
+                            params +
+                            "@@response.data:" +
+                            JSON.stringify(response.data)
+                        );
+
+                        if (keys.length > 0) {
+                          for (let k in response.data.result) {
+                            props.opt.push({
+                              label: response.data.result[k],
+                              value: k,
+                            });
+                          }
+
+                          if (!popupWinVisible.value) {
+                            popupWinVisible.value = true; // 确保下拉框打开
+                          }
+
+                          console.log("props.opt11:" + JSON.stringify(props.opt));
+                        }
+                      }
+                    });
+              }
+            } catch (error) {
+              alert(error);
+              // callback(null, error.message); // 失败回调,传递错误
+            }
+          }
+
+          getSelectItems(props.modelValue, needInitParam);
+
+          // popupWinVisible.value = !popupWinVisible.value;
+        }
+      };
+
+      const hidePopup = () => {
+        popupWinVisible.value = false;
+      };
+
+      // 合并所有的 onMounted 逻辑
+      Vue.onMounted(() => {
+        window.addEventListener("resize", calculatePopupDirection);
+
+        // 1. 监听展开下一级事件
+        window.addEventListener("open-next-cascader", (event) => {
+          const { name, level } = event.detail;
+          if (name === props.name && level === props.level) {
+            popupWinVisible.value = true;
+          }
+        });
+
+        // 2. 监听级联事件
+        window.addEventListener("cascader-change", (event) => {
+          const { name, level, value } = event.detail;
+          if (name === props.name && level < props.level) {
+            selectItem.value = { label: "", value: "" };
+            emit("update:modelValue", "");
+            if (ssHidden) {
+              ssHidden.updateValue(value);
+            }
+          }
+        });
+      });
+      Vue.onUnmounted(() => {
+        window.removeEventListener("resize", calculatePopupDirection);
+      });
+      // 监听值变化,处理回显 (mode 2)
+      Vue.watch(
+          () => props.modelValue,
+          (newVal) => {
+            if (props.mode === "2" && newVal) {
+              // 使用 watchEffect 替代嵌套的 watch
+              Vue.watchEffect(() => {
+                if (props.opt.length > 0) {
+                  const matchedOption = props.opt.find(
+                      (opt) => opt.value === newVal
+                  );
+                  if (matchedOption) {
+                    selectItem.value = matchedOption;
+                  }
+                }
+              });
+            } else {
+              // 原有的值变化处理
+              const item = props.opt.find((it) => it.value === newVal);
+              if (item) {
+                selectItem.value = item;
+              } else {
+                selectItem.value = { label: "", value: "" };
+              }
+            }
+          },
+          { immediate: true }
+      );
+
+      // 监听选项变化,当数据加载完成时进行匹配
+      Vue.watch(
+          () => props.opt,
+          (newOptions) => {
+            if (newOptions.length > 0) {
+              const matchedOption = newOptions.find(
+                  (opt) => opt.value === selectItem.value.value
+              );
+              if (matchedOption) {
+                selectItem.value = matchedOption;
+                emit("update:modelValue", matchedOption.value);
+                emit("change", matchedOption.value);
+              }
+            }
+          }
+      );
+
+      return {
+        selectItem,
+        popupWinVisible,
+        popupDirection,
+        togglePopup,
+        hidePopup,
+        doSelectItem,
+      };
+    },
+
+    template: `
+      <div class="input ss-ccp-container" style="position: relative" :style="{width: width}">
+        <input type="hidden" :name="name" :value="modelValue">
+        <div class="select-container" @mouseleave="hidePopup">
+          <div class="input" @click="togglePopup">
+            <input 
+              type="hidden"
+              :name="name"
+              :value="selectItem.value"
+            />
+            <input
+              :placeholder="placeholder"
+              :value="selectItem.label"
+              disabled
+              style="pointer-events: none;"
+            />
+            <div class="suffix">
+              <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
+            </div>
+          </div>
+
+          <div v-show="popupWinVisible" class="popup-win " :class="popupDirection">
+            <div v-if="opt && opt.length > 0" class="popup-content">
+              <div class="content-area">
+                <div 
+                  v-for="(item, index) in opt" 
+                  :key="index" 
+                  @click="doSelectItem(item)" 
+                  :class="{ active: item.value === selectItem.value }"
+                >
+                  <span class="check-icon">
+                    <ss-form-icon class="form-icon-select-checked" />
+                  </span>
+                  <span>{{ item.label }}</span>
+                </div>
+              </div>
+            </div>
+            <div v-else class="popup-content">
+              <div class="content-area">
+                <div class="content-area">
+                  <span>无选项</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    `,
+  };
+  // ss-date-picker 日期时间选择器组件
+  const SsDatePicker = {
+    name: "SsDatePicker",
+    props: {
+      modelValue: {
+        type: [String, Number, Date],
+        default: "",
+      },
+      name: {
+        type: String,
+        required: true,
+      },
+      type: {
+        type: String,
+        default: "date",
+        validator: (value) => ["date", "datetime", "time"].includes(value),
+      },
+      fmt: {
+        type: String,
+        default: null,
+      },
+      placeholder: {
+        type: String,
+        default: "",
+      },
+      width: {
+        type: String,
+        default: "100%",
+      },
+    },
+
+    emits: ["update:modelValue"],
+
+    setup(props, { emit }) {
+      const errMsg = ref("");
+      const validate = () => {
+        if (window.ssVm) {
+          const result = window.ssVm.validateField(props.name);
+          console.log("validate", window.ssVm.validateField(props.name));
+          errMsg.value = result.valid ? "" : result.message;
+        }
+      };
+      // 根据type确定默认格式
+      const defaultFormat = computed(() => {
+        switch (props.type) {
+          case "datetime":
+            return "YYYY-MM-DD HH:mm:ss";
+          case "date":
+            return "YYYY-MM-DD";
+          case "time":
+            return "HH:mm:ss";
+        }
+      });
+      const convertJavaFormatToElement = (javaFormat) => {
+        if (!javaFormat) return null;
+        return javaFormat
+            .replace("yyyy", "YYYY")
+            .replace("MM", "MM")
+            .replace("dd", "DD")
+            .replace("HH", "HH")
+            .replace("mm", "mm")
+            .replace("ss", "ss");
+      };
+
+      const finalFormat = computed(() => {
+        if (props.fmt) {
+          return convertJavaFormatToElement(props.fmt);
+        }
+        return defaultFormat.value;
+      });
+
+      // 使用 resolveComponent 获取组件
+      const ElDatePicker = resolveComponent("ElDatePicker");
+      const ElTimePicker = resolveComponent("ElTimePicker");
+      const SsFormIcon = resolveComponent("SsFormIcon");
+      const ElIcon = resolveComponent("ElIcon");
+
+      const handleValueUpdate = (val) => {
+        emit("update:modelValue", val);
+        emit("change", val); // 同时触发 change 事件
+        setTimeout(() => {
+          validate();
+        }, 50);
+      };
+      const dateType = computed(() => {
+        const fmt = props.fmt || "";
+        if (fmt.includes("HH:mm:ss")) {
+          return "datetime";
+        } else if (fmt.includes("HH:mm")) {
+          return "datetime";
+        } else if (fmt.includes("mm:ss")) {
+          return "time";
+        }
+        return "date";
+      });
+
+      let useTimePicker = true;
+      //"yyyy-MM-dd HH:mm:ss";  "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式
+      if (props.fmt) {
+        //有fmt属性,则以fmt属性优先判断类型
+        if (/[dMy]/.test(props.fmt)) {
+          //如果有传入日期格式,且含年月日
+          useTimePicker = false;
+        } else {
+          useTimePicker = true;
+        }
+      } else if (props.type !== "time") {
+        useTimePicker = false;
+      }
+
+      return () =>
+          h("div", { class: "ss-date-picker", style: { width: props.width } }, [
+            h("input", {
+              type: "hidden",
+              name: props.name,
+              value: props.modelValue,
+            }),
+            // 选择组件
+            h(useTimePicker ? ElTimePicker : ElDatePicker, {
+              modelValue: props.modelValue,
+              "onUpdate:modelValue": handleValueUpdate,
+              type: dateType.value,
+              format: finalFormat.value,
+              "value-format": finalFormat.value,
+              clearable: true,
+              placeholder: props.placeholder,
+              class: "custom-date-picker", // 用于自定义样式
+              "time-arrow-control": props.type === "datetime", // 修改这里
+              size: "large", // 添加这一行,改为 large 尺寸
+              style: { width: "100%" },
+              "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }),
+            }),
+          ]);
+    },
+  };
+  // ss-icon 图标
+  const SsIcon = {
+    name: "SsIcon",
+    props: {
+      name: { type: String, required: true },
+      size: { type: [Number, String], default: 16 },
+      unit: { type: String, default: "px" },
+      color: String,
+      type: {
+        type: String,
+        default: ssIcon.name,
+        validator: function (value) {
+          return [ssIcon, commonIcon].some((icon) => icon.name === value);
+        },
+      },
+    },
+    emits: ["update:modelValue", "input", "blur", "change"],
+    setup(props, { emit }) {
+      const useIconType = computed(() => {
+        return [ssIcon, commonIcon].find(
+            (iconConfig) => iconConfig.name === props.type
+        );
+      });
+      const iconName = computed(() => {
+        const iconConfig = useIconType.value; // 注意:使用 .value 来访问响应式引用的值
+        if (!iconConfig) {
+          console.error(`Icon type "${props.type}" not found.`);
+          return "";
+        }
+        const iconType = iconConfig.types[props.name];
+        if (!iconType) {
+          console.error(
+              `Icon name "${props.name}" not found in type "${props.type}".`
+          );
+          return "";
+        }
+        return `${iconConfig.prefix}${iconType}`;
+      });
+
+      // 类似地,你可以计算 fontFamily 和 style
+      const fontFamily = computed(() => {
+        return useIconType.value ? useIconType.value.family : "";
+      });
+      // console.log(iconName.value,fontFamily.value)
+      const style = computed(() => {
+        const sizeStyle = isNum(props.size)
+            ? `${props.size}${props.unit}`
+            : props.size;
+        const styleObj = {
+          fontSize: sizeStyle,
+          color: props.color || "",
+        };
+        return toStyleStr(styleObj);
+      });
+
+      // 使用渲染函数定义模板逻辑
+      return () =>
+          h("i", {
+            class: ["icon-container", iconName.value, fontFamily.value],
+            style: style.value,
+          });
+    },
+  };
+  // 通用icon
+  const SsCommonIcon = {
+    name: "SsCommonIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("i", {
+            class: props.class + " common-icon",
+          });
+    },
+  };
+  // 登录页icon
+  const SsLoginIcon = {
+    name: "SsLoginIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " login-icon",
+          });
+    },
+  };
+  // 弹窗icon
+  const SsDialogIcon = {
+    name: "SsDialogIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("i", {
+            class: props.class + " dialog-icon",
+          });
+    },
+  };
+  // 全局左侧导航图标组件
+  const SsNavIcon = {
+    name: "SsNavIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " nav-icon",
+          });
+    },
+  };
+  // 顶部工具栏图标组件
+  const SsHeaderIcon = {
+    name: "SsHeaderIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " header-icon",
+          });
+    },
+  };
+  // 全局菜单图标组件
+  const SsGolbalMenuIcon = {
+    name: "SsGolbalMenuIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " global-menu-icon",
+          });
+    },
+  };
+  // 全局查询列表卡片图标
+  const SsCartListIcon = {
+    name: "SsCartListIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " cart-list-icon",
+          });
+    },
+  };
+  // 全局底部工具栏图标组件
+  const SsQuickIcon = {
+    name: "SsQuickIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " quick-icon",
+          });
+    },
+  };
+  // 表单组件icon
+  const SsFormIcon = {
+    name: "SsFormIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " form-icon",
+          });
+    },
+  };
+  // 弹窗底部按钮icon
+  const SsBottomDivIcon = {
+    name: "SsBottomDivIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("div", {
+            class: props.class + " bottom-div-icon",
+          });
+    },
+  };
+  // editor组件icon
+  const SsEditorIcon = {
+    name: "SsEditorIcon",
+    props: {
+      class: {
+        type: String,
+        required: true,
+      },
+    },
+    setup(props) {
+      return () =>
+          h("i", {
+            class: props.class + " editor-icon",
+          });
+    },
+  };
+  // ss-validate校验器
+  const SsValidate = {
+    name: "SsValidate",
+    props: {
+      errMsg: { type: String },
+      textAlign: { type: String, default: "left" },
+      style: { type: Object, default: () => ({}) },
+    },
+    template: `<div class="validate-vline"></div>
+    <div class="validate-tip" :style="style">
+      <div class="tip" :style="{ textAlign: textAlign }">{{ errMsg }}</div>
+      <div class="tip-more" :style="{ textAlign: textAlign }">{{ errMsg }}</div>
+    </div>`,
+  };
+  // ss-onoff-array 多选按钮 数组形式
+  const SsonoffArray = {
+    name: "SsonoffArray",
+    props: {
+      name: {
+        type: String,
+        required: true,
+      },
+      opt: {
+        type: Array,
+        default: () => [],
+      },
+      defaultValue: [String, Number, Array],
+      modelValue: [String, Number, Array],
+      multiple: {
+        // 新增多选模式属性
+        type: Boolean,
+        default: false,
+      },
+    },
+    emits: ["update:modelValue"], // 允许更新 v-model 绑定的值
+    setup(props, { emit }) {
+      console.log("多选按钮", props.opt);
+      // 使用数组来存储选中值
+      const checkedValue = ref(
+          props.multiple
+              ? Array.isArray(props.defaultValue)
+                  ? props.defaultValue
+                  : []
+              : props.defaultValue
+      );
+      const errMsg = ref(props.errTip);
+
+      // 生成icon名字
+      const genIconName = (itemValue) => {
+        if (props.multiple) {
+          return checkedValue.value.includes(itemValue)
+              ? "form-icon-onoff-checked"
+              : "form-icon-onoff-unchecked";
+        }
+        return checkedValue.value === itemValue
+            ? "form-icon-onoff-checked"
+            : "form-icon-onoff-unchecked";
+      };
+      // 选中项
+      const selectItem = (value) => {
+        if (props.multiple) {
+          // 多选模式
+          const index = checkedValue.value.indexOf(value);
+          if (index === -1) {
+            checkedValue.value = [...checkedValue.value, value];
+          } else {
+            checkedValue.value = checkedValue.value.filter((v) => v !== value);
+          }
+        } else {
+          // 单选模式
+          checkedValue.value = value;
+        }
+
+        emit("update:modelValue", checkedValue.value);
+        nextTick(() => {
+          // 触发验证
+          if (window.ssVm) {
+            window.ssVm.validateField(props.name);
+          }
+        });
+      };
+
+      return { checkedValue, genIconName, selectItem };
+    },
+    // 使用渲染函数定义模板逻辑
+    render() {
+      const SsFormIcon = resolveComponent("ss-form-icon");
+      return h("div", { class: "radio-container" }, [
+        // 根据情况创建 input
+        this.multiple
+            ? this.checkedValue.length
+                ? // 多选且有选中值:为选中项创建 input
+                this.checkedValue.map((value) =>
+                    h("input", {
+                      type: "checkbox",
+                      name: this.name,
+                      value: value,
+                      checked: true,
+                      style: { display: "none" },
+                    })
+                )
+                : // 多选但没有选中值:创建一个空值 input
+                h("input", {
+                  type: "hidden",
+                  name: this.name,
+                  value: "",
+                })
+            : // 单选模式:创建一个 input
+            h("input", {
+              type: "hidden",
+              name: this.name,
+              value: this.checkedValue || "",
+            }),
+
+        this.opt.map((item, i) =>
+            h(
+                "div",
+                {
+                  key: i,
+                  class: {
+                    checked: this.multiple
+                        ? this.checkedValue.includes(item.value)
+                        : this.checkedValue === item.value,
+                  },
+                  style: { width: item.width },
+                  onClick: () => this.selectItem(item.value),
+                },
+                [
+                  h("span", null, item.label),
+                  h("div", { class: "mark" }, [
+                    h(SsFormIcon, {
+                      class: this.genIconName(item.value),
+                    }),
+                  ]),
+                ]
+            )
+        ),
+      ]);
+    },
+  };
+
+  // ss-onoff 一个按钮
+  const Ssonoff = {
+    name: "Ssonoff",
+    props: {
+      name: {
+        type: String,
+        required: true,
+      },
+      label: {
+        type: String,
+        required: true,
+      },
+      value: {
+        type: [String, Number],
+        required: true,
+      },
+      width: {
+        type: String,
+        default: "",
+      },
+      modelValue: [String, Number, Array],
+      multiple: {
+        type: Boolean,
+        default: false,
+      },
+      null: {
+        type: Boolean,
+        default: true,
+      },
+    },
+    emits: ["update:modelValue"],
+
+    setup(props, { emit }) {
+      const parseModelValue = (val) => {
+        if (!val) return [];
+        // 如果以逗号开头,去掉开头的逗号
+        const cleanValue = val.toString().replace(/^,+/, "");
+        if (cleanValue.includes("|")) {
+          return cleanValue.split("|");
+        }
+        if (cleanValue.includes(",")) {
+          return cleanValue.split(",");
+        }
+        return [cleanValue];
+      };
+      // 判断当前按钮是否选中
+      const isChecked = computed(() => {
+        if (props.multiple) {
+          const currentValue = parseModelValue(props.modelValue);
+          return currentValue.includes(props.value.toString());
+
+          // return Array.isArray(props.modelValue) && props.modelValue.includes(props.value);
+        }
+        return (props.modelValue+'') === (props.value+'');//强转为字符串类型再比较(改之前是数字类型和字符串类型作比较,永远为false) Ben 20251206
+      });
+
+      // 切换选中状态
+      const toggleSelect = () => {
+        if (props.multiple) {
+          const currentValue = parseModelValue(props.modelValue);
+          const index = currentValue.indexOf(props.value.toString());
+          let newValue;
+
+          if (index === -1) {
+            // 选中当前项
+            newValue = [...currentValue, props.value];
+          } else {
+            // 取消选中当前项
+            const filteredValue = currentValue.filter((v) => v !== props.value.toString());
+
+            // 如果不允许为空且取消后为空,则阻止取消操作
+            if (!props.null && filteredValue.length === 0) {
+              return; // 阻止取消最后一项
+            }
+
+            newValue = filteredValue;
+          }
+          emit("update:modelValue", newValue.join(","));
+        } else {
+          // 单选模式
+          const currentValue = parseModelValue(props.modelValue);
+          const isCurrentlySelected = currentValue.includes(props.value.toString());
+
+          if (!isCurrentlySelected) {
+            // 选中当前项
+            emit("update:modelValue", props.value);
+          } else {
+            // 取消选中当前项
+            // 如果不允许为空且当前只有这一项被选中,则阻止取消操作
+            if (!props.null && currentValue.length === 1) {
+              return; // 阻止取消唯一选中项
+            }
+            emit("update:modelValue", "");
+          }
+        }
+
+        nextTick(() => {
+          // 触发验证
+          if (window.ssVm) {
+            window.ssVm.validateField(props.name);
+          }
+        });
+      };
+
+      return { isChecked, toggleSelect };
+    },
+
+    render() {
+      const SsFormIcon = resolveComponent("ss-form-icon");
+      return h("div", { class: "radio-container2" }, [
+        // 隐藏的表单元素
+        this.multiple
+            ? h("input", {
+              type: "hidden",
+              name: `${this.name}`, // 多选模式下使用数组形式的 name
+              value: this.isChecked ? this.value : "",
+            })
+            : this.isChecked &&
+            h("input", {
+              // 只有当前按钮被选中时才创建 input
+              type: "hidden",
+              name: this.name,
+              value: this.value,
+            }),
+
+        // 按钮显示
+        h(
+            "div",
+            {
+              class: { checked: this.isChecked },
+              style: { width: this.width },
+              onClick: this.toggleSelect,
+            },
+            [
+              h("span", null, this.label),
+              h("div", { class: "mark" }, [
+                h(SsFormIcon, {
+                  class: this.isChecked
+                      ? "form-icon-onoff-checked"
+                      : "form-icon-onoff-unchecked",
+                }),
+              ]),
+            ]
+        ),
+      ]);
+    },
+  };
+
+  // ss-textarea
+  const SsTextarea = {
+    name: "SsTextarea",
+    props: {
+      name: {
+        type: String,
+        required: true,
+      },
+      placeholder: {
+        type: String,
+        default: "请输入",
+      },
+      defaultValue: [String, Number],
+      modelValue: [String, Number],
+    },
+    emits: ["update:modelValue"],
+    setup(props, { emit }) {
+      const inputValue = ref(props.modelValue || props.defaultValue || "");
+
+      // 监听 modelValue 变化
+      watch(
+          () => props.modelValue,
+          (newVal) => {
+            inputValue.value = newVal;
+          }
+      );
+
+      // 输入事件处理
+      const onInput = (event) => {
+        const newValue = event.target.value;
+        inputValue.value = newValue;
+        emit("update:modelValue", newValue);
+        // 触发验证
+        if (window.ssVm) {
+          window.ssVm.validateField(props.name);
+        }
+      };
+
+      // 失焦时验证
+      const onBlur = () => {
+        if (window.ssVm) {
+          window.ssVm.validateField(props.name);
+        }
+      };
+
+      return { inputValue, onInput, onBlur };
+    },
+    render() {
+      return h("div", { class: "textarea-container" }, [
+        h("div", { class: "textarea" }, [
+          // 添加隐藏的 input 用于验证
+          h("input", {
+            type: "hidden",
+            name: this.name,
+            value: this.inputValue || "",
+          }),
+          h("textarea", {
+            placeholder: this.placeholder,
+            value: this.inputValue,
+            onInput: this.onInput,
+            onBlur: this.onBlur,
+          }),
+        ]),
+      ]);
+    },
+  };
+  // ss-editor 富文本编辑器 基于Jodit
+  const SsEditor = {
+    name: "SsEditor",
+    props: {
+      modelValue: {
+        type: String,
+        default: "",
+      },
+      name: {
+        type: String,
+        default: "",
+      },
+      url: {
+        type: String,
+        default: "",
+      },
+      height: {
+        type: [Number, String],
+        default: 400,
+      },
+      placeholder: {
+        type: String,
+        default: "请输入内容",
+      },
+      readonly: {
+        type: Boolean,
+        default: false,
+      },
+      uploadUrl: {
+        type: String,
+        default: "/ulByHttp",//原值为“upload” Ben(20251205)
+      },
+      param: {
+        type: Object,
+        default: () => ({}),
+      },
+    },
+
+    emits: ["update:modelValue", "ready", "change"],
+
+    setup(props, { emit }) {
+      const SsEditorIcon = resolveComponent("SsEditorIcon");
+      const editorRef = ref(null);
+      let editorContent = '';//保存富文本编辑器里面的富文本内容
+      const uniqueId = "editor-" + Date.now();
+      const errMsg = Vue.ref("");
+      let fjid = ref(props.param.button.val);
+      let fjName = props.param.button.desc;
+      let mode = props.param.mode;
+      const validate = () => {
+        if (window.ssVm) {
+          const result = window.ssVm.validateField(props.name);
+          console.log("validate", window.ssVm.validateField(props.name));
+          errMsg.value = result.valid ? "" : result.message;
+        }
+      };
+      onMounted(() => {
+        validate();
+        const editor = Jodit.make(`#${uniqueId}`, {
+          height: props.height,
+          placeholder: props.placeholder,
+          readonly: props.readonly,
+          language: "zh_cn",
+          i18n: {
+            zh_cn: {
+              Link: "链接",
+              URL: "链接",
+              "No follow": "无跟踪",
+              "Class name": "类名",
+              Image: "图片",
+              File: "文件",
+              "Line height": "行高",
+              Alternative: "描述",
+              "Alternative text": "描述",
+              "Lower Alpha": "小写字母",
+              "Upper Alpha": "大写字母",
+              "Upper Roman": "大写罗马数字",
+              "Lower Roman": "小写罗马数字",
+              "Lower Greek": "小写希腊字母",
+              "Lower Letter": "小写字母",
+              "Upper Letter": "大写字母",
+            },
+          },
+          showXPathInStatusbar: false,
+          showCharsCounter: false,
+          showWordsCounter: false,
+          allowResizeY: false,
+          toolbarSticky: false,
+
+          statusbar: false,
+          uploader: {
+            url: props.uploadUrl,
+            format: "json",
+            method: "POST",
+            filesVariableName: function (i) {
+              return "imgs[" + i + "]";
+            },
+            headers: {
+              Accept: "application/json",
+            },
+            prepareData: function (formData) {
+              // 这里可以在发送前处理表单数据
+              return formData;
+            },
+            isSuccess: function (resp) {
+              console.log("isSuccess resp:", resp);
+              return resp.code === 0;
+            },
+            getMessage: function (resp) {
+              console.log("getMessage resp:", resp);
+              return resp.msg || "上传失败";
+            },
+            process: function (resp) {
+              console.log("process resp:", resp);
+              return resp.data.url;
+            },
+            error: function (e) {
+              console.error("上传失败:", e.message);
+            },
+            defaultHandlerSuccess: function (resp) {
+              console.log("上传成功:", resp);
+            },
+            defaultHandlerError: function (err) {
+              console.error("上传错误:", err);
+            },
+            contentType: function (requestData) {
+              // 可以根据需要修改 Content-Type
+              return false; // 让浏览器自动设置
+            },
+          },
+
+          // 自定义字体列表
+          controls: {
+            font: {
+              list: {
+                Arial: "Arial",
+                SimSun: "宋体",
+                SimHei: "黑体",
+                "Microsoft YaHei": "微软雅黑",
+                KaiTi: "楷体",
+                FangSong: "仿宋",
+                "Times New Roman": "Times New Roman",
+                "Courier New": "Courier New",
+              },
+            },
+            customLinkButton: {
+              name: "link",
+              tooltip: "附件",
+              exec: function (editor) {
+                // 按钮点击时的处理函数
+                console.log("附件点击了");
+                console.log("param", props.param);
+                console.log("cmsAddUrl",props.param.button.cmsAddUrl);
+
+                if (fjid.value == null || fjid.value == "") {
+                  $.ajax({
+                    type: "post",
+                    url: props.param.button.cmsAddUrl,
+                    async: false,
+                    data: {
+                      name: "fjid",
+                      ssNrObjName: "sh",
+                      ssNrObjId: "",
+                    },
+                    success: function (_fjid) {
+                      console.log("cmsAddUrl success", _fjid);
+                      fjid.value = _fjid;
+                    },
+                  });
+                }
+                var str =
+                    "&nrid=T-" +
+                    fjid.value +
+                    "&objectId=" +
+                    fjid.value +
+                    "&objectName=" +
+                    fjName +
+                    "&callback=" +
+                    (window["fjidCallbackName"] || "");
+                console.log("str", str);
+                SS.openDialog({
+                  src: props.param.button.cmsUpdUrl + str,
+                  headerTitle: "编辑",
+                  width: 900,
+                  high: 664,
+                  zIndex:51
+                });
+                // ss.display.showComponent({
+                //   show: ["wdDialog"],
+                //   url: props.param.button.cmsUpdUrl + str,
+                //   title: "编辑",
+                //   width: 900,
+                //   high: 664,
+                // });
+              },
+            },
+          },
+          toolbarAdaptive: true,
+          buttons: [
+            "fullsize",
+            "bold",
+            "italic",
+            "underline",
+            "strikethrough",
+            "eraser",
+            "|",
+            "font",
+            "fontsize",
+            "brush",
+            "paragraph",
+            "|",
+            "left",
+            "center",
+            "right",
+            "justify",
+            "|",
+            "ul",
+            "ol",
+            "indent",
+            "outdent",
+            "|",
+            "image",
+            "table",
+            "customLinkButton",
+            "print",
+            "|",
+            "undo",
+            "redo",
+            "find",
+          ],
+          // 中等宽度时显示的按钮
+          buttonsMD: [
+            "fullsize",
+            "bold",
+            "italic",
+            "underline",
+            "strikethrough",
+            "eraser",
+            "|",
+            "font",
+            "fontsize",
+            "brush",
+            "paragraph",
+            "|",
+            "font",
+            "fontsize",
+            "|",
+            "left",
+            "center",
+            "right",
+            "justify",
+            "|",
+            "image",
+            "customLinkButton",
+            "|",
+            "dots", // 其余按钮会自动进入 dots 菜单
+          ],
+
+          // 小屏幕时显示的按钮
+          buttonsSM: ["fullsize", "bold", "italic", "|", "image", "|", "dots"],
+
+          // 超小屏幕时显示的按钮
+          buttonsXS: ["fullsize", "bold", "|", "dots"],
+          // 设置响应式断点
+          sizeLG: 1024, // 大屏幕
+          sizeMD: 768, // 中等屏幕
+          sizeSM: 576, // 小屏幕
+          // 自定义图标
+          getIcon: function (name, clearName) {
+            // 定义图标映射
+            const iconMap = {
+              bold: "editor-icon-bold",
+              italic: "editor-icon-italic",
+              underline: "editor-icon-underline",
+              strikethrough: "editor-icon-strikethrough",
+              eraser: "editor-icon-eraser",
+              copyformat: "editor-icon-copyformat",
+              font: "editor-icon-font",
+              fontsize: "editor-icon-fontsize",
+              brush: "editor-icon-brush",
+              paragraph: "editor-icon-paragraph",
+              left: "editor-icon-align-left",
+              center: "editor-icon-align-center",
+              right: "editor-icon-align-right",
+              justify: "editor-icon-align-justify",
+              ul: "editor-icon-ul",
+              ol: "editor-icon-ol",
+              indent: "editor-icon-indent",
+              outdent: "editor-icon-outdent",
+              image: "editor-icon-image",
+              file: "editor-icon-file",
+              video: "editor-icon-video",
+              table: "editor-icon-table",
+              link: "editor-icon-link",
+              source: "editor-icon-source",
+              eye: "editor-icon-preview",
+              fullsize: "editor-icon-fullsize",
+              shrink: "editor-icon-fullsize-exit", // 添加退出全屏图标
+
+              print: "editor-icon-print",
+              undo: "editor-icon-undo",
+              redo: "editor-icon-redo",
+              search: "editor-icon-find",
+              selectall: "editor-icon-selectall",
+            };
+            // 获取对应的图标类名
+            const iconClass = iconMap[clearName] || iconMap[name];
+
+            if (iconClass) {
+              // 返回带有我们自定义图标类的 span 元素
+              return `<span class="editor-icon ${iconClass}"></span>`;
+            }
+
+            return null;
+          },
+        });
+
+
+
+        // 设置初始值
+        editor.value = editorContent;
+        // editor.value = props.modelValue;
+
+        // 监听变化
+        editor.events.on("change", () => {
+          // emit("update:modelValue", editor.value);
+          editorContent = editor.value;
+          // alert('editorContent:'+editorContent);
+          let contentElements = document.getElementsByName(props.name.replace(/wj$/, "") + "Edit");
+          if(contentElements.length>0){
+            contentElements[0].value = editorContent;
+          }
+          emit("change", editor.value);
+          setTimeout(() => {
+            validate();
+          }, 50);
+        });
+
+        // 保存编辑器实例
+        editorRef.value = editor;
+
+        emit("ready", editor);
+
+        //回显编辑器富文本文件
+        if (props.url ) {
+          const params = new URLSearchParams();
+
+        if(mode)
+          params.append("mode", mode);
+
+          if(props.modelValue)
+            params.append("path", props.modelValue);
+
+          // alert('props.url:'+props.url+',props.modelValue:'+props.modelValue);
+
+          axios
+              .post(props.url, params, {
+                headers: {
+                  "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
+                },
+              })
+              .then((response) => {
+                // alert(JSON.stringify(response.data));
+
+                if ("timeout" == response.data.statusText) {
+                  alert("网络超时!");
+                  return;
+                }
+
+                let content = response.data.content;
+                if (content) {
+                  // editor.value = content;
+                  editorRef.value.value = content;
+                  editorContent = content;
+                  // alert('editor.value:'+editor.value)
+                }
+
+                let filePath = response.data.path;
+                // alert('response.data:'+JSON.stringify(response.data));
+                if(filePath){
+                  props.modelValue = filePath;
+                  emit("update:modelValue", filePath);
+                }
+              });
+        }
+      });
+
+      // 监听值变化
+      watch(
+          // () => props.modelValue,
+          () => editorContent,
+          (newValue) => {
+            if (editorRef.value && newValue !== editorRef.value.value) {
+              editorRef.value.value = newValue || "";
+            }
+          }
+      );
+
+      // 监听只读状态变化
+      watch(
+          () => props.readonly,
+          (newValue) => {
+            if (editorRef.value) {
+              editorRef.value.setReadOnly(newValue);
+            }
+          }
+      );
+
+      // 组件销毁时清理
+      onBeforeUnmount(() => {
+        if (editorRef.value) {
+          editorRef.value.destruct();
+        }
+      });
+
+      return () =>
+          h("div", { class: "ss-editor-container" }, [
+            fjid.value &&
+            h("input", {
+              type: "hidden",
+              name: "fjid",
+              value: fjid.value,
+            }),
+            h("input", {
+              type: "hidden",
+              name: props.name.replace(/wj$/, "") + "Edit",
+              value: editorContent
+              // value: props.modelValue,
+            }),
+            h("input", {
+              type: "hidden",
+              name: props.name.replace(/wj$/, "") + "wj",
+              value: props.modelValue
+              // value: props.url
+            }),
+            h("input", { type: "hidden", name: "ueditorpath", value: "mswj" }),
+            h("textarea", { id: uniqueId }),
+          ]);
+    },
+  };
+  // 弹窗右边图标
+  const SsFullStyleHeader = {
+    name: "SsFullStyleHeader",
+    props: {
+      title: {
+        type: String,
+        default: "标题",
+      },
+    },
+    emits: ["close"],
+    setup(props, { emit }) {
+      // console.log(props.title)
+      const onClose = () => {
+        emit("close");
+      };
+      const SsIcon = resolveComponent("ss-icon");
+      return () =>
+          h("div", { class: "header-container" }, [
+            h("div", { class: "title" }, props.title),
+            h("div", { class: "handle-bar" }, [
+              h("div", { class: "left-bar" }, [
+                h(SsDialogIcon, { class: "dialog-icon-download" }),
+                h(SsDialogIcon, { class: "dialog-icon-print" }),
+                h(SsDialogIcon, { class: "dialog-icon-setting" }),
+                h(SsDialogIcon, { class: "dialog-icon-collect" }),
+                h(SsDialogIcon, { class: "dialog-icon-help" }),
+                h(SsDialogIcon, { class: "dialog-icon-full-screen" }),
+                h(SsDialogIcon, { class: "dialog-icon-lock" }),
+              ]),
+              h("div", { class: "close-bar", onClick: onClose }, [
+                h(SsDialogIcon, { class: "dialog-icon-close" }),
+              ]),
+            ]),
+          ]);
+    },
+  };
+  // ss-dialog弹窗
+  const SsDialog = {
+    name: "SsDialog",
+    props: {
+      src: {
+        type: String,
+      },
+      headerTitle: {
+        type: String,
+        // required: true,
+        default: "弹窗",
+      },
+      width: {
+        type: String,
+        default: "1400",
+      },
+      height: {
+        type: String,
+        default: "600",
+      },
+      params: {
+        type: Object,
+        default: () => ({}),
+      },
+      zIndex: {
+        type: Number,
+        default: 1000,
+      },
+    },
+    emits: ["close"],
+    setup(props, { slots, emit }) {
+      // 关闭窗口方法
+      const onClose = () => {
+        emit("close");
+      };
+      const showHeader = ref(true);
+      const headerVisible = ref(false);
+      const popupHieght = ref(props.height);
+      // 状态:存储位置信息
+      const position = reactive({
+        // 页面居中
+        x: (window.innerWidth - props.width) / 2,
+        y: (window.innerHeight - popupHieght.value) / 2,
+        isDragging: false,
+        offsetX: 0,
+        offsetY: 0,
+      });
+      // 鼠标按下时设置起始坐标并开始拖拽
+      const startDrag = (event) => {
+        position.isDragging = true;
+        position.offsetX = event.clientX - position.x;
+        position.offsetY = event.clientY - position.y;
+      };
+      // 鼠标移动时更新位置
+      const onDrag = (event) => {
+        if (position.isDragging) {
+          position.x = event.clientX - position.offsetX;
+          position.y = event.clientY - position.offsetY;
+        }
+      };
+      // 鼠标放开时结束拖拽
+      const endDrag = () => {
+        position.isDragging = false;
+      };
+      // 监听来自 iframe 的消息
+      const handleMessage = (event) => {
+        //  顶天立地
+        if (event.data && typeof event.data.hasScrollBar !== "undefined") {
+          if (event.data.hasScrollBar) {
+            //   console.log(event);
+            position.y = 10;
+            showHeader.value = false;
+            headerVisible.value = true;
+            popupHieght.value = window.innerHeight - 20;
+            //   console.log(popupHieght.value);
+            document.querySelector(".body").style.height = "100%";
+            document.querySelector(".body").style.paddingTop = "30px";
+            document.querySelector(".header-container ").style.position =
+                "absolute";
+            document.querySelector(".header-container ").style.zIndex = "10";
+          }
+        }
+      };
+      // 鼠标移入关闭按钮区域时显示头部
+      const onMouseEnterCloseButton = () => {
+        headerVisible.value = false;
+      };
+      // 鼠标移出关闭按钮区域时隐藏头部
+      const onMouseLeaveCloseButton = () => {
+        headerVisible.value = true;
+      };
+      // 在组件挂载时添加全局事件监听器
+      Vue.onMounted(() => {
+        // 如果传过来的高度大于窗口高度,则设置为窗口高度减去20 否则保持传过来的高度
+        popupHieght.value =
+            popupHieght.value > window.innerHeight
+                ? window.innerHeight - 20
+                : popupHieght.value;
+        const container = document.querySelector(".header-container");
+        if (container) {
+          container.addEventListener("mousedown", startDrag);
+        }
+        document.addEventListener("mousemove", onDrag);
+        document.addEventListener("mouseup", endDrag);
+        window.addEventListener("message", handleMessage);
+      });
+
+      // 在组件卸载时移除全局事件监听器
+      Vue.onUnmounted(() => {
+        document.removeEventListener("mousemove", onDrag);
+        document.removeEventListener("mouseup", endDrag);
+        window.removeEventListener("message", handleMessage);
+      });
+      const SsMark = resolveComponent("ss-mark");
+      const SsFullStyleHeader = resolveComponent("ss-full-style-header");
+      // render函数定义组件结构
+      return () =>
+          h(
+              Teleport,
+              { to: "body" }, // 使用 Teleport 将弹窗内容挂载到 body
+              h(SsMark, { }, [
+                h(
+                    "div",
+                    {
+                      class: "popup-container",
+                      style: {
+                        position: "absolute",
+                        left: `${position.x}px`,
+                        top: `${position.y}px`,
+                        width: props.width + "px",
+                        height: popupHieght.value + "px",
+                        zIndex: props.zIndex, // 确保弹窗在最上层
+                      },
+                    },
+                    [
+                      h(SsFullStyleHeader, {
+                        class: "header",
+                        title: props.headerTitle,
+                        onClose: onClose,
+                        onMousedown: startDrag, // 绑定拖动事件
+                        onMouseUp: endDrag,
+                        ...(!showHeader.value && {
+                          onMouseenter: onMouseEnterCloseButton,
+                          onMouseleave: onMouseLeaveCloseButton,
+                        }),
+                        style: {
+                          cursor: position.isDragging ? "grabbing" : "grab",
+                          visibility: headerVisible.value ? "hidden" : "visible",
+                        },
+                      }),
+                      h(
+                          "div",
+                          {
+                            class: "body",
+                            style: {},
+                          },
+                          [
+                            h("iframe", {
+                              src: props.src,
+                              frameborder: 0,
+                              style: { width: "100%", height: "100%" },
+                            }),
+                          ]
+                      ),
+                      headerVisible.value &&
+                      h("div", {
+                        class: "close-button",
+
+                        onMouseenter: onMouseEnterCloseButton,
+                        onMouseleave: onMouseLeaveCloseButton,
+                        style: {
+                          position: "absolute",
+                          top: "0",
+                          right: "0",
+                          // background: 'black',
+                          width: "60px",
+                          height: "60px",
+                          cursor: "pointer",
+                        },
+                      }),
+                    ]
+                ),
+              ])
+          );
+    },
+  };
+  // ss-mark遮罩层
+  const SsMark = {
+    name: "SsMark",
+
+    setup(props, { slots, emit }) {
+      return () =>
+          h("div", { class: "dialog-container" }, [
+            h("div", { class: "mark-content" }, [
+              h("div", { class: "dialog-contianer" }, [
+                slots.default ? slots.default() : "",
+              ]),
+            ]),
+          ]);
+    },
+  };
+  // ss-bottom-button 底部按钮
+  const SsBottomButton = {
+    name: "SsBottomButton",
+    props: {
+      text: {
+        type: String,
+        required: true,
+      },
+      type: {
+        type: String,
+        default: "button",
+      },
+      iconClass: {
+        type: String,
+      },
+      class: {
+        type: String,
+        default: "",
+      },
+      onclick: {
+        type: [Function, String],
+        default: null,
+      },
+    },
+    setup(props, { emit }) {
+      const SsBottomDivIcon = Vue.resolveComponent("ss-bottom-div-icon");
+      return () =>
+          h(
+              "button",
+              {
+                class: props.class,
+                onClick: (e) => {
+                  e.stopPropagation();
+                  if (props.onclick) {
+                    // 如果是函数直接调用
+                    if (typeof props.onclick === "function") {
+                      props.onclick(e);
+                    } else if (typeof props.onclick === "string") {
+                      // 如果是字符串,使用直接的方法执行
+                      // 临时存储按钮元素到全局变量
+                      window.__ss_current_button = e.currentTarget;
+
+                      // 直接执行代码,使用eval以保留原始上下文
+                      try {
+                        eval(props.onclick);
+                      } finally {
+                        // 清理全局变量
+                        delete window.__ss_current_button;
+                      }
+                    }
+                  }
+                },
+                type: props.type,
+              },
+              [
+                h("span", null, [
+                  h(SsBottomDivIcon, {
+                    class: props.iconClass,
+                  }),
+                ]),
+                h("span", null, props.text),
+              ]
+          );
+    },
+  };
+  // ss-search搜索框
+  const SsSearch = {
+    name: "SsSearch",
+    props: {
+      theme: {
+        type: String,
+        default: "light",
+        validator: function (value) {
+          return ["dark", "light"].includes(value);
+        },
+      },
+      placeholder: {
+        type: String,
+        default: "请输入搜索条件",
+      },
+    },
+    setup(props, { emit }) {
+      const onClick = () => {
+        console.log("Search clicked");
+        emit("click");
+      };
+      const SsIcon = Vue.resolveComponent("ss-icon");
+      return () =>
+          Vue.h(
+              "div",
+              {
+                class: ["search-container", props.theme],
+                onClick: onClick,
+              },
+              [
+                Vue.h("input", {
+                  placeholder: props.placeholder,
+                  disabled: true,
+                }),
+                Vue.h(SsIcon, {
+                  name: "search-result",
+                  size: "20px",
+                }),
+              ]
+          );
+    },
+  };
+  // ss-cart-item 菜单页面的卡片 左右结构
+  const SsCartItem = {
+    name: "SsCartItem",
+    props: {
+      active: Boolean,
+      item: {
+        type: Object,
+        default: () => ({
+          thumb: "images/example/project-img.png",
+          title: "广州(国际)科技成果转化天河基地专",
+          description: "佳能中国广州分公司",
+          all: 50,
+          finish: 5,
+        }),
+      },
+    },
+    setup(props, { emit }) {
+      const item = props.item;
+
+      const itemWidth = Vue.computed(() => {
+        const containerWidth =
+            document.body.clientWidth || document.body.scrollWidth - 520;
+        const halfWidth = containerWidth / 2;
+        if (halfWidth < 480) {
+          return Math.min(containerWidth, 702) + "px";
+        } else {
+          return Math.min(halfWidth, 702) + "px";
+        }
+      });
+
+      const onItemClick = (e) => {
+        emit("click", e);
+      };
+
+      return {
+        item,
+        itemWidth,
+        onItemClick,
+      };
+    },
+    render() {
+      const SsIcon = Vue.resolveComponent("ss-icon");
+      return Vue.h(
+          "div",
+          {
+            class: { "item-container": true, active: this.active },
+            onClick: this.onItemClick,
+            style: { width: this.itemWidth },
+          },
+          [
+            Vue.h("div", { class: "header" }, [
+              Vue.h(SsIcon, { name: "setting", size: "20px" }),
+            ]),
+            Vue.h("div", { class: "body" }, [
+              Vue.h("div", { class: "left" }, [
+                Vue.h("img", {
+                  src: this.item.thumb,
+                  alt: "Thumbnail",
+                  class: "imgUnHandle",
+                  style: { "object-fit": "cover", width: "100%", height: "100%" },
+                }),
+              ]),
+              Vue.h("div", { class: "right" }, [
+                Vue.h("div", { class: "title" }, this.item.title),
+                Vue.h("div", { class: "desc" }, this.item.description),
+                Vue.h("div", { class: "progress" }, [
+                  Vue.h(
+                      "div",
+                      {
+                        style: {
+                          width: `${(this.item.finish / this.item.all) * 100}%`,
+                        },
+                      },
+                      [Vue.h("div", `${this.item.finish}/${this.item.all}`)]
+                  ),
+                ]),
+              ]),
+            ]),
+          ]
+      );
+    },
+  };
+  // ss-cart-item2 菜单页面的卡片 上下结构
+  const SsCartItem2 = {
+    name: "SsCartItem2",
+    props: {
+      active: Boolean,
+      item: {
+        type: Object,
+        default: () => ({
+          thumb: "images/example/project-img.png",
+          title: "广州(国际)科技成果转化天河基地专",
+          description: "佳能中国广州分公司",
+          all: 50,
+          finish: 5,
+        }),
+      },
+    },
+    setup(props, { emit }) {
+      const item = props.item;
+      const itemWidth = Vue.computed(() => {
+        const containerWidth =
+            document.body.clientWidth || document.body.scrollWidth - 520;
+        const halfWidth = containerWidth / 2;
+        if (halfWidth < 480) {
+          return Math.min(containerWidth, 702) + "px";
+        } else {
+          return Math.min(halfWidth, 702) + "px";
+        }
+      });
+
+      const onItemClick = (e) => {
+        emit("click", e);
+      };
+
+      return {
+        item,
+        itemWidth,
+        onItemClick,
+      };
+    },
+
+    render() {
+      const SsIcon = Vue.resolveComponent("ss-icon");
+      return Vue.h(
+          "div",
+          {
+            class: { "item-container2": true, active: this.active },
+            onClick: this.onItemClick,
+            style: { width: this.itemWidth },
+          },
+          [
+            Vue.h("div", { class: "action-bar" }, [
+              Vue.h(SsIcon, { name: "setting", size: "20px" }),
+            ]),
+            Vue.h("div", { class: "header" }, [
+              Vue.h("div", { class: "title" }, `${this.item.title}`),
+            ]),
+            Vue.h("div", { class: "body" }, [
+              Vue.h("div", { class: "left" }, [
+                Vue.h("img", {
+                  src: this.item.thumb,
+                  alt: "Thumbnail",
+                  class: "imgUnHandle",
+                  style: { "object-fit": "cover", width: "100%", height: "100%" },
+                }),
+              ]),
+              Vue.h("div", { class: "right" }, [
+                Vue.h("div", { class: "content" }, this.item.description),
+                Vue.h("div", { class: "tip" }, [
+                  Vue.h("div", { class: "progress" }, [
+                    Vue.h(
+                        "div",
+                        {
+                          style: {
+                            width: `${(this.item.finish / this.item.all) * 100}%`,
+                          },
+                        },
+                        [Vue.h("div", `${this.item.finish}/${this.item.all}`)]
+                    ),
+                  ]),
+                ]),
+              ]),
+            ]),
+          ]
+      );
+    },
+  };
+  // ss-list-card 通用列表卡片
+  const SsListCard = {
+    name: "SsListCard",
+    props: {
+      item: {
+        type: Object,
+        required: true,
+      },
+    },
+    emits: ["click", "change"],
+    setup(props, { emit }) {
+      const item = props.item;
+      const itemWidth = Vue.computed(() => {
+        const containerWidth =
+            document.body.clientWidth || document.body.scrollWidth;
+        if (containerWidth > 1200) {
+          return "30%";
+        }
+        return "45%";
+      });
+      const onItemClick = (e) => {
+        // 清除所有类型卡片的 active 状态
+        const allListCards = document.querySelectorAll(
+            ".knowledge-item-container"
+        );
+        const allFolderCards = document.querySelectorAll(".ss-folder-list");
+
+        allListCards.forEach((card) => card.classList.remove("active"));
+        allFolderCards.forEach((card) => card.classList.remove("active"));
+
+        // 设置当前项的 active 状态
+        e.currentTarget.classList.add("active");
+        props.item.onclick();
+      };
+      const onItemChange = (e, icon, index) => {
+        e.stopPropagation(); // 阻止事件冒泡到卡片
+        props.item.buttons[0].onclick();
+        // emit("change", { item: props.item, icon, index });
+      };
+      return {
+        item,
+        itemWidth,
+        onItemClick,
+        onItemChange,
+      };
+    },
+
+    data() {
+      return {
+        showButtons: false,
+      };
+    },
+
+    render() {
+      const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
+      return Vue.h(
+          "div",
+          {
+            class: { "knowledge-item-container": true, active: this.item.active },
+            onClick: this.onItemClick,
+            style: { width: this.itemWidth },
+          },
+          [
+            this.item?.buttons?.length > 0 &&
+            Vue.h(
+                "div",
+                {
+                  class: "header",
+                  onMouseenter: () => (this.showButtons = true),
+                  onMouseleave: () => (this.showButtons = false),
+                  onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0),
+                },
+                [
+                  // 只在有按钮时渲染设置图标
+                  // this.item?.buttons?.length > 0 &&
+                  Vue.h("div", {
+                    class: "cart-list-setting cart-list-icon",
+                    title: this.item?.buttons?.[0]?.title,
+                  }),
+
+                  // 鼠标移入时显示按钮列表,与图标同级
+                  // this.item?.buttons?.length > 0 &&
+                  this.showButtons &&
+                  this.item?.buttons?.length > 1 &&
+                  Vue.h(
+                      "div",
+                      {
+                        class: "cart-list-button-popup",
+                      },
+                      this.item.buttons.map((btn) =>
+                          Vue.h(
+                              "div",
+                              {
+                                onClick: (e) => {
+                                  e.stopPropagation();
+                                  btn.onclick?.();
+                                },
+                              },
+                              [
+                                // 如果有 class,显示对应的图标
+                                btn.class &&
+                                Vue.h(SsCartListIcon, {
+                                  class: [btn.class],
+                                }),
+                                // 显示按钮文本
+                                Vue.h("span", null, btn.title),
+                              ]
+                          )
+                      )
+                  ),
+                ]
+            ),
+
+            Vue.h("div", { class: "body" }, [
+              Vue.h("div", { class: "box-header" }, [
+                Vue.h("div", `${this.item.title}`),
+              ]),
+              Vue.h(
+                  "div",
+                  {
+                    class: !this.item.thumb ? "no-thumb box-body" : "box-body",
+                  },
+                  [
+                    this.item.thumb
+                        ? Vue.h("div", { class: "left" }, [
+                          Vue.h("img", {
+                            src: this.item.thumb,
+                            alt: "Thumbnail",
+                            class: "imgUnHandle",
+                            style: {
+                              "object-fit": "cover",
+                              width: "100%",
+                              height: "100%",
+                            },
+                          }),
+                        ])
+                        : null,
+                    Vue.h("div", { class: "right" }, [
+                      ...this.item.tags.map((tag) => {
+                        const [key, value] = Object.entries(tag)[0];
+                        return Vue.h(
+                            "div",
+                            {
+                              class: "title",
+                              title: `${key}: ${value}`,
+                            },
+                            `${key}: ${value}`
+                        );
+                      }),
+                    ]),
+                  ]
+              ),
+            ]),
+          ]
+      );
+    },
+  };
+  // ss-folder-card 文件夹卡片
+  const SsFolderCard = {
+    name: "SsFolderCard",
+    props: {
+      item: {
+        type: Object,
+        required: true,
+      },
+    },
+    data() {
+      return {
+        showButtons: false,
+      };
+    },
+    emits: ["click", "change"],
+    setup(props, { emit }) {
+      const item = props.item;
+      const showChildren = ref(false);
+      const eventBus = window.parent.sharedEventBus;
+
+      const itemWidth = Vue.computed(() => {
+        const containerWidth =
+            document.body.clientWidth || document.body.scrollWidth;
+        return containerWidth > 1200 ? "45%" : "90%";
+      });
+      onMounted(() => {
+        eventBus.subscribe("folderPath", (path) => {
+          const currentPath = path || [];
+          // 如果当前文件夹不在路径中,则销毁视图
+          if (
+              !currentPath.some((item) => item.folder.title === props.item.title)
+          ) {
+            showChildren.value = false;
+          }
+        });
+      });
+      const onItemClick = (e) => {
+        if (e && e.stopPropagation) {
+          e.stopPropagation();
+        }
+        // 单击只处理 active 状态
+        if (e && e.currentTarget) {
+          const allListCards = document.querySelectorAll(
+              ".knowledge-item-container"
+          );
+          const allFolderCards = document.querySelectorAll(".ss-folder-list");
+
+          allListCards.forEach((card) => card.classList.remove("active"));
+          allFolderCards.forEach((card) => card.classList.remove("active"));
+
+          e.currentTarget.classList.add("active");
+        } else {
+          // 如果是数据对象,需要找到对应的 DOM 元素
+          const allListCards = document.querySelectorAll(
+              ".knowledge-item-container"
+          );
+          const allFolderCards = document.querySelectorAll(".ss-folder-list");
+          allListCards.forEach((card) => card.classList.remove("active"));
+          allFolderCards.forEach((card) => card.classList.remove("active"));
+          // 找到标题匹配的文件夹元素
+          const targetFolder = Array.from(allFolderCards).find((card) =>
+              card.textContent.includes(e.title)
+          );
+
+          if (targetFolder) {
+            targetFolder.classList.add("active");
+          }
+        }
+        emit("click", item);
+      };
+      // 修改双击处理函数
+      const handleFolderDblClick = (folder, e) => {
+        if (e) e.stopPropagation();
+        if (folder.children?.length) {
+          showChildren.value = true;
+          const pathInfo = {
+            title: folder.title,
+            folder: folder,
+          };
+          const currentPath = eventBus.getState("folderPath") || [];
+          if (!currentPath.some((item) => item.title === folder.title)) {
+            eventBus.publish("folderPath", [...currentPath, pathInfo]);
+          }
+        }
+      };
+      const onItemChange = (e, icon, index) => {
+        e.stopPropagation();
+        props.item.buttons[0].onclick();
+        // emit("change", { item: props.item, icon, index });
+      };
+      return {
+        item,
+        itemWidth,
+        showChildren,
+        onItemClick,
+        onItemChange,
+        handleFolderDblClick,
+      };
+    },
+    render() {
+      const SsCartListIcon = Vue.resolveComponent("ss-cart-list-icon");
+      if (this.showChildren) {
+        return h(SsFolderCartView, {
+          folder: this.item,
+        });
+      }
+      return Vue.h(
+          "div",
+          {
+            class: { "ss-folder-list": true, active: this.item.active },
+            onClick: (e) => {
+              e.stopPropagation();
+              this.onItemClick(e);
+            },
+            onDblclick: (e) => this.handleFolderDblClick(this.item, e),
+            style: { width: this.itemWidth },
+          },
+          [
+            // 文件夹特有的装饰元素
+            Vue.h("div", { class: "ss-folder-list-trapezoid" }),
+            Vue.h("div", { class: "ss-folder-list-top-transparent" }),
+            Vue.h("div", { class: "ss-folder-list-top" }),
+            Vue.h("div", { class: "ss-folder-list-right" }),
+
+            // header 部分(按钮)
+            this.item?.buttons?.length > 0 &&
+            Vue.h(
+                "div",
+                {
+                  class: "header",
+                  onMouseenter: () => (this.showButtons = true),
+                  onMouseleave: () => (this.showButtons = false),
+                  onClick: (e) => this.onItemChange(e, this.item.buttons[0], 0),
+                },
+                [
+                  // this.item?.buttons?.length > 0 &&
+                  Vue.h("div", {
+                    class: "cart-list-setting cart-list-icon",
+                    title: this.item?.buttons?.[0]?.title,
+                  }),
+
+                  // this.item?.buttons?.length > 0 &&
+                  this.showButtons &&
+                  this.item?.buttons?.length > 1 &&
+                  Vue.h(
+                      "div",
+                      {
+                        class: "cart-list-button-popup",
+                      },
+                      this.item.buttons.map((btn) =>
+                          Vue.h(
+                              "div",
+                              {
+                                onClick: (e) => {
+                                  e.stopPropagation();
+                                  btn.onclick?.();
+                                },
+                              },
+                              [
+                                btn.class &&
+                                Vue.h(SsCartListIcon, {
+                                  class: [btn.class],
+                                }),
+                                Vue.h("span", null, btn.title),
+                              ]
+                          )
+                      )
+                  ),
+                ]
+            ),
+            // body 部分
+            Vue.h("div", { class: "body" }, [
+              Vue.h("div", { class: "box-header" }, [
+                Vue.h("div", null, this.item.title),
+              ]),
+              Vue.h(
+                  "div",
+                  {
+                    class: !this.item.thumb ? "no-thumb box-body" : "box-body",
+                  },
+                  [
+                    this.item.thumb
+                        ? Vue.h("div", { class: "left" }, [
+                          Vue.h("img", {
+                            src: this.item.thumb,
+                            alt: "Thumbnail",
+                            class: "imgUnHandle",
+                            style: {
+                              "object-fit": "cover",
+                              width: "100%",
+                              height: "100%",
+                            },
+                          }),
+                        ])
+                        : null,
+                    Vue.h("div", { class: "right" }, [
+                      ...this.item.tags.map((tag) => {
+                        const [key, value] = Object.entries(tag)[0];
+                        return Vue.h(
+                            "div",
+                            {
+                              class: "title",
+                              title: `${key}: ${value}`,
+                            },
+                            `${key}: ${value}`
+                        );
+                      }),
+                    ]),
+                  ]
+              ),
+            ]),
+          ]
+      );
+    },
+  };
+  // SsFolderCartView 组件 - 用于显示文件夹内容
+  const SsFolderCartView = {
+    name: "SsFolderCartView",
+    props: {
+      folder: {
+        type: Object,
+        required: true,
+      },
+    },
+    emits: ["click"],
+    setup(props, { emit }) {
+      const eventBus = window.parent.sharedEventBus;
+      const currentFolder = ref(props.folder);
+      const showChildren = ref(false);
+
+      const onItemClick = (e) => {
+        if (e && e.stopPropagation) {
+          e.stopPropagation();
+        }
+        // 单击只处理 active 状态
+        if (e && e.currentTarget) {
+          const allListCards = document.querySelectorAll(
+              ".knowledge-item-container"
+          );
+          const allFolderCards = document.querySelectorAll(".ss-folder-list");
+
+          allListCards.forEach((card) => card.classList.remove("active"));
+          allFolderCards.forEach((card) => card.classList.remove("active"));
+
+          e.currentTarget.classList.add("active");
+        } else {
+          // 如果是数据对象,需要找到对应的 DOM 元素
+          const allListCards = document.querySelectorAll(
+              ".knowledge-item-container"
+          );
+          const allFolderCards = document.querySelectorAll(".ss-folder-list");
+
+          allListCards.forEach((card) => card.classList.remove("active"));
+          allFolderCards.forEach((card) => card.classList.remove("active"));
+
+          // 找到标题匹配的文件夹元素
+          const targetFolder = Array.from(allFolderCards).find((card) =>
+              card.textContent.includes(e.title)
+          );
+
+          if (targetFolder) {
+            targetFolder.classList.add("active");
+          }
+        }
+        emit("click", props.folder);
+      };
+      const handleFolderDblClick = (folder, e) => {
+        if (e) e.stopPropagation();
+        if (folder.children?.length) {
+          showChildren.value = true;
+          const pathInfo = {
+            title: folder.title,
+            folder: folder,
+          };
+          const currentPath = eventBus.getState("folderPath") || [];
+          if (!currentPath.some((item) => item.title === folder.title)) {
+            eventBus.publish("folderPath", [...currentPath, pathInfo]);
+            currentFolder.value = folder;
+          }
+        }
+      };
+
+      const goBack = (targetFolder) => {
+        if (targetFolder === null) {
+          // 返回根目录
+          eventBus.publish("folderPath", []);
+        } else {
+          currentFolder.value = targetFolder;
+        }
+      };
+
+      return {
+        currentFolder,
+        showChildren,
+        onItemClick,
+        handleFolderDblClick,
+        goBack,
+      };
+    },
+
+    render() {
+      return h(
+          "div",
+          {
+            class: "page-container",
+            style: {
+              position: "fixed",
+              top: 0,
+              left: 0,
+              width: "100%",
+              height: "100%",
+              background: "var(--lightgray)",
+              padding: "20px 0",
+              zIndex: 1000,
+            },
+          },
+          [
+            // 搜索栏
+            h("div", { class: "search-bar" }, [
+              h("div", { class: "search-bar-contaienr" }, [
+                h(SsBreadcrumb, {
+                  level: {
+                    onBack: this.goBack,
+                  },
+                }),
+              ]),
+            ]),
+
+            // 内容区域
+            h(
+                "div",
+                {
+                  class: "content-area item-content-area",
+                  style: { gap: "20px" },
+                },
+                [
+                  ...(this.currentFolder.children || []).map((child, index) =>
+                      h(child.children ? SsFolderCard : SsListCard, {
+                        key: index,
+                        item: child,
+                        onClick: (e) => this.onItemClick(e),
+                        onDblclick: (e) => this.handleFolderDblClick(child, e),
+                      })
+                  ),
+                ]
+            ),
+          ]
+      );
+    },
+  };
+
+  // ss-page分页
+  const SsPage = {
+    name: "SsPage",
+    props: {
+      total: {
+        type: Number,
+        required: true,
+      },
+
+      size: {
+        type: Number,
+        default: 10,
+      },
+      page: {
+        type: Number,
+        default: 1,
+      },
+      onChange: {
+        type: Function,
+        default: () => {},
+      },
+    },
+    setup(props) {
+      const totalItems = ref(props.total); // 总条目数
+      const totalPages = ref(Math.ceil(props.total / props.size));
+      const currentPage = ref(props.page); // 当前页码
+
+      // 计算显示的信息
+      const pageInfo = ref(
+          `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页`
+      );
+
+      // 上一页的逻辑
+      const goToPreviousPage = (e) => {
+        e.preventDefault(); // 阻止默认行为
+        if (currentPage.value > 1) {
+          currentPage.value -= 1;
+          updatePageInfo();
+          props.onChange?.({
+            pageNo: currentPage.value, // 当前页码
+            rowNumPer: props.size, // 每页条数
+            rowNum: props.total, // 总记录数
+          });
+        }
+      };
+
+      // 下一页的逻辑
+      const goToNextPage = (e) => {
+        e.preventDefault(); // 阻止默认行为
+        if (currentPage.value < totalPages.value) {
+          currentPage.value += 1;
+          updatePageInfo();
+          props.onChange?.({
+            pageNo: currentPage.value, // 当前页码
+            rowNumPer: props.size, // 每页条数
+            rowNum: props.total, // 总记录数
+          });
+        }
+      };
+
+      // 更新页码信息的函数
+      const updatePageInfo = () => {
+        pageInfo.value = `共${totalItems.value}条,第 ${currentPage.value}/${totalPages.value} 页`;
+      };
+
+      return {
+        pageInfo,
+        totalPages,
+        goToPreviousPage,
+        goToNextPage,
+      };
+    },
+
+    render(props, { slots, emit }) {
+      return Vue.h("div", { class: "pager-container" }, [
+        Vue.h("input", { type: "hidden", name: "rowNum", value: props.total }),
+        Vue.h("input", {
+          type: "hidden",
+          name: "rowNumPer",
+          value: props.size,
+        }),
+        Vue.h("input", {
+          type: "hidden",
+          name: "pageCount",
+          value: this.totalPages,
+        }),
+        Vue.h("input", { type: "hidden", name: "pageNo", value: props.page }),
+        Vue.h("div", { class: "pager-content" }, [
+          Vue.h("div", { class: "info" }, this.pageInfo),
+          Vue.h(
+              "div",
+              { class: "btn" },
+              Vue.h(
+                  "button",
+                  { onClick: (e) => this.goToPreviousPage(e) },
+                  "上一页"
+              )
+          ),
+          Vue.h(
+              "div",
+              { class: "btn" },
+              Vue.h("button", { onClick: (e) => this.goToNextPage(e) }, "下一页")
+          ),
+        ]),
+      ]);
+    },
+  };
+  // ss-right-info 一级页面右边栏
+  const SSRightInfo = {
+    name: "SSRightInfo",
+    setup() {
+      // 初始化响应式数据
+      const item = ref({
+        thumb: "images/example/project-img.png", // 更换为适合你项目的实际路径
+        title: "工业和信息化产业高质量发展资金",
+      });
+
+      return {
+        item,
+      };
+    },
+    render() {
+      return Vue.h("div", { class: "info-container" }, [
+        Vue.h("div", { class: "header" }, [
+          Vue.h("div", [
+            Vue.h("img", {
+              src: this.item.thumb,
+              class: "imgUnHandle",
+              style: { "object-fit": "cover", width: "100%", height: "100%" },
+            }), // 将 ImageViewer 替换为 img 标签
+          ]),
+          Vue.h("div", [Vue.h("div", this.item.title)]),
+        ]),
+        Vue.h("div", { class: "section-container" }, [
+          Vue.h("div", { class: "section" }, [
+            Vue.h("div", { class: "title" }, "合同"),
+            Vue.h("div", { class: "text" }, "合同总金额:42,399,320"),
+            Vue.h(
+                "div",
+                { class: "a" },
+                "《工业和信息化产业高质量发展资金补助合同》"
+            ),
+          ]),
+          Vue.h("div", { class: "section" }, [
+            Vue.h("div", { class: "title" }, "发票"),
+            Vue.h("div", { class: "text" }, "应开发票总额:42,399,320"),
+            Vue.h("div", { class: "text" }, "已开发票金额:17,235,345"),
+            Vue.h("div", { class: "text" }, "未开发票金额:25,163,975"),
+          ]),
+          Vue.h("div", { class: "section" }, [
+            Vue.h("div", { class: "title" }, "项目组成员"),
+            Vue.h("div", { class: "text" }, "我司:3人"),
+            Vue.h("div", { class: "text" }, "对方:2人"),
+            Vue.h("div", { class: "text" }, "项目负责人:张三"),
+          ]),
+          Vue.h("div", { class: "section" }, [
+            Vue.h("div", { class: "title" }, "采购"),
+            Vue.h("div", { class: "text" }, "总额:999,320"),
+            Vue.h("div", { class: "text" }, "已付金额:335,345"),
+            Vue.h("div", { class: "text" }, "未付金额:663,975"),
+          ]),
+        ]),
+      ]);
+    },
+  };
+  //
+  const SsSuccessPopup = {
+    name: "SsSuccessPopup",
+    props: {
+      right: {
+        type: String,
+        default: "20px",
+      },
+      bottom: {
+        type: String,
+        default: "calc(100% + 5px)",
+      },
+    },
+    setup(props, { expose }) {
+      // 响应式状态:是否可见
+      const visible = ref(false);
+
+      // 计算样式
+      const style = computed(() => {
+        return {
+          "--message-dialog-right": props.right,
+          "--message-dialog-bottom": props.bottom,
+        };
+      });
+
+      // 显示对话框的方法
+      const show = () => {
+        visible.value = true;
+      };
+
+      // 隐藏对话框的方法
+      const hide = () => {
+        visible.value = false;
+      };
+
+      // 将方法暴露给外部使用
+      expose({ show, hide });
+
+      // 返回渲染函数
+      return () => {
+        if (!visible.value) return null;
+        const SsIcon = resolveComponent("ss-icon");
+        return h(
+            "div",
+            {
+              class: "success-popup",
+              style: style.value,
+              onClick: (e) => e.stopPropagation(),
+            },
+            [
+              h("div", { class: "left" }, [
+                h("div", { class: "icon" }, [
+                  h(SsIcon, { name: "check", size: "36px" }),
+                ]),
+              ]),
+              h("div", { class: "right" }, [
+                h("div", { class: "title" }, "提交成功"),
+                h("div", { class: "desc" }, "您的信息已成功提交。"),
+              ]),
+            ]
+        );
+      };
+    },
+  };
+  const SsErrorDialog = {
+    name: "SsErrorDialog",
+    setup(props, { emit }) {
+      const visible = ref(false);
+
+      const style = computed(() => {
+        return {};
+      });
+
+      const show = () => {
+        visible.value = true;
+      };
+
+      const hide = () => {
+        visible.value = false;
+      };
+
+      const onBack = () => {
+        emit("back");
+        hide();
+      };
+
+      return {
+        visible,
+        style,
+        show,
+        hide,
+        onBack,
+      };
+    },
+    render() {
+      const SsIcon = resolveComponent("ss-icon");
+      return this.visible
+          ? h(
+              "div",
+              {
+                class: "errorDialog",
+                style: this.style,
+                onClick: (event) => event.stopPropagation(),
+              },
+              [
+                h("div", { class: "body" }, [
+                  h("div", { class: "left" }, [
+                    h("div", { class: "icon" }, [
+                      h(SsIcon, { name: "close", size: "36px" }),
+                    ]),
+                  ]),
+                  h("div", { class: "right" }, [
+                    h("div", { class: "title" }, "操作失败"),
+                    h("div", { class: "desc" }, "请点击返回以继续。"),
+                  ]),
+                ]),
+                h("div", { class: "footer" }, [
+                  h("div", { class: "left" }),
+                  h("div", { class: "right" }, [
+                    h(
+                        "div",
+                        {
+                          class: "btn",
+                          onClick: this.onBack,
+                        },
+                        [h(SsIcon, { name: "arrow-left-line" }), h("div", "返回")]
+                    ),
+                  ]),
+                ]),
+              ]
+          )
+          : null;
+    },
+  };
+  /**
+   * 审核链条
+   * @name ss-verify
+   * @param { Array } verify-list 审核节点列表
+   * @property { Array } verify-list 审核节点列表
+   * @example <ss-verify :verify-list="verifyList"></ss-verify>
+   * verify-list [
+   *    {
+   *        groupName: "",  // 群组名称
+   *        open: true, // 默认是否展开
+   *        children:[ //群组里的人员
+   *           {
+   *            thumb: "images/example/user-4.png", // 头像
+   *             name: "李丽思 ", // 姓名
+   *             role: "人事处处长",  // 角色
+   *             description: "同意。",  // 审核意见
+   *             time: "09:38 08/11", // 审核时间
+   *             video: false,  // false不显示/true显示 视频icon
+   *             link: false,  // false不显示/true显示 链接icon 后续应该是附件
+   *           }
+   *        ]
+   *    }
+   *  ]
+   */
+  const SsVerify = {
+    name: "SsVerify",
+    props: {
+      verifyList: {
+        type: Array,
+        required: true,
+      },
+    },
+    setup(props) {
+      const toggleOpen = (item) => {
+        item.open = !item.open;
+      };
+      onMounted(() => {
+        setTimeout(() => {
+          const lastOpenGroup = document.querySelector(".group-item-last-open");
+          console.log("lastOpenGroup", lastOpenGroup);
+          if (lastOpenGroup) {
+            const nodes = $(lastOpenGroup).find(".verify-node-container");
+            if (nodes.length) {
+              let totalHeight = 0;
+              const gudingHeight = 100;
+
+              if (nodes.length === 1) {
+                totalHeight = gudingHeight;
+              } else {
+                // 累加除最后一个节点外的所有节点高度
+                for (let i = 0; i < nodes.length - 1; i++) {
+                  totalHeight += $(nodes[i]).outerHeight();
+                }
+                totalHeight += gudingHeight;
+              }
+
+              console.log("节点信息:", {
+                节点总数: nodes.length,
+                计算后的高度: totalHeight,
+              });
+
+              lastOpenGroup.style.setProperty(
+                  "--group-line-height",
+                  `${totalHeight}px`
+              );
+            }
+          }
+        }, 0);
+      });
+      return {
+        toggleOpen,
+      };
+    },
+    render() {
+      const SsIcon = resolveComponent("ss-icon");
+      const SsCommonIcon = resolveComponent("ss-common-icon");
+      const SsVerifyNode = resolveComponent("ss-verify-node");
+      return h(
+          "div",
+          { class: "verify-nodes" },
+          this.verifyList.map((item, i) =>
+              h(
+                  "div",
+                  {
+                    key: i,
+                    class: {
+                      "group-item": true,
+                      "group-item-last-open":
+                          i === this.verifyList.length - 1 && item.open,
+                    },
+                  },
+                  [
+                    h(
+                        "div",
+                        {
+                          class: "group-item-title",
+                          onClick: () => this.toggleOpen(item),
+                        },
+                        [
+                          h("div", { class: "icon" }, [
+                            item.open
+                                ? h(SsCommonIcon, { class: "common-icon-folder-open" })
+                                : h(SsCommonIcon, { class: "common-icon-folder-close" }),
+                            h(
+                                "div",
+                                {
+                                  class: "num",
+                                  style: { top: item.open ? "60%" : "55%" },
+                                },
+                                item.children?.length || 0
+                            ),
+                          ]),
+                          h("div", { class: "name" }, item.groupName),
+                        ]
+                    ),
+                    item.open && item.children?.length > 0
+                        ? h(
+                            "div",
+                            { class: "group-item-children" },
+                            item.children.map((citem, j) =>
+                                h(SsVerifyNode, {
+                                  key: j,
+                                  item: citem,
+                                  // isGroup: i + 1 !== this.verifyList.length,
+                                  isGroup: true,
+                                })
+                            )
+                        )
+                        : null,
+                  ]
+              )
+          )
+      );
+    },
+  };
+
+  /**
+   * 审核页面的审核节点
+   * @name ss-verify-node
+   * @param {Object} item 审核节点信息
+   * @param {Boolean} isGroup 是否为分组节点
+   */
+  const SsVerifyNode = {
+    name: "SsVerifyNode",
+    props: {
+      item: {
+        type: Object,
+        required: true,
+      },
+      isGroup: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    render() {
+      const SsIcon = resolveComponent("ss-icon");
+      const SsCommonIcon = resolveComponent("ss-common-icon");
+      return Vue.h("div", { class: "verify-node-container" }, [
+        Vue.h("div", { class: "info" }, [
+          Vue.h("div", { class: "avatar" }, [
+            Vue.h("img", {
+              src: this.item.thumb,
+              style: {
+                width: "50px",
+                height: "50px",
+                borderRadius: "50%",
+              },
+            }),
+          ]),
+          Vue.h("div", { class: "desc" }, [
+            Vue.h("div", this.item.name),
+            Vue.h("div", this.item.role),
+          ]),
+          Vue.h("div", { class: "link" }, [
+            Vue.h("div", [
+              this.item.video
+                  ? Vue.h(SsCommonIcon, { class: "common-icon-video" })
+                  : null,
+              this.item.link
+                  ? Vue.h(SsCommonIcon, {
+                    class: "common-icon-paper-clip",
+                  })
+                  : null,
+            ]),
+          ]),
+        ]),
+        Vue.h(
+            "div",
+            {
+              class: {
+                description: true,
+                link: this.isGroup,
+              },
+              attrs: { "data-num": "3" },
+            },
+            [Vue.h("div", this.item.description)]
+        ),
+        Vue.h("div", { class: "time" }, this.item.time),
+      ]);
+    },
+  };
+  /**
+   * 智能识别图片的左侧图片播放 可以放大缩小旋转图片
+   * @name ss-orc-img-box
+   * @param { Object } image-obj 包含图片的url, 和图片的名称
+   *
+   */
+  const SsOrcImgBox = {
+    name: "SsOrcImgBox",
+    props: {
+      imageObj: {
+        type: Object,
+        required: true,
+      },
+    },
+    setup(props) {
+      const zoom = ref(1);
+      const rotation = ref(0);
+      const containerWidth = ref(0);
+      const containerHeight = ref(0);
+      const container = ref(null);
+      const imgPosition = ref({ x: 0, y: 0 });
+      const isDragging = ref(false);
+      const lastMousePosition = ref({ x: 0, y: 0 });
+      const imgStyle = computed(() => ({
+        width: `${zoom.value * 100}%`,
+        height: `${zoom.value * 100}%`,
+        transform: `rotate(${rotation.value}deg) translate(${imgPosition.value.x}px, ${imgPosition.value.y}px)`,
+        transformOrigin: "center center",
+        cursor: isDragging.value ? "grabbing" : "grab",
+      }));
+
+      const resetZoom = () => {
+        zoom.value = 1;
+        rotation.value = rotation.value + 90;
+        imgPosition.value = { x: 0, y: 0 };
+      };
+
+      const handleRangeChange = (event) => {
+        const value = event.target.value / 50; // 0 到 100 映射到 0 到 2 的缩放
+        zoom.value = Math.max(value, 0.1); // 设置最小缩放值为 0.1
+      };
+      const updateImgBoxDimensions = () => {
+        if (container.value) {
+          containerWidth.value = container.value.clientWidth;
+          containerHeight.value = container.value.clientHeight;
+        }
+      };
+      const onMouseDown = (event) => {
+        isDragging.value = true;
+        lastMousePosition.value = { x: event.clientX, y: event.clientY };
+      };
+
+      const onMouseMove = (event) => {
+        if (isDragging.value) {
+          const dx = event.clientX - lastMousePosition.value.x;
+          const dy = event.clientY - lastMousePosition.value.y;
+
+          // 防止旋转后拖动的x,y反转
+          // 首先将当前旋转角度从度数转换为弧度,因为 JavaScript 的 Math 库使用弧度
+          const angle = rotation.value * (Math.PI / 180);
+          // 使用基本的二维旋转矩阵将原始位移 dx 和 dy 转换为旋转后的位移 rotatedDx 和 rotatedDy。
+          const rotatedDx = dx * Math.cos(angle) + dy * Math.sin(angle);
+          const rotatedDy = dy * Math.cos(angle) - dx * Math.sin(angle);
+
+          imgPosition.value = {
+            x: imgPosition.value.x + rotatedDx,
+            y: imgPosition.value.y + rotatedDy,
+          };
+          lastMousePosition.value = { x: event.clientX, y: event.clientY };
+        }
+      };
+
+      const onMouseUp = () => {
+        isDragging.value = false;
+      };
+      onMounted(() => {
+        nextTick(() => {
+          updateImgBoxDimensions();
+          window.addEventListener("resize", updateImgBoxDimensions);
+          window.addEventListener("mousemove", onMouseMove);
+          window.addEventListener("mouseup", onMouseUp);
+        });
+      });
+
+      return {
+        zoom,
+        rotation,
+        container,
+        imgStyle,
+        resetZoom,
+        handleRangeChange,
+        containerWidth,
+        containerHeight,
+        onMouseDown,
+        imgPosition,
+      };
+    },
+    render() {
+      const SsIcon = resolveComponent("ss-icon");
+      return h("div", { class: "ocr-img-box" }, [
+        h("div", { class: "img-bar" }, [
+          h("div", this.imageObj.name),
+          h("div", { class: "action-bar" }, [
+            h("div", { class: "ocr-img-range-box" }, [
+              h("input", {
+                type: "range",
+                min: 0,
+                max: 100,
+                value: this.zoom * 50, // 初始位置为50
+                onInput: this.handleRangeChange,
+              }),
+              h("span", { class: "line" }),
+            ]),
+            h(SsIcon, {
+              name: "reset",
+              size: "26px",
+              onClick: this.resetZoom,
+            }),
+          ]),
+        ]),
+        h("div", { class: "img-viewer", ref: "container" }, [
+          h(
+              "div",
+              {
+                class: "img-box",
+                style: {
+                  width: `${this.containerWidth}px`,
+                  height: `${this.containerHeight}px`,
+                  overflow: "hidden",
+                  position: "relative",
+                },
+              },
+              [
+                h("img", {
+                  src: this.imageObj.thumb,
+                  style: this.imgStyle,
+                  class: "zoomable-img",
+                  onMousedown: this.onMouseDown,
+                }),
+              ]
+          ),
+        ]),
+      ]);
+    },
+  };
+  // 搜索输入框组件
+  const SsSearchInput = {
+    name: "SsSearchInput",
+    props: {
+      name: String,
+      placeholder: String,
+      width: {
+        type: String,
+        default: "100px",
+      },
+      modelValue: String,
+    },
+    emits: ["update:modelValue", "search"],
+    setup(props, { emit }) {
+      const handleInput = (e) => {
+        emit("update:modelValue", e.target.value);
+      };
+      const handleKeyup = (e) => {
+        if (e.key === "Enter") {
+          emit("search");
+        }
+      };
+
+      return { handleInput, handleKeyup };
+    },
+    render() {
+      return h(
+          "div",
+          {
+            class: "input",
+            style: this.width ? { width: this.width } : undefined,
+          },
+          [
+            h("input", {
+              name: this.name,
+              placeholder: this.placeholder,
+              value: this.modelValue,
+              onInput: this.handleInput,
+              onKeyup: this.handleKeyup,
+            }),
+          ]
+      );
+    },
+  };
+  // ss-search-date-picker 日期时间选择器组件
+  const SsSearchDatePicker = {
+    name: "SsSearchDatePicker",
+    props: {
+      modelValue: {
+        type: [String, Number, Date],
+        default: "",
+      },
+      name: {
+        type: String,
+        required: true,
+      },
+      type: {
+        type: String,
+        default: "date",
+        validator: (value) => ["date", "datetime", "time"].includes(value),
+      },
+      fmt: {
+        type: String,
+        default: null,
+      },
+      placeholder: {
+        type: String,
+        default: "",
+      },
+      width: {
+        type: String,
+        default: "100%",
+      },
+    },
+
+    emits: ["update:modelValue"],
+
+    setup(props, { emit }) {
+      const errMsg = ref("");
+      const validate = () => {
+        if (window.ssVm) {
+          const result = window.ssVm.validateField(props.name);
+          console.log("validate", window.ssVm.validateField(props.name));
+          errMsg.value = result.valid ? "" : result.message;
+        }
+      };
+      // 根据type确定默认格式
+      const defaultFormat = computed(() => {
+        switch (props.type) {
+          case "datetime":
+            return "YYYY-MM-DD HH:mm:ss";
+          case "date":
+            return "YYYY-MM-DD";
+          case "time":
+            return "HH:mm:ss";
+        }
+      });
+
+      const convertJavaFormatToElement = (javaFormat) => {
+        if (!javaFormat) return null;
+        return javaFormat
+            .replace("yyyy", "YYYY")
+            .replace("MM", "MM")
+            .replace("dd", "DD")
+            .replace("HH", "HH")
+            .replace("mm", "mm")
+            .replace("ss", "ss");
+      };
+
+      const finalFormat = computed(() => {
+        if (props.fmt) {
+          return convertJavaFormatToElement(props.fmt);
+        }
+        return defaultFormat.value;
+      });
+
+      // 使用 resolveComponent 获取组件
+      const ElDatePicker = resolveComponent("ElDatePicker");
+      const ElTimePicker = resolveComponent("ElTimePicker");
+      const SsFormIcon = resolveComponent("SsFormIcon");
+      const ElIcon = resolveComponent("ElIcon");
+      let useTimePicker = true;
+      //"yyyy-MM-dd HH:mm:ss";  "日期字符串格式在java的写法",传到本组件fmt属性也是按这个格式
+      if (props.fmt) {
+        //有fmt属性,则以fmt属性优先判断类型
+        if (/[dMy]/.test(props.fmt)) {
+          //如果有传入日期格式,且含年月日
+          useTimePicker = false;
+        } else {
+          useTimePicker = true;
+        }
+      } else if (props.type !== "time") {
+        useTimePicker = false;
+      }
+      const dateType = computed(() => {
+        const fmt = props.fmt || "";
+        if (fmt.includes("HH:mm:ss")) {
+          return "datetime";
+        } else if (fmt.includes("HH:mm")) {
+          return "datetime";
+        } else if (fmt.includes("mm:ss")) {
+          return "time";
+        }
+        return "date";
+      });
+      const handleValueUpdate = (val) => {
+        emit("update:modelValue", val);
+        emit("change", val); // 同时触发 change 事件
+        setTimeout(() => {
+          validate();
+        }, 50);
+      };
+      return () =>
+          h(
+              "div",
+              { class: "ss-search-date-picker", style: { width: props.width } },
+              [
+                h("input", {
+                  type: "hidden",
+                  name: props.name,
+                  value: props.modelValue,
+                }),
+                h(useTimePicker ? ElTimePicker : ElDatePicker, {
+                  modelValue: props.modelValue,
+                  "onUpdate:modelValue": handleValueUpdate,
+                  type: dateType.value,
+                  format: finalFormat.value,
+                  "value-format": finalFormat.value,
+                  clearable: true,
+                  placeholder: props.placeholder,
+                  class: "custom-date-picker", // 用于自定义样式
+                  "time-arrow-control": props.type === "datetime", // 修改这里
+                  size: "large", // 添加这一行,改为 large 尺寸
+                  style: { width: "100%" },
+                  "prefix-icon": h(SsFormIcon, { class: "form-icon-time" }),
+                }),
+              ]
+          );
+    },
+  };
+  // 搜索按钮组件(包含下拉按钮)
+  const SsSearchButton = {
+    name: "SsSearchButton",
+    props: {
+      text: {
+        type: String,
+        required: true,
+      },
+      iconClass: {
+        type: String,
+        required: false,
+      },
+      opt: {
+        type: Array,
+        default: () => [],
+      },
+      checkId: {
+        type: String,
+        default: "0",
+      },
+    },
+    emits: ["click"],
+    setup(props, { emit }) {
+      const currentId = ref(props.checkId || "0");
+      const showPopup = ref(false);
+
+      const handleMouseEnter = () => {
+        showPopup.value = true;
+      };
+
+      const handleMouseLeave = () => {
+        showPopup.value = false;
+      };
+
+      // 添加点击事件处理,阻止默认行为
+      const handleClick = (e) => {
+        e.preventDefault();
+        if (props.opt?.length > 0) {
+          const selectedOption =
+              currentId.value === "0"
+                  ? props.opt[0]
+                  : props.opt.find((opt) => opt.id === currentId.value);
+
+          if (selectedOption) {
+            selectedOption.callback?.();
+          }
+        } else {
+          emit("click", e);
+        }
+      };
+      // 获取显示文本
+      const getDisplayText = () => {
+        if (!props.opt?.length) return props.text;
+
+        const selectedOption =
+            currentId.value === "0"
+                ? props.opt[0]
+                : props.opt.find((opt) => opt.id === currentId.value);
+
+        return selectedOption ? selectedOption.desc : props.opt[0].desc;
+      };
+
+      return () =>
+          h(
+              "button",
+              {
+                class:
+                    props.opt?.length > 0
+                        ? "ss-drop-button ss-drop-button-more"
+                        : "ss-drop-button",
+                type: "button", // 明确指定按钮类型为 button
+                onMouseenter: handleMouseEnter,
+                onMouseleave: handleMouseLeave,
+                onClick: handleClick, // 添加点击事件处理
+              },
+              [
+                props.iconClass
+                    ? h("span", {
+                      class: props.iconClass,
+                      style: { fontFamily: "iconfont", marginRight: "5px" },
+                    })
+                    : null,
+                h("span", getDisplayText()),
+                props.opt.length > 0 &&
+                showPopup.value &&
+                h(
+                    "div",
+                    {
+                      class: "popup",
+                    },
+                    props.opt.map((item) =>
+                        h(
+                            "div",
+                            {
+                              onClick: (e) => {
+                                e.preventDefault(); // 选项点击也阻止默认行为
+                                e.stopPropagation(); // 阻止事件冒泡
+                                currentId.value = item.id; // 更新当前选中的ID
+                                item.callback();
+                                showPopup.value = false; // 选择后关闭弹窗
+                              },
+                            },
+                            item.desc
+                        )
+                    )
+                ),
+              ]
+          );
+    },
+  };
+  // 下拉按钮组件
+  const SsDropButton = {
+    name: "SsDropButton",
+    props: {
+      text: {
+        type: String,
+        required: true,
+      },
+      iconClass: {
+        type: String,
+        required: true,
+      },
+      opt: {
+        type: Array,
+        default: () => [],
+      },
+      checkId: {
+        type: String,
+        default: "0",
+      },
+      onclick: {
+        type: Function,
+        default: null,
+      },
+    },
+    setup(props) {
+      const currentId = ref(props.checkId || "0");
+      const showPopup = ref(false);
+
+      const handleMouseEnter = () => {
+        showPopup.value = true;
+      };
+
+      const handleMouseLeave = () => {
+        showPopup.value = false;
+      };
+
+      // 添加点击事件处理,阻止默认行为
+      const handleClick = (e) => {
+        e.preventDefault();
+        if (props.opt?.length > 0) {
+          const selectedOption =
+              currentId.value === "0"
+                  ? props.opt[0]
+                  : props.opt.find((opt) => opt.id === currentId.value);
+
+          if (selectedOption) {
+            selectedOption.callback?.();
+          }
+        } else if (props.onclick) {
+          props.onclick();
+        }
+      };
+      // 获取显示文本
+      const getDisplayText = () => {
+        if (!props.opt?.length) return props.text;
+
+        const selectedOption =
+            currentId.value === "0"
+                ? props.opt[0]
+                : props.opt.find((opt) => opt.id === currentId.value);
+
+        return selectedOption ? selectedOption.desc : props.opt[0].desc;
+      };
+
+      return () =>
+          h(
+              "button",
+              {
+                class:
+                    props.opt?.length > 0
+                        ? "ss-drop-button ss-drop-button-more"
+                        : "ss-drop-button",
+                type: "button", // 明确指定按钮类型为 button
+                onMouseenter: handleMouseEnter,
+                onMouseleave: handleMouseLeave,
+                onClick: handleClick, // 添加点击事件处理
+              },
+              [
+                h("span", {
+                  class: props.iconClass,
+                  style: { fontFamily: "iconfont" },
+                }),
+                h("span", getDisplayText()),
+                props.opt.length > 0 &&
+                showPopup.value &&
+                h(
+                    "div",
+                    {
+                      class: "popup",
+                    },
+                    props.opt.map((item) =>
+                        h(
+                            "div",
+                            {
+                              onClick: (e) => {
+                                e.preventDefault(); // 选项点击也阻止默认行为
+                                e.stopPropagation(); // 阻止事件冒泡
+                                currentId.value = item.id; // 更新当前选中的ID
+                                item.callback();
+                                showPopup.value = false; // 选择后关闭弹窗
+                              },
+                            },
+                            item.desc
+                        )
+                    )
+                ),
+              ]
+          );
+    },
+  };
+  /**
+   * 二级页面标签组件
+   * @name ss-sub-tab
+   * @description 用于展示二级页面的布局组件,包含左侧垂直标签导航(支持分组)和右侧iframe内容区
+   * @property {String} headerImage - 左侧顶部图片地址
+   * @property {Array} menuList - 菜单配置列表
+   * @property {Object} [activeMenu] - 当前选中的菜单项,不传则自动选择第一个可选菜单
+   * @property {Array} footerButtons - 底部按钮配置列表
+
+   */
+  const SsSubTab = {
+    name: "SsSubTab",
+    props: {
+      headerImage: {
+        type: String,
+        default: "",
+      },
+      menuList: {
+        type: Array,
+        required: true,
+      },
+      activeMenu: {
+        // 只需要传入标题
+        type: String,
+        default: "",
+      },
+      footerButtons: {
+        type: Array,
+        default: () => [],
+      },
+      leftDisplay: {
+        type: Boolean,
+        default: true,
+      },
+    },
+    emits: ["menu-change", "footer-click"],
+    setup(props, { emit }) {
+      // 根据标题找到对应的菜单项
+      const findMenuByTitle = (title) => {
+        for (const item of props.menuList) {
+          if (item.children?.length > 0) {
+            const child = item.children.find((c) => c.title === title);
+            if (child) return child;
+          } else if (item.title === title) {
+            return item;
+          }
+        }
+        return null;
+      };
+
+      // 计算默认选中的菜单项
+      const defaultActiveMenu = computed(() => {
+        if (props.activeMenu) {
+          const menu = findMenuByTitle(props.activeMenu);
+          if (menu) return menu;
+        }
+
+        const firstItem = props.menuList[0];
+        if (!firstItem) return null;
+
+        return firstItem.children?.length > 0
+            ? firstItem.children[0]
+            : firstItem;
+      });
+
+      const currentMenu = ref(defaultActiveMenu.value);
+
+      // 监听外部activeMenu变化
+      watch(
+          () => props.activeMenu,
+          (newTitle) => {
+            if (newTitle) {
+              const menu = findMenuByTitle(newTitle);
+              if (menu) {
+                currentMenu.value = menu;
+              }
+            }
+          }
+      );
+
+      // 选择菜单项时触发 menu-change 钩子
+      const selectItem = (item) => {
+        currentMenu.value = item;
+        emit("menu-change", item);
+      };
+
+      // 处理底部按钮点击
+      const handleFooterClick = (button, index) => {
+        emit("footer-click", { button, index });
+      };
+
+      return {
+        currentMenu,
+        selectItem,
+        handleFooterClick,
+      };
+    },
+    template: `
+          <div class="project-edit-container">
+              <div class="left-side" v-if="leftDisplay">
+              
+                  <!-- 头部图片 -->
+                  <div v-if="headerImage" class="menu-header">
+                      <div class="thumb">
+                          <img :src="headerImage" style="object-fit: cover;width: 100%;height: 100%;" />
+                      </div>
+                  </div>
+
+                  <!-- 菜单内容 -->
+                  <div class="menu-content">
+                      <div class="scroll-view">
+                          <template v-for="(menuItem, i) in menuList" :key="i">
+                              <!-- 分组菜单 -->
+                              <div v-if="menuItem.children?.length > 0" class="group">
+                                  <div class="menu-item" @click="menuItem.open = !menuItem.open">
+                                      <span>{{ menuItem.title }}</span>
+                                      
+                                      <span class="arrow">
+                                          <ss-icon :name="menuItem.open ? 'arrow-up' : 'arrow-down'" size="20px" />
+                                      </span>
+                                  </div>
+                                  <div v-show="menuItem.open" class="group-detail">
+                                      <div v-for="(item, j) in menuItem.children" 
+                                          :key="j" 
+                                          class="menu-item"
+                                          :class="{ active: item.name === currentMenu?.name }"
+                                          @click="selectItem(item)"
+                                      >
+                                          {{ item.title }}
+                                           
+                                           &nbsp;
+                                          <span class="menu-item-point" v-if="item.cgxList || item.objectList"></span>
+                                      </div>
+                                  </div>
+                              </div>
+                              <!-- 普通菜单项 -->
+                              <div v-else 
+                                  class="menu-item"
+                                  :class="{ active: menuItem.name === currentMenu?.name }"
+                                  @click="selectItem(menuItem)"
+                              >
+                                    {{ menuItem.title }}
+                                   &nbsp;
+                                  <span class="menu-item-point" v-if="menuItem.cgxList || menuItem.objectList"></span>
+                              </div>
+                          </template>
+                      </div>
+                  </div>
+                  
+                  <!-- 底部按钮 -->
+                  <div v-if="footerButtons.length > 0" 
+                      class="sub-tab-menu-footer"
+                      @click="footerButtons[0].onclick"
+                  >
+                      <div>{{ footerButtons[0].text }}</div>
+                      <ss-icon 
+                          v-if="footerButtons.length > 1" 
+                          name="arrow-up" 
+                          size="24px" 
+                      />
+                      
+                      <!-- 悬浮按钮 -->
+                      <div v-if="footerButtons.length > 1" class="sub-tab-menu-popup">
+                          <div v-for="(button, index) in footerButtons.slice(1)" 
+                              :key="index"
+                              @click.stop="button.onclick"
+                          >
+                              {{ button.text }}
+                          </div>
+                      </div>
+                  </div>
+                  
+              </div>
+
+              <!-- 右侧内容区域 -->
+              <div class="content-area fit-height-content" style="overflow: hidden;" :style="!leftDisplay ? { width: '100%' } : {}">
+                 
+                  <template v-for="(menuItem, i) in menuList" :key="i">
+                      <iframe 
+                          :src="menuItem.url"
+                          style="height: 100%;width: 100%;"
+                          frameborder="0"
+                          class="sub-tab-iframe"
+                          :id="i === 0 ? 'sub-tab-iframe' : ''"
+                          v-show="currentMenu?.name === menuItem.name"
+                      />
+                  </template>
+              </div>
+          </div>
+      `,
+  };
+  // <iframe
+  //     v-if="currentMenu?.url"
+  //     :src="currentMenu.url"
+  //     style="height: 100%;width: 100%;"
+  //     frameborder="0"
+  //     id="sub-tab-iframe"
+  // />
+
+  // ss-photo-upload 通用图片上传组件
+  const SsImgUpload = {
+    name: "SsImgUpload",
+    props: {
+      name: {
+        type: String,
+        required: true,
+      },
+      // 图片URL,用于回显
+      // url: {
+      //   type: String,
+      //   default: "",
+      // },
+      // 样式类名
+      class: {
+        type: String,
+        required: true,
+      },
+      // 裁剪配置
+      cropperOpt: {
+        type: Object,
+        default: () => ({
+          width: 360,
+          height: 360,
+          aspectRatio: 1,
+        }),
+      },
+      //上传图片url(未加图片名参数之前的部分)
+      ulUrl: {
+        type: String,
+        required: true,
+      },
+      //下载图片url(未加图片名参数之前的部分)
+      dlUrl: {
+        type: String,
+        required: true,
+      },
+      modelValue: [String, Number],
+    },
+
+    emits: ["update:modelValue"],
+
+    setup(props, { emit }) {
+      const inputId = Vue.computed(
+          () => `file_${Vue.getCurrentInstance().uid}`
+      );
+      //修改图片初始显示路径
+      let pathVal = ref(props.modelValue);
+      let picUrl = ref("");
+      if (props.modelValue) {
+        picUrl.value = props.dlUrl + "&path=" + props.modelValue;
+      }
+
+      Vue.onMounted(() => {
+        window.SS.cropper.init({
+          el: $(`#${inputId.value}`),
+          photoSize: {
+            width: props.cropperOpt.width,
+            height: props.cropperOpt.height,
+          },
+          aspectRatio: props.cropperOpt.aspectRatio,
+          uploadUrl: props.ulUrl,
+          success: (path) => {
+            pathVal.value = path;
+            picUrl.value = props.dlUrl + "&path=" + path;
+            emit("update:modelValue", path);
+          },
+        });
+      });
+
+      return () =>
+          h("div", { class: [props.class] }, [
+            h("input", {
+              type: "file",
+              accept: "image/*",
+              id: inputId.value,
+              style: { display: "none" },
+            }),
+            h("input", {
+              type: "hidden",
+              name: props.name,
+              value: pathVal.value,
+            }),
+            h(
+                "div",
+                {
+                  style: {
+                    width: "100%",
+                    height: "100%",
+                  },
+                  onClick: () => $(`#${inputId.value}`).click(),
+                },
+                [
+                  picUrl.value &&
+                  h("img", {
+                    src: picUrl.value,
+                    style:
+                        "width: 100%; height: 100%;object-fit: inherit;position: relative;z-index: 11;",
+                  }),
+                ]
+            ),
+          ]);
+    },
+  };
+
+  // 初始化函数,负责创建和挂载 Vue 应用
+  // window.SS = { dom: {} };
+  /**
+   * 获取当前窗口的父窗口
+   * @returns {Window} 父窗口对象
+   */
+  window.SS.topWin = (function (p, c) {
+    while (p != c) {
+      c = p;
+      p = p.parent;
+    }
+    return c;
+  })(window.parent, window);
+
+  window.SS.createSsDialogInstance = createSsDialogInstance;
+  /**
+   * 创建弹窗
+   * @param {Object} setting
+   * @param {Function} callbackEvent
+   */
+  window.SS.openDialog = function (setting, callbackEvent) {
+    if (setting.params) {
+      const encodedParams = encodeURIComponent(JSON.stringify(setting.params));
+      setting.src +=
+          (setting.src.includes("?") ? "&" : "?") + "params=" + encodedParams;
+    }
+
+    if (window.parent && window.parent !== window) {
+      window.parent.SS.createSsDialogInstance(setting, callbackEvent);
+    } else {
+      createSsDialogInstance(setting, callbackEvent);
+    }
+  };
+  //关闭弹窗
+  window.SS.closeDialog = function () {
+    console.log("关闭弹窗");
+    if (topWindow.dialogInstances.length > 0) {
+      const instance = topWindow.dialogInstances.pop();
+      console.log("instance", instance);
+      console.log("instance.callbackEvent", instance.callbackEvent);
+      console.log(
+          "instance.callbackEvent.end",
+          typeof instance.callbackEvent === "function"
+      );
+      if (instance.callbackEvent) {
+        // 判断是否有end回调并执行
+        if (typeof instance.callbackEvent === "function") {
+          instance.callbackEvent();
+        }
+        if (typeof instance.callbackEvent.end === "function") {
+          instance.callbackEvent.end();
+        }
+      }
+      instance.app.unmount(); // 卸载最后一个实例
+      if (instance.container && instance.container.parentNode) {
+        instance.container.parentNode.removeChild(instance.container); // 移除容器
+      }
+    }
+  };
+
+  /**
+   * 裁剪插件
+   */
+  window.SS.cropper = {
+    init: function (setting) {
+      if (!window.top.SS) window.top.SS = {};
+      // 重要:确保 cropper 对象的完整初始化
+      if (!window.top.SS.cropper) {
+        window.top.SS.cropper = {
+          settings: new Map(),
+          _backupSettings: {},
+          getSetting: this.getSetting,
+          clearSetting: this.clearSetting,
+          debug: this.debug,
+        };
+      } else if (!window.top.SS.cropper.settings) {
+        // 如果 cropper 存在但 settings 不存在,重新初始化 settings
+        window.top.SS.cropper.settings = new Map();
+        window.top.SS.cropper._backupSettings = {};
+      }
+      const uploaderId = `uploader_${Date.now()}_${Math.random()
+          .toString(36)
+          .substr(2, 9)}`;
+
+      window.top.SS.cropper.settings.set(uploaderId, setting);
+      window.top.SS.cropper._backupSettings[uploaderId] = setting;
+
+      setting.box = setting.box || "1";
+      var winSetting = {
+        headerTitle: "图片裁剪",
+        src: "/js/cropper/cropper.jsp",//原来在"/newUI/page/cropper.jsp" Ben(20251205)
+        width: "900",
+        height: "500",
+      };
+
+      $(setting.el).change(function () {
+        if (!window.SS.cropper.verify(setting)) {
+          $(setting.el).val(""); // 清空文件选择
+          return false;
+        }
+        var files = this.files;
+        if (files && files.length) {
+          if (!window.SS.cropper.verifySize($(setting.el)[0], 5)) {
+            $(setting.el).val(""); // 清空文件选择
+            alert("文件大小不能超过5M,请重新选择");
+            return false;
+          }
+          var URL = window.URL || window.webkitURL;
+          var file = files[0];
+          setting.file = file;
+
+          if (
+              /^image\/\w+$/.test(file.type) &&
+              /\.(jpg|jpeg|png|)$/i.test(file.name)
+          ) {
+            var uploadedImageURL = URL.createObjectURL(file);
+            setting.data = uploadedImageURL;
+            setting.fileName = file.name;
+            // console.log()
+            winSetting.params = {
+              ...setting,
+              uploaderId,
+            };
+            console.log("ss-componets中change之后的winSetting", winSetting);
+            SS.openDialog(winSetting, {
+              success: function (win) {
+                console.log("裁剪插件成功");
+                // win.cropperSetting = setting;
+              },
+              end: function () {
+                console.log("裁剪插件结束");
+                $(setting.el).val(""); // 清空文件选择
+              },
+            });
+          } else {
+            alert("请选择图片文件,支持jpg、jpeg、png格式");
+          }
+        }
+      });
+      return uploaderId;
+    },
+    verify: function (setting) {
+      if (!setting) {
+        console.error(" cropper setting is not undefined! ");
+        return false;
+      }
+      if (!setting.el) {
+        console.error(" cropper setting.el is not undefined! ");
+        return false;
+      }
+      if (setting.photoSize) {
+        if (
+            (!setting.photoSize.width && setting.photoSize.height) ||
+            (!setting.photoSize.height && setting.photoSize.width)
+        ) {
+          console.error(
+              " cropper setting.photoSize { width, height } is not undefined! "
+          );
+          return false;
+        }
+      }
+      if (!setting.box) {
+        setting.box = "1";
+      }
+
+      if (!setting.aspectRatio) {
+        setting.aspectRatio = 1 / 1;
+      }
+
+      return true;
+    },
+    verifySize: function (fileEl, maxSize) {
+      // 判断是否为IE浏览器: /msie/i.test(navigator.userAgent) 为一个简单正则
+      var isIE = /msie/i.test(navigator.userAgent) && !window.opera;
+      var fileSize = 0;
+      if (isIE && !fileEl.files) {
+        // IE浏览器
+        var filePath = fileEl.value; // 获得上传文件的绝对路径
+        var fileSystem = new ActiveXObject("Scripting.FileSystemObject");
+        var file = fileSystem.GetFile(filePath);
+        fileSize = file.Size; // 文件大小,单位:b
+      } else {
+        // 非IE浏览器
+        fileSize = fileEl.files[0].size;
+      }
+
+      var size = fileSize / 1024 / 1024;
+      return !(size > maxSize);
+    },
+    // 获取特定上传组件的setting
+    getSetting: function (uploaderId) {
+      if (!window.top.SS?.cropper) {
+        console.warn("顶层窗口中未找到 SS.cropper");
+        return null;
+      }
+
+      // 优先从 Map 中获取
+      let setting = window.top.SS.cropper.settings.get(uploaderId);
+
+      // 如果 Map 中没有,尝试从备份中获取
+      if (!setting && window.top.SS.cropper._backupSettings[uploaderId]) {
+        console.log("从备份中恢复 setting");
+        setting = window.top.SS.cropper._backupSettings[uploaderId];
+        // 恢复到 Map 中
+        window.top.SS.cropper.settings.set(uploaderId, setting);
+      }
+
+      return setting;
+    },
+
+    // 清理特定上传组件的setting
+    clearSetting: function (uploaderId) {
+      if (!window.top.SS?.cropper) return;
+
+      window.top.SS.cropper.settings.delete(uploaderId);
+      delete window.top.SS.cropper._backupSettings[uploaderId];
+      console.log(
+          "清理设置后的 Map size:",
+          window.top.SS.cropper.settings.size
+      );
+    },
+  };
+
+  /**
+   * 获取url中的参数
+   * @returns {Object}
+   */
+  window.SS.getQueryParams = function () {
+    const params = {};
+    const queryString = window.location.search.substring(1);
+    const pairs = queryString.split("&");
+    for (let i = 0; i < pairs.length; i++) {
+      const pair = pairs[i].split("=");
+      params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1]);
+    }
+    if (params.params) {
+      try {
+        params.params = JSON.parse(params.params);
+      } catch (e) {
+        console.error("Failed to parse params:", e);
+      }
+    }
+
+    return params;
+  };
+
+  /**
+   * 创建vue应用
+   * @param {Object} config 配置项
+   * @values {String} config.el 挂载的元素
+   * @values {Boolean} config.isDialogPage 是否是弹窗页面 决定了是否可以使用顶天立地
+   * @values {Object} config.vueOptions vue配置项
+   * @returns {Object} vue实例
+   */
+  window.SS.dom.initializeFormApp = function (config) {
+    const { el, isDialogPage = false, ...vueOptions } = config;
+    const app = createApp({
+      ...vueOptions,
+    });
+    // 如果是弹窗iframe里面的html的话 给当前的页面挂上事件 实现顶天立地的效果
+    if (isDialogPage) {
+      function checkScroll() {
+        // 选出所有fit-height-content的元素 如果有滚动条
+        const elements = document.querySelectorAll(".fit-height-content");
+        let hasScrollBar = false;
+        // 检查元素是否有滚动条 检查当前窗口的所有元素
+        // 如果有滚动条,则将结果设置为true
+        elements.forEach((el) => {
+          if (el.scrollHeight > el.clientHeight) {
+            hasScrollBar = true;
+          }
+        });
+        // 将结果发送给父窗口
+        window.parent.postMessage({ hasScrollBar }, "*");
+      }
+
+      function addScrollListeners() {
+        const elements = document.querySelectorAll("div");
+        elements.forEach((el) => {
+          el.addEventListener("scroll", checkScroll);
+        });
+      }
+      const observer = new MutationObserver((mutations) => {
+        addScrollListeners();
+        checkScroll();
+      });
+
+      observer.observe(document.body, {
+        childList: true,
+        subtree: true,
+      });
+
+      window.addEventListener("resize", checkScroll);
+    }
+    // 注册组件
+    app.component("SsLoginIcon", SsLoginIcon);
+    app.component("SsMark", SsMark);
+    app.component("SsFullStyleHeader", SsFullStyleHeader);
+    app.component("SsDialog", SsDialog);
+    app.component("SsInput", SsInput);
+    app.component("SsObjp", SsObjp);
+    app.component("SsHidden", SsHidden);
+    app.component("SsCcp", SsCcp);
+    app.component("SsDatePicker", SsDatePicker);
+    app.component("SsIcon", SsIcon);
+    app.component("SsCommonIcon", SsCommonIcon);
+    app.component("SsBreadcrumb", SsBreadcrumb);
+    app.component("SsEditor", SsEditor);
+    app.component("SsDialogIcon", SsDialogIcon);
+    app.component("SsBottomButton", SsBottomButton);
+    app.component("SsNavIcon", SsNavIcon);
+    app.component("SsHeaderIcon", SsHeaderIcon);
+    app.component("SsGolbalMenuIcon", SsGolbalMenuIcon);
+    app.component("SsCartListIcon", SsCartListIcon);
+    app.component("SsQuickIcon", SsQuickIcon);
+    app.component("SsFormIcon", SsFormIcon);
+    app.component("SsBottomDivIcon", SsBottomDivIcon);
+    app.component("SsEditorIcon", SsEditorIcon);
+    app.component("SsValidate", SsValidate);
+    app.component("SsOnoff", Ssonoff);
+    app.component("SsonoffArray", SsonoffArray);
+    app.component("SsTextarea", SsTextarea);
+    app.component("SsLoginInput", SsLoginInput);
+    app.component("SsLoginButton", SsLoginButton);
+    app.component("SsSearch", SsSearch);
+    app.component("SsCartItem", SsCartItem);
+    app.component("SsCartItem2", SsCartItem2);
+    app.component("SsListCard", SsListCard);
+    app.component("SsFolderCard", SsFolderCard);
+    app.component("SsFolderCartView", SsFolderCartView);
+    app.component("SsPage", SsPage);
+    app.component("SsRightInfo", SSRightInfo);
+    app.component("SsSuccessPopup", SsSuccessPopup);
+    app.component("SsErrorDialog", SsErrorDialog);
+    app.component("SsVerify", SsVerify);
+    app.component("SsVerifyNode", SsVerifyNode);
+    app.component("SsOrcImgBox", SsOrcImgBox);
+    app.component("ss-search-input", SsSearchInput);
+    app.component("ss-search-date-picker", SsSearchDatePicker);
+
+    app.component("ss-search-button", SsSearchButton);
+    app.component("ss-drop-button", SsDropButton);
+    app.component("ss-sub-tab", SsSubTab);
+    app.component("ss-img", SsImgUpload);
+    // 设置为中文
+    app.use(ElementPlus, {
+      locale: ElementPlusLocaleZhCn,
+    });
+    // console.log(ElementPlus);
+    // 确保 ElementPlusIconsVue
+    // if (window.ElementPlusIconsVue) {
+    //   // 注册 Element Plus 图标组件
+    //   for (const [key, component] of Object.entries(
+    //     window.ElementPlusIconsVue
+    //   )) {
+    //     console.log(key, component);
+    //     app.component(key, component);
+    //   }
+    // }
+    // 挂载首页的组件
+    for (const componentName in IndexComponents) {
+      app.component(componentName, IndexComponents[componentName]);
+    }
+    // 挂载echarts的组件
+    for (const componentName in EchartComponents) {
+      app.component(componentName, EchartComponents[componentName]);
+    }
+    // 挂载 Vue 应用
+    const vm = app.mount(el);
+    vm.data = vueOptions.data();
+
+    return vm;
+  };
+})();

+ 316 - 0
js/vue/ss-echarts-compnents.js

@@ -0,0 +1,316 @@
+// import { isNum, toStyleStr } from "../lib/tools.js";
+// import {eventBus,EVEN_VAR} from "../lib/EventBus.js";、
+import { debounce } from "./tools.js";
+export const MoneyStatistics = {
+    name: 'MoneyStatistics',
+    setup() {
+        // 初始化响应式引用
+        const chartInstance = Vue.ref(null);
+        const chartOption = {
+            xAxis: {
+                type: "category",
+                data: [
+                    "1万以下",
+                    "1-10万",
+                    "10-50万",
+                    "50-100万",
+                    "100-300万",
+                    "300-500万",
+                    "500-1000万",
+                    "1000-3000万",
+                    "3000万以上",
+                ],
+            },
+            yAxis: {
+                type: "value",
+            },
+            series: [
+                {
+                    data: [6, 12, 18, 23, 25, 30, 12, 7, 3],
+                    type: "bar",
+                },
+            ],
+        };
+        
+        const win = Vue.ref({ width: window.innerWidth });  // 假设宽度是浏览器窗口宽度
+        const domStyle = Vue.computed(() => {
+            const domWidth = (win.value.width - 140) / 2;
+            const initSize = { width: 890, height: 378 };
+            // 根据窗口大小调整宽高
+            if (domWidth < initSize.width / 2 || domWidth > initSize.width) {
+                return {
+                    width: initSize.width + "px",
+                    height: initSize.height + "px",
+                };
+            } else {
+                return {
+                    width: domWidth + "px",
+                    height: (domWidth * initSize.height) / initSize.width + "px",
+                };
+            }
+        });
+
+        // 初始化图表和调整大小
+        const initChart = () => {
+            const element = document.getElementById("statistics-chart");
+            if (element) {
+                chartInstance.value = echarts.init(element);
+                chartInstance.value.setOption(chartOption);
+            }
+        };
+        const resizeChart = debounce(() => chartInstance.value?.resize?.(), 80);
+
+        return {
+            domStyle,
+            initChart,
+            resizeChart,
+            onSetting: () => console.log("点击了设置按钮"),
+            onRefresh: () => console.log("点击了刷新按钮"),
+        };
+    },
+    mounted() {
+        this.initChart();
+    },
+    render() {
+        const HeaderContainer = Vue.resolveComponent('header-container');
+        
+        return Vue.h('div', { class: 'statistics-container', style: this.domStyle }, [
+            Vue.h('div', { class: 'header' }, [
+                Vue.h(HeaderContainer, { title: "金额分布", onSetting: this.onSetting, onRefresh: this.onRefresh })
+            ]),
+            Vue.h('div', { class: 'body' }, [
+                Vue.h('div', { id: "statistics-chart", class: 'chart-container' })
+            ])
+        ]);
+    }
+};
+export const DepartmentStatistics = {
+    name: 'DepartmentStatistics',
+    setup() {
+        const chartInstance = Vue.ref(null);
+        const chartOption = Vue.computed(() => ({
+            title: {
+                text: "Referer of a Website",
+                subtext: "Fake Data",
+                left: "center",
+            },
+            tooltip: {
+                trigger: "item",
+            },
+            legend: {
+                orient: "vertical",
+                left: "left",
+            },
+            series: [
+                {
+                    name: "Access From",
+                    type: "pie",
+                    radius: "50%",
+                    data: [
+                        { value: 1048, name: "Search Engine" },
+                        { value: 735, name: "Direct" },
+                        { value: 580, name: "Email" },
+                        { value: 484, name: "Union Ads" },
+                        { value: 300, name: "Video Ads" }
+                    ],
+                    emphasis: {
+                        itemStyle: {
+                            shadowBlur: 10,
+                            shadowOffsetX: 0,
+                            shadowColor: "rgba(0, 0, 0, 0.5)"
+                        }
+                    }
+                }
+            ]
+        }));
+
+        const win = Vue.ref({ width: window.innerWidth });
+        const domStyle = Vue.computed(() => {
+            const domWidth = (win.value.width - 140) / 2;
+            const initSize = { width: 890, height: 378 };
+            if (domWidth < initSize.width / 2 || domWidth > initSize.width) {
+                return {
+                    width: initSize.width + "px",
+                    height: initSize.height + "px",
+                };
+            } else {
+                return {
+                    width: domWidth + "px",
+                    height: (domWidth * initSize.height) / initSize.width + "px",
+                };
+            }
+        });
+        const initChart = () => {
+            const element = document.getElementById("statistics-chart-department");
+            if (element) {
+                chartInstance.value = echarts.init(element);
+                chartInstance.value.setOption(chartOption.value);
+            }
+        };
+        const resizeChart = debounce(() => chartInstance.value?.resize?.(), 80);
+
+        return {
+            domStyle,
+            initChart,
+            resizeChart,
+            onSetting: () => console.log("点击了设置按钮"),
+            onRefresh: () => console.log("点击了刷新按钮"),
+        };
+    },
+    mounted() {
+        this.initChart();
+    },
+    render() {
+        const HeaderContainer = Vue.resolveComponent('header-container');
+        return Vue.h('div', { class: 'statistics-container', style: this.domStyle }, [
+            Vue.h('div', { class: 'header' }, [
+                Vue.h(HeaderContainer, { title: "省内项目主管部门分布", onSetting: this.onSetting, onRefresh: this.onRefresh })
+            ]),
+            Vue.h('div', { class: 'body' }, [
+                Vue.h('div', { id: "statistics-chart-department", class: 'chart-container' })
+            ])
+        ]);
+    }
+};
+export const ProjectReportStatistics = {
+    name: 'ProjectReportStatistics',
+    setup() {
+        const win = Vue.ref({ width: window.innerWidth });
+        const domStyle = Vue.computed(() => {
+            const domWidth = (win.value.width - 140) / 2;
+            const initSize = { width: 890, height: 444 };
+            if (domWidth < initSize.width / 2 || domWidth > initSize.width) {
+                return {
+                    width: initSize.width + "px",
+                    height: initSize.height + "px",
+                };
+            } else {
+                return {
+                    width: domWidth + "px",
+                    height: (domWidth * initSize.height) / initSize.width + "px",
+                };
+            }
+        });
+
+        const list = Vue.ref([
+            {
+                title: "年度项目收付款情况",
+                columns: [
+                    "客户名称", "项目", "项目状态", "负责人", "金额", "款项进度", "客户联系人", "联系电话"
+                ]
+            },
+            {
+                title: "年度项目合同情况汇总表",
+                columns: [
+                    "客户名称", "名称", "状态", "金额", "签订日期", "到期日期", "业务负责人"
+                ]
+            },
+            {
+                title: "项目省内分布报表",
+                columns: ["区域", "数量", "总金额", "项目", "客户", "状态"]
+            }
+        ]);
+
+        return {
+            domStyle,
+            list,
+            onSetting: () => console.log("点击了设置按钮"),
+            onRefresh: () => console.log("点击了刷新按钮"),
+        };
+    },
+    render() {
+        const HeaderContainer = Vue.resolveComponent('header-container');
+        return Vue.h('div', { class: 'statistics-container', style: this.domStyle }, [
+            Vue.h('div', { class: 'header' }, [
+                Vue.h(HeaderContainer, { title: "项目情况报表", onSetting: this.onSetting, onRefresh: this.onRefresh })
+            ]),
+            Vue.h('div', { class: 'body' }, this.list.map((item, i) =>
+                Vue.h('div', { key: i, class: 'table-container' }, [
+                    Vue.h('div', { class: 'title' }, item.title),
+                    Vue.h('div', { class: 'table-columns' }, item.columns.map((tableItem, j) =>
+                        Vue.h('div', { key: j, class: 'table-item' }, tableItem)
+                    ))
+                ])
+            ))
+        ]);
+    }
+};
+
+export const IndustryStatistics = {
+    name: 'IndustryStatistics',
+    setup() {
+        // Initialize reactive references
+        const win = Vue.ref({ width: window.innerWidth });
+        const chartInstance = Vue.ref(null);
+        const domStyle = Vue.computed(() => {
+            const domWidth = (win.value.width - 140) / 2;
+            const initSize = { width: 890, height: 444 };
+            if (domWidth < initSize.width / 2 || domWidth > initSize.width) {
+                return {
+                    width: initSize.width + "px",
+                    height: initSize.height + "px",
+                };
+            } else {
+                return {
+                    width: domWidth + "px",
+                    height: (domWidth * initSize.height) / initSize.width + "px",
+                };
+            }
+        });
+
+        const chartOption = {
+            xAxis: {
+                type: "category",
+                data: [
+                    "1万以下", "1-10万", "10-50万", "50-100万", "100-300万",
+                    "300-500万", "500-1000万", "1000-3000万", "3000万以上"
+                ]
+            },
+            yAxis: {
+                type: "value"
+            },
+            series: [{
+                data: [6, 12, 18, 23, 25, 30, 12, 7, 3],
+                type: "bar"
+            }]
+        };
+
+        const initChart = () => {
+            chartInstance.value = echarts.init(document.getElementById("statistics-chart-industry"));
+            chartInstance.value.setOption(chartOption);
+        };
+
+        const onSetting = () => console.log("Clicked on settings button");
+        const onRefresh = () => console.log("Clicked on refresh button");
+
+        const resizeChart = debounce(() => {
+            chartInstance.value?.resize();
+        }, 80);
+
+        Vue.onMounted(() => {
+            initChart();
+            window.addEventListener('resize', resizeChart);
+        });
+
+        Vue.onUnmounted(() => {
+            window.removeEventListener('resize', resizeChart);
+        });
+
+        return {
+            domStyle,
+            onSetting,
+            onRefresh,
+        };
+    },
+    render() {
+        const HeaderContainer = Vue.resolveComponent('header-container');
+        return Vue.h('div', { class: 'statistics-container', style: this.domStyle }, [
+            Vue.h('div', { class: 'header' }, [
+                Vue.h(HeaderContainer, { title: "项目产业分布", onSetting: this.onSetting, onRefresh: this.onRefresh })
+            ]),
+            Vue.h('div', { class: 'body' }, [
+                Vue.h('div', { id: 'statistics-chart-industry', class: 'chart-container' })
+            ])
+        ]);
+    }
+};

+ 1156 - 0
js/vue/ss-index-components.js

@@ -0,0 +1,1156 @@
+import { isNum, toStyleStr } from "./tools.js";
+import { eventBus, EVEN_VAR } from "./EventBus.js";
+// import { debounce } from "../lib/tools.js";
+// 首页组件的名字
+const winName = {
+    launch: "launch",
+    Notice: "Notice",
+    Statistics: "Statistics",
+    TodoList: "TodoList",
+    UrgingList: "UrgingList",
+    UserInfo: "UserInfo",
+};
+// 尺寸窗口名字
+const size2Win = (name) => `${name}-${document.body.clientWidth}`;
+// 加载尺寸
+const loadWinSize = function (name) {
+    const val = localStorage.getItem(size2Win(name));
+    if (val) {
+        try {
+            return JSON.parse(val) || {};
+        } catch (err) {
+            return {};
+        }
+    } else {
+        return {};
+    }
+};
+const saveWinSize = function (name, obj) {
+    localStorage.setItem(size2Win(name), JSON.stringify(obj));
+};
+
+// 全局头部
+export const GlobalHeader = {
+    name: 'GlobalHeader',
+    props: {
+        menuData: {
+            type: Array,
+            required: true
+        },
+        iconItems: {
+            type: Array,
+            required: true
+        }
+    },
+    setup(props) {
+
+        //原值为'/newUI/skin/easy/images/logo/full-logo.png' Ben(20251205)
+        const fullLogo = Vue.ref('/skin/easy/images/logo/full-logo.png'); // 需要指定完整路径
+
+
+        const convertMenuData = (menuData) => {
+            return menuData.map(item => ({
+              id: item.id, //菜单id
+              pid: item.pid, //父菜单id
+              label: item.desc, //菜单名称
+              component: item.url, //菜单链接
+              js: item.js, //菜单要执行的js
+              class: item.icon, // 如果icon为空则使用默认class
+              itemType: item.type, //菜单 1:菜单项 2:菜单组
+            }));
+        };
+        const menuItemsNew = Vue.ref(convertMenuData(props.menuData));
+        Vue.watchEffect(() => {
+            if (props.menuData && props.menuData.length) {
+                menuItemsNew.value = convertMenuData(props.menuData);
+                console.log('Header menu items updated:', menuItemsNew.value);
+            }
+        });
+
+        
+        
+        const menuItems = Vue.ref([]);
+        const breadCrumbs = Vue.ref([
+            { label: '首页', component: '/index.html' },
+        ]);
+        const iconItems = Vue.ref(props.iconItems);
+        const onClickMenuItemsNew = (item) => {
+            console.log(item.component, item.js)
+            if (item.component && item.js) {
+                eval(item.js)
+            } else if(item.component){
+                eventBus.publish(EVEN_VAR.currentPage, item.component);
+            } else {
+                eval(item.js)
+            }
+        };
+        const onClickMenuItem = (item) => {
+            if (item.type == 'page') {
+                eventBus.publish(EVEN_VAR.currentPage, item.component);
+                breadCrumbs.value[0] = item
+            } else if (item.type == 'dialog') {
+                SS.openDialog({
+                    headerTitle: item.label,
+                    src: '.'+item.component,
+                    height: item.height,
+                    width:item.width
+                });
+            }
+        };
+        const breadCrumbClick = (item) => {
+            eventBus.publish(EVEN_VAR.currentPage, item.component);
+            breadCrumbs.value[0] = item
+        }
+        const gotoSearch = () => {
+            eventBus.publish(EVEN_VAR.showGlobalSearchDialog);
+        };
+        const SsIcon = Vue.resolveComponent('ss-icon');
+        const SsHeaderIcon = Vue.resolveComponent('ss-header-icon');
+        const SsGolbalMenuIcon = Vue.resolveComponent('ss-golbal-menu-icon');
+        // 引入搜索框
+        const SsSearch = Vue.resolveComponent('ss-search');
+        // 处理面包屑导航
+        const renderBreadcrumbs = () => {
+            const breadcrumbElements = [Vue.h(SsIcon, { class: "home-icon", name: "home", type: "common", size: "22px" })];
+            // 遍历面包屑数组,添加面包屑和分隔符
+            breadCrumbs.value.forEach((crumb, index) => {
+                // 添加面包屑链接
+                breadcrumbElements.push(
+                    Vue.h('span', { onClick: () => breadCrumbClick(crumb) }, crumb.label)
+                );
+                // 除了最后一个元素,每个面包屑后添加分隔符图标
+                if (index < breadCrumbs.value.length - 1) {
+                    breadcrumbElements.push(
+                        Vue.h(SsIcon, { class: "split-icon", size: "12px", name: "double-arrow-right", type: "common" })
+                    );
+                }
+            });
+            return breadcrumbElements;
+        };
+        return () => Vue.h('div', { class: 'block-self flex-between-center global-header-container' }, [
+            Vue.h('div', { class: 'icon-area flex-start-center' }, [
+                Vue.h('div', { class: 'logo', onClick: () => console.log('Home clicked') }, [
+                    Vue.h('div', { class: 'img' }, [
+                        Vue.h('img', { src: fullLogo.value })
+                    ]),
+                    Vue.h('div', { class: 'menu', onClick: Vue.withModifiers(() => { }, ['stop']) },
+                        // menuItems.value.map(item =>
+                        //     Vue.h('div', { onClick: () => onClickMenuItem(item) },[
+                        //         Vue.h(SsGolbalMenuIcon, { class: item.class || '' }),
+                        //         item.label
+                        //     ])
+                        // )
+                        menuItemsNew.value.map(item =>
+                            Vue.h('div', { onClick: () => onClickMenuItemsNew(item) },[
+                                Vue.h(SsGolbalMenuIcon, { class: item.class || '' }),
+                                item.label
+                            ])
+                        )
+                    )
+                ]),
+                // Vue.h('div', { class: 'bread-crumb', style: { height: "100% !important" } }, [
+                    // Vue.h('div', { class: 'content' }, renderBreadcrumbs())
+                // ])
+            ]),
+            Vue.h('div', { class: 'menu-area' },
+                Vue.h('div', { class: 'search-area' }, [
+                    Vue.h(SsSearch, { theme: 'dark', placeholder: '跨对象搜索', onClick: gotoSearch })
+                ]),
+                iconItems.value.map(icon =>
+                    Vue.h('div', { class: 'icon-item', onClick: icon.action, style: { display: icon.condition ? (icon.condition() ? 'block' : 'none') : 'block' } }, [
+                        Vue.h(SsHeaderIcon, { class: icon.class })
+                    ])
+                )
+            )
+        ]);
+    }
+};
+// 全局菜单
+export const GlobalMenu = {
+    name: 'GlobalMenu',
+    props: {
+        menuItems: {
+            type: Array,
+            required: true
+        },
+        initialSize: {
+            type: String,
+            default: 'min'
+        },
+        // onMenuClick: {
+        //     type: Function,
+        //     required: true,
+        //     default: () => {}
+        // }
+    },
+    setup(props) {
+        const menuItemsNew = Vue.ref([]);
+        
+        const activeItem = Vue.ref(''); // 默认选中第一个菜单项
+     
+        // 将后台返回的菜单数据转换为树结构
+        const convertMenuDataToTree = (menuData) => {
+            // 先转换格式
+            const formattedData = menuData.map(item => ({
+                id: item.id,
+                pid: item.pid,
+                name: item.desc,
+                component: item.url,
+                js: item.js,
+                class: item.type == 1 ? 'nav-icon-folder-close' : item.icon || 'header-home',
+                itemType: item.type,
+                children: []
+            }));
+        
+            // 创建一个映射表,方便查找
+            const map = {};
+            formattedData.forEach(item => {
+                map[item.id] = item;
+            });
+        
+            // 构建树结构
+            const treeData = [];
+            formattedData.forEach(item => {
+                if (item.pid && map[item.pid]) {
+                    // 如果有父节点,就放到父节点的children中
+                    if (!map[item.pid].children) {
+                        map[item.pid].children = [];
+                    }
+                    map[item.pid].children.push(item);
+                } else {
+                    // 没有父节点就是顶层节点
+                    treeData.push(item);
+                }
+            });
+        
+            return treeData;
+        };
+         // 添加 watchEffect 来监听 props.menuItems 的变化
+         Vue.watchEffect(() => {
+            if (props.menuItems && props.menuItems.length) {
+                menuItemsNew.value = convertMenuDataToTree(props.menuItems);
+                activeItem.value = menuItemsNew.value[0]?.name || '';
+                console.log('Menu items updated:', menuItemsNew.value);
+                if(menuItemsNew.value[0]?.component){
+                    eventBus.publish(EVEN_VAR.currentPage, menuItemsNew.value[0]?.component);
+                }else{
+                    eventBus.publish(EVEN_VAR.currentPage, menuItemsNew.value[0]?.children[0]?.component);
+                }
+
+            }
+        });
+        const SsIcon = Vue.resolveComponent('ss-icon');
+        const SsNavIcon = Vue.resolveComponent('ss-nav-icon');
+        // 移动状态控制逻辑到组件内
+        const leftSideTypeDict = {
+            min: { key: "min", icon: "arrow-double-right" },
+            max: { key: "max", icon: "arrow-double-left" },
+        };
+        
+        const leftSideType = Vue.ref(leftSideTypeDict[props.initialSize]);
+        let waitLeftSideChangeTimer = null;
+        
+        // 控制子菜单展开状态
+        const expandedMenus = Vue.ref(new Set());
+
+        const onMenuClick = (item) => {
+            console.log(item)
+            console.log(activeItem.value)
+            if(item.component && item.js){
+                eval(item.js)
+            } else if(item.component){
+                eventBus.publish(EVEN_VAR.currentPage, item.component);
+            }else{
+                eval(item.js)
+            }
+        }
+        const toggleLeftSideType = () => {
+            leftSideType.value = 
+                leftSideType.value.key === 'min' ? leftSideTypeDict.max : leftSideTypeDict.min;
+        };
+
+        const doChangeLeftSideType2Max = () => {
+            if (leftSideType.value.key === 'min') {
+                clearTimeout(waitLeftSideChangeTimer);
+                waitLeftSideChangeTimer = setTimeout(() => {
+                    leftSideType.value = leftSideTypeDict.max;
+                }, 1000);
+            }
+        };
+
+        const onLeaveLeftMenuArea = () => {
+            if (waitLeftSideChangeTimer) {
+                clearTimeout(waitLeftSideChangeTimer);
+                waitLeftSideChangeTimer = null;
+                leftSideType.value = leftSideTypeDict.min;
+            }
+        };
+
+        return () => Vue.h('div', { class: 'left-side' }, [
+            Vue.h('div', { 
+                class: 'left-side-container',
+                size: leftSideType.value.key
+            }, [
+                // 收缩按钮
+                Vue.h('div', { 
+                    class: 'btn-size',
+                    onClick: toggleLeftSideType
+                }, [
+                    Vue.h(SsIcon, {
+                        name: leftSideType.value.icon,
+                        type: 'common',
+                        size: '12px'
+                    })
+                ]),
+                // 左侧内容
+                Vue.h('div', {
+                    class: 'left-side-content',
+                    onMouseenter: doChangeLeftSideType2Max,
+                    onMousemove: doChangeLeftSideType2Max,
+                    onMouseleave: onLeaveLeftMenuArea
+                }, [
+                    // 菜单项列表
+                    ...menuItemsNew.value.map(icon => [
+                        // 父菜单项
+                        Vue.h('div', { 
+                            class:['menu-item',{active:activeItem.value === icon.name}],
+                            onClick: () => {
+                                activeItem.value = icon.name;
+                                if (icon.children.length > 0) {
+                                    if (expandedMenus.value.has(icon.name)) {
+                                        expandedMenus.value.delete(icon.name);
+                                    } else {
+                                        expandedMenus.value.add(icon.name);
+                                    }
+                                } else {
+                                    onMenuClick(icon);
+
+                                }
+                            }
+                        }, [
+                            Vue.h('div', { class: 'menu-item-content' }, [
+                                Vue.h(SsNavIcon, { 
+                                    // 如果是文件夹类型(type == 1),根据展开状态设置不同的class
+                                    class: icon.itemType == 1 ? 
+                                        (expandedMenus.value.has(icon.name) ? 'nav-icon-folder-open' : 'nav-icon-folder-close') 
+                                        : (icon.class || '')
+                                }),
+                                Vue.h('div', { class: 'menu-item-label' }, icon.name || ''),
+                                Vue.h('div', { class: 'edit-mark' }, [
+                                    Vue.h(SsIcon, { name: 'close-mark-fill', size: '36px' })
+                                ])
+                            ])
+                        ]),
+                        // 子菜单项(与父菜单项同级)
+                        ...(icon.children && expandedMenus.value.has(icon.name) ? 
+                            icon.children.map(child => 
+                                Vue.h('div', { 
+                                    class:['menu-item',{active:activeItem.value === child.name}],
+                                    onClick: (e) => {
+                                        activeItem.value = child.name;
+                                        e.stopPropagation();
+                                        onMenuClick(child);
+                                    }
+                                }, [
+                                    Vue.h('div', { class: 'menu-item-content' }, [
+                                        Vue.h('div', { class: 'icon-container' }, [  // 添加图标容器
+                                            Vue.h(SsNavIcon, { 
+                                                class: child.class || ''
+                                            }),
+                                            Vue.h('div', { class: 'menu-item-dot' })  // 小圆点放在图标容器内
+                                        ]),
+                                        Vue.h('div', { class: 'menu-item-label' }, child.name),
+                                        Vue.h('div', { class: 'edit-mark' }, [
+                                            Vue.h(SsIcon, { name: 'close-mark-fill', size: '36px' })
+                                        ])
+                                    ])
+                                ])
+                            ) : []
+                        )
+                    ]).flat(),
+                    // 添加按钮
+                    Vue.h('div', { class: 'menu-item add-menu-btn' }, [
+                        Vue.h('div', { class: 'add' }, [
+                            Vue.h(SsNavIcon, { class: 'nav-icon-add' })
+                        ])
+                    ])
+                ])
+            ])
+        ]);
+    }
+};
+// 基础组件 首页组件头部
+export const HeaderContainer = {
+    name: 'HeaderContainer',
+    props: {
+        title: String,
+        icon: String,
+    },
+    emits: ['setting', 'refresh'],
+    setup(props, { emit }) {
+        const onSetting = () => {
+            emit('setting');
+        };
+        const onRefresh = () => {
+            emit('refresh');
+        };
+        return {
+            props,
+            onSetting,
+            onRefresh
+        };
+    },
+    render() {
+        const SsIcon = Vue.resolveComponent('ss-icon');
+       
+        return Vue.h('div', { class: 'edit-box-header-container' }, [
+            Vue.h('div', { class: ['title', { visibility: !!(this.props.icon || this.props.title) }] }, [
+                this.props.icon ? Vue.h('div', { class: 'icon', onClick: this.onRefresh }, [
+                    Vue.h(SsIcon, { class: 'normal', name: this.props.icon, size: '22px' }),
+                    Vue.h(SsIcon, { class: 'hover', name: 'refresh', size: '22px' })
+                ]) : null,
+                Vue.h('div', this.props.title)
+            ]),
+            Vue.h('div', { class: 'handle-bar' }, [
+                Vue.h('div', { class: 'left-bar' }), // Assuming left-bar is empty
+                Vue.h('div', { class: 'setting', onClick: this.onSetting }, [
+                    Vue.h(SsIcon, { name: 'setting', size: '22px' })
+                ])
+            ])
+        ]);
+    }
+};
+// 基础组件 首页组件边框
+export const EditBox = {
+    name: 'EditBox',
+    setup(props, { emit, slots }) {
+        const mousePos = Vue.reactive({
+            rightCenter: "right-center",
+            rightBottom: "right-bottom",
+            bottomCenter: "bottom-center",
+        });
+        const curActionMousePos = Vue.ref("");
+        const editBoxContainer = Vue.ref(null);
+        const onMousemove = (e) => {
+            if (e.buttons === 1 && curActionMousePos.value) {
+                const dom = editBoxContainer.value;
+                if (dom) {
+                    const zoom = Number(document.body.style.zoom || 1);
+                    const winRect = dom.getBoundingClientRect();
+                    const { x: xSource, y: ySource } = e;
+                    const x = xSource / zoom;
+                    const y = ySource / zoom;
+                    const val = {
+                        left: winRect.left,
+                        top: winRect.top,
+                        width: winRect.width,
+                        height: winRect.height,
+                    };
+
+                    const toHObj = (source) => ({
+                        ...source,
+                        height: Math.abs(y - source.top),
+                        top: y >= 0 ? source.top : source.top + (y - source.top),
+                    });
+
+                    const toWObj = (source) => ({
+                        ...source,
+                        width: Math.abs(x - source.left),
+                        left: x >= 0 ? source.left : source.left + (x - source.left),
+                    });
+
+                    if (curActionMousePos.value === mousePos.bottomCenter) {
+                        emit("size", toHObj(val));
+                    } else if (curActionMousePos.value === mousePos.rightCenter) {
+                        emit("size", toWObj(val));
+                    } else if (curActionMousePos.value === mousePos.rightBottom) {
+                        emit("size", toWObj(toHObj(val)));
+                    }
+                }
+            }
+        };
+        const onMouseup = () => {
+            curActionMousePos.value = "";
+        };
+        const onMouseDown = (pos) => {
+            console.log(pos)
+            curActionMousePos.value = pos;
+        };
+        Vue.onMounted(() => {
+            document.body.addEventListener("mousemove", onMousemove);
+            document.body.addEventListener("mouseup", onMouseup);
+        });
+        Vue.onUnmounted(() => {
+            document.body.removeEventListener("mousemove", onMousemove);
+            document.body.removeEventListener("mouseup", onMouseup);
+        });
+        const SsIcon = Vue.resolveComponent('ss-icon');
+        return () => Vue.h('div', { class: 'edit-box-container', ref: editBoxContainer }, [
+            Vue.h('div', { class: ['edit-tools', { active: !!curActionMousePos.value }] }, [
+                Vue.h('div', { class: 'close' }, [
+                    Vue.h(SsIcon, { name: "close-mark-fill", size: "36px" })
+                ]),
+                Vue.h('div', { class: 'right-center', onMousedown: () => onMouseDown(mousePos.rightCenter) }, [
+                    Vue.h('div', { class: 'icon' }, [
+                        Vue.h(SsIcon, { name: "resize", size: "30px" })
+                    ])
+                ]),
+                Vue.h('div', { class: 'right-bottom', onMousedown: () => onMouseDown(mousePos.rightBottom) }, [
+                    Vue.h('div', { class: 'icon' }, [
+                        Vue.h(SsIcon, { name: "resize", size: "30px" })
+                    ])
+                ]),
+                Vue.h('div', { class: 'bottom-center', onMousedown: () => onMouseDown(mousePos.bottomCenter) }, [
+                    Vue.h('div', { class: 'icon' }, [
+                        Vue.h(SsIcon, { name: "resize", size: "30px" })
+                    ])
+                ])
+            ]),
+            Vue.h('div', { class: 'content-area' }, slots.default ? slots.default() : [])
+        ]);
+    }
+};
+// 基础组件 头像
+export const Avatar = {
+    name: 'Avatar',
+    props: {
+        url: {
+            type: String,
+            required: true
+        },
+        size: {
+            type: [String, Number],
+            default: 50
+        },
+        unit: {
+            type: String,
+            default: 'px'
+        },
+        shape: {
+            validator: function (value) {
+                return ['circle', 'round'].includes(value);
+            },
+            default: 'circle'
+        }
+    },
+    setup(props) {
+        // 计算属性,转换尺寸并添加单位
+        const style = Vue.computed(() => {
+            const addUnit = (n) => isNum(n) ? `${n}${props.unit}` : (n || '');
+            const styleObj = {
+                width: addUnit(props.size),
+                height: addUnit(props.size),
+            };
+            return toStyleStr(styleObj);
+        });
+
+        // 返回渲染函数所需要的数据
+        return {
+            props,
+            style
+        };
+    },
+    render() {
+        return Vue.h('img', {
+            src: this.props.url,
+            style: this.style.value,
+            class: ['avatar-container', this.props.shape]
+        });
+    }
+};
+
+// 基础组件 文件夹或者文件的组件
+export const FolderContainer = {
+    name: 'FolderContainer',
+    props: {
+        list: {
+            type: Array,
+            required: true,
+        },
+        draggable: {
+            type: Boolean,
+            default: false,
+        },
+    },
+    setup(props, { emit }) {
+        const toggleFolder = (index, isFolder) => {
+            const item = props.list[index];
+            if (isFolder) {
+                item.open = !item.open;
+            }
+        };
+
+        const onClickItem = (item) => {
+            emit('click', item);
+        };
+
+        const onItemStartDrag = (e, item) => {
+            e.dataTransfer.setData("text/plain", JSON.stringify(item));
+            console.log("开始拖拽的对象是=>", { item });
+        };
+
+        const onItemDrop = (e, item, pre) => {
+            var data = e.dataTransfer.getData("text/plain");
+            try {
+                const obj = JSON.parse(data);
+                item.children.push(obj);
+                console.log("目标对象=>", { e, item, pre, obj });
+            } catch (err) {
+                console.log("拖拽的节点不正确", { item, err });
+            }
+        };
+
+        return {
+            toggleFolder,
+            onClickItem,
+            onItemStartDrag,
+            onItemDrop,
+        };
+    },
+    render() {
+        const SsIcon = Vue.resolveComponent('ss-icon');
+        return Vue.h('div', { class: 'folder-container' }, this.list.map((groupItem, i) => {
+            return Vue.h('div', { class: 'group-item', key: i }, [
+                Vue.h('div', {
+                    class: ['group-title', { active: !!groupItem.curActionMousePos }],
+                    onDrop: $event => this.onItemDrop($event, groupItem),
+                    onDragover: $event => $event.preventDefault(),
+                }, [
+                    groupItem.type === 'folder' ? Vue.h('div', {
+                        class: 'folder',
+                        onClick: $event => this.toggleFolder(i, groupItem.type === 'folder'),
+                        'data-num': groupItem.children.length,
+                    }, [
+                        groupItem.open ? Vue.h(SsIcon, { name: 'folder-expand-fill' }) : Vue.h(SsIcon, { name: 'folder-collapse-fill' })
+                    ]) : groupItem.type === 'file' ? Vue.h(SsIcon, { name: 'file' }) : null,
+                    Vue.h('div', groupItem.title)
+                ]),
+                groupItem.children.length > 0 && groupItem.open ? Vue.h('ul', {
+                    class: 'group-childs',
+                    onDrop: $event => this.onItemDrop($event, groupItem),
+                    onDragover: $event => $event.preventDefault(),
+                }, groupItem.children.map((item, j) => {
+                    return Vue.h('li', {
+                        key: j,
+                        draggable: this.draggable,
+                        onDragstart: $event => this.onItemStartDrag($event, item),
+                        onDrop: $event => this.onItemDrop($event, groupItem, item),
+                        onDragover: $event => $event.preventDefault(),
+                        onClick: () => this.onClickItem(item),
+                    }, [
+                        Vue.h('div', item.title),
+                        Vue.h('div', item.time)
+                    ]);
+                })) : null
+            ]);
+        }));
+    }
+};
+// 个人卡片
+export const UserInfo = {
+    name: 'UserInfo',
+    props: {
+        obj: {
+            type: Object,
+            default: () => ({})
+        }
+    },
+    setup(props, { emit }) {
+        const winInfo = Vue.reactive(loadWinSize(winName.UserInfo));
+        const onSetting = () => {
+            console.log("点击了设置按钮");
+        };
+        const onRefresh = () => {
+            console.log("点击了刷新按钮");
+        };
+        const onSizeChange = (rect) => {
+
+            const style = {};
+            for (const key in rect) {
+                style[key] = rect[key] + 'px';
+            }
+            saveWinSize(winName.UserInfo, style);
+            Object.assign(winInfo, style);
+        };
+        return {
+            winInfo,
+            onSetting,
+            onRefresh,
+            onSizeChange,
+            props
+        };
+    },
+    render() {
+        const SsIcon = Vue.resolveComponent('ss-icon');
+        return Vue.h('div', { class: 'user-info-container can-resize-box', style: this.winInfo },
+            Vue.h(EditBox, {
+                onSize: this.onSizeChange
+            }, {
+                default: () => [
+                    Vue.h('div', { class: 'header' },
+                        Vue.h(HeaderContainer, {
+                            onSetting: this.onSetting,
+                            onRefresh: this.onRefresh
+                        })
+                    ),
+                    Vue.h('div', { class: 'body' }, [
+                        Vue.h('div', { class: 'user-info', style: "margin-bottom: 18px" }, [
+                            Vue.h('div', { class: 'avatar' },
+                                Vue.h(Avatar, {
+                                    url: '../images/example/user-avatar.png',
+                                    size: '90px'
+                                })
+                            ),
+                            Vue.h('div', { class: 'info' }, [
+                                Vue.h('p', this.props.obj.name + ',上午好!'),
+                                Vue.h('p', '最近登录时间:2024年/08/08 13:03'),
+                                Vue.h('p', '明天有暴雨,记得出门带伞噢!')
+                            ])
+                        ]),
+                        Vue.h('div', { class: 'progress-bar' }, [
+                            Vue.h('div', { class: 'progress' }, [
+                                Vue.h('div', { class: 'line', style: 'width: 60%' }, [
+                                    Vue.h('div', '60%(课时)')
+                                ])
+                            ])
+                        ]),
+                        Vue.h('div', { class: 'other-info' }, [
+                            Vue.h('div', {}, [
+                                Vue.h(SsIcon, { name: "userGroup", size: "22px" }),
+                                Vue.h('div', {}, "《关于公司全面预算规划会议》"),
+                            ]),
+                            Vue.h('div', {}, [
+                                Vue.h(SsIcon, { name: "card", type: "common", size: "22px" }),
+                                Vue.h('div', {}, "一卡通余额:**************"),
+                            ]),
+                            Vue.h('div', {}, [
+                                Vue.h(SsIcon, { name: "site", type: "common", size: "22px" }),
+                                Vue.h('div', {}, "个人网站:xxx.xxx.xxx"),
+                            ]),
+                        ])
+                    ])
+                ]
+            })
+        );
+    }
+};
+// 待办
+export const TodoList = {
+    name: 'TodoList',
+    setup() {
+        const todoGroupList = Vue.reactive([
+            {
+                title: "草稿",
+                type: "folder",
+                open: false,
+                children: [
+                    { title: "被退回", time: "08/10 18:12" },
+                    { title: "项目立项申请", time: "08/10 18:12" },
+                    { title: "报销申请", time: "08/10 18:12" },
+                    { title: "资产领用申请", time: "08/10 18:12" },
+                ],
+            },
+            {
+                title: "被退回",
+                type: "folder",
+                open: true,
+                children: [
+                    { title: "被退回", time: "08/10 18:12" },
+                    { title: "项目立项申请", time: "08/10 18:12" },
+                    { title: "报销申请", time: "08/10 18:12" },
+                    { title: "资产领用申请", time: "08/10 18:12" },
+                ],
+            },
+            {
+                title: "2020-高薪技术业项目申报",
+                type: "file",
+                children: [],
+            },
+        ]);
+        const winInfo = Vue.ref(loadWinSize(winName.TodoList));
+        const onSetting = () => {
+            console.log("Clicked on settings button");
+        };
+        const onRefresh = () => {
+            console.log("Clicked on refresh button");
+        };
+        const onSizeChange = (rect) => {
+            console.log(rect)
+            const style = {};
+            for (const key in rect) {
+                style[key] = rect[key] + "px";
+            }
+            saveWinSize(winName.TodoList, style);
+            winInfo.value = style;
+        };
+        const onItemClick = (e) => {
+            console.log("===>>>", e);
+        };
+        Vue.onMounted(() => {
+            // console.log("TodoList component is mounted");
+        });
+
+        return () => Vue.h('div', { class: 'todo-list-container can-resize-box', style: winInfo.value },
+            Vue.h(EditBox, { onSize: onSizeChange }, {
+                default: () => [
+                    Vue.h('div', { class: 'header' }, [
+                        Vue.h(HeaderContainer, {
+                            title: "待办",
+                            icon: "todo",
+                            onSetting,
+                            onRefresh
+                        })
+                    ]),
+                    Vue.h('div', { class: 'body' }, [
+                        Vue.h(FolderContainer, { list: todoGroupList, onClick: onItemClick })
+                    ])
+                ]
+            })
+        )
+    }
+};
+// 催办
+export const UrgingList = {
+    name: 'UrgingList',
+    setup() {
+
+        const todoGroupList = Vue.reactive([
+            {
+                title: "项目",
+                type: "folder",
+                open: true,
+                children: [
+                    { title: "《高新技术企业认定》项目报销", time: "08/10 18:12" },
+                    { title: "日常办公报销", time: "08/10 18:12" },
+                    { title: "固定资产维修费用报销", time: "08/10 18:12" },
+                ],
+            },
+            {
+                title: "报销",
+                type: "folder",
+                open: true,
+                children: [
+                    {
+                        title: "2020-企业-技术改造专项资金项目-立项申请",
+                        time: "08/10 18:12",
+                    },
+                ],
+            },
+            {
+                title: "2020-高薪技术企业项目申报",
+                type: "file",
+                children: [],
+            },
+            {
+                title: "2020-高薪技术企业项目申报-技术改造专项资金项目-立项申请",
+                type: "file",
+                children: [],
+            },
+        ]);
+
+        const winInfo = Vue.ref(loadWinSize(winName.UrgingList));
+
+        const onSetting = () => {
+            console.log("Clicked on settings button");
+        };
+
+        const onRefresh = () => {
+            console.log("Clicked on refresh button");
+        };
+
+        const onSizeChange = (rect) => {
+            const style = {};
+            for (const key in rect) {
+                style[key] = rect[key] + "px";
+            }
+            saveWinSize(winName.UrgingList, style);
+            winInfo.value = style;
+        };
+
+        return () =>
+            Vue.h('div', { class: 'todo-list-container can-resize-box', style: winInfo.value }, Vue.h(EditBox, { onSize: onSizeChange }, {
+                default: () => [
+                    Vue.h('div', { class: 'header' }, [
+                        Vue.h(HeaderContainer, {
+                            title: "催办",
+                            icon: "alarm-clock",
+                            onSetting,
+                            onRefresh
+                        })
+                    ]),
+                    Vue.h('div', { class: 'body' }, [
+                        Vue.h(FolderContainer, { list: todoGroupList })
+                    ])
+                ]
+            })
+            )
+    }
+};
+// 公示公告
+export const Notice = {
+    name: 'Notice',
+    setup() {
+        const list = Vue.reactive([
+            { title: "关于2022年度中层干部的任免公告", time: "10/08 08:30" },
+            { title: "2022年重组团队从心出发", time: "10/08 08:30" },
+            { title: "关于单位进入粤港澳创新创业大赛决赛", time: "10/08 08:30" },
+            { title: "关于2022年度中层干部的任免公告", time: "10/08 08:30" },
+        ]);
+
+        const winInfo = Vue.ref(loadWinSize(winName.Notice));
+
+        const onSetting = () => {
+            console.log("Clicked on settings button");
+        };
+
+        const onRefresh = () => {
+            console.log("Clicked on refresh button");
+        };
+
+        const onSizeChange = (rect) => {
+            const style = {};
+            for (const key in rect) {
+                style[key] = rect[key] + "px";
+            }
+            saveWinSize(winName.Notice, style);
+            winInfo.value = style;
+        };
+        const SsIcon = Vue.resolveComponent('ss-icon');
+
+
+        return () => Vue.h('div', { class: 'notice-list-container can-resize-box', style: winInfo.value }, [
+            Vue.h(EditBox, { onSize: onSizeChange }, {
+                default: () => [
+                    Vue.h('div', { class: 'header' }, [
+                        Vue.h(HeaderContainer, {
+                            title: "公示公告",
+                            icon: "notice",
+                            onSetting: onSetting,
+                            onRefresh: onRefresh
+                        })
+                    ]),
+                    Vue.h('div', { class: 'body' }, list.map((item, i) =>
+                        Vue.h('div', { key: i }, [
+                            Vue.h('div', [
+                                Vue.h(SsIcon, { name: "file", size: "22px" }),
+                                Vue.h('span', item.title)
+                            ]),
+                            Vue.h('div', item.time)
+                        ])
+                    ))
+                ]
+            })
+        ]);
+    }
+};
+// 快捷发起
+export const Launch = {
+    name: 'Launch',
+    setup() {
+        const winInfo = Vue.ref(loadWinSize(winName.launch));
+        const popupInfo = Vue.reactive({
+            status: false,
+            height: 100,
+            width: 100,
+            left: 0,
+            top: 0,
+        });
+        const popupStyle = Vue.computed(() => {
+            const { width, height, left, top } = popupInfo;
+            return {
+                left: `${left - (130 - width) / 2}px`,
+                top: `${top + height}px`,
+            };
+        });
+        const items = Vue.reactive([
+            { name: "qingjia", text: "请假" },
+            {
+                name: "shoufukuan",
+                text: "收付款",
+                hasPopup: true,
+                popupContent: [
+                    { text: "收款" },
+                    { text: "付款" }
+                ]
+            },
+            {
+                name: "kaoqin",
+                text: "考勤",
+                hasPopup: true,
+                popupContent: [
+                    { text: "2" },
+                    { text: "3" }
+                ]
+
+            }
+        ]);
+        const onSetting = () => console.log("Clicked on settings button");
+        const onRefresh = () => console.log("Clicked on refresh button");
+        const onMouseMove = (e, item) => {
+            console.log("鼠标进入了",item.text)
+            showPopupDialog(e, item)
+        };
+
+        const onMouseleave = (e,item) => {
+            console.log("鼠标离开了",item.text)
+            popupInfo.status = false;
+        };
+        const showPopupDialog = (e, item) => {
+            const dom = e.target.closest('.item');
+            if (dom) {
+                const rect = dom.getBoundingClientRect();
+                popupInfo.height = rect.height;
+                popupInfo.left = rect.left;
+                popupInfo.top = rect.top;
+                popupInfo.width = rect.width;
+                setTimeout(() => {
+                    popupInfo.status = true;
+                }, 600)
+            }
+        };
+        const onSizeChange = (rect) => {
+            const style = {};
+            for (const key in rect) style[key] = rect[key] + "px";
+            saveWinSize(winName.launch, style);
+            winInfo.value = style;
+        };
+        // Vue.onMounted(() => console.log("LaunchContainer component is mounted"));
+        const SsIcon = Vue.resolveComponent('ss-icon');
+        return () => Vue.h('div', { class: 'launch-container can-resize-box', style: winInfo.value }, [
+            Vue.h(EditBox, { onSize: onSizeChange }, {
+                default: () => [
+                    Vue.h('div', { class: 'header' }, [
+                        Vue.h(HeaderContainer, { title: "快捷发起", icon: "lightning", onSetting, onRefresh })
+                    ]),
+                    Vue.h('div', { class: 'body' }, items.map(item =>
+                        Vue.h('div', { class: 'item', onMousemove: e => onMouseMove(e, item), onMouseleave: e => onMouseleave(e, item) }, [
+                            Vue.h(SsIcon, {
+                                class: item.hasPopup && item.popupContent.length > 0 ? "mark-down" : "",
+                                name: item.name,
+                                size: "36px"
+                            }),
+                            Vue.h('div', { class: 'text' }, item.text),
+                            item.hasPopup && popupInfo.status ? Vue.h('div', { class: 'popup', style: popupStyle.value }, item.popupContent.map(content =>
+                                Vue.h('div', content.text)
+                            )) : null
+                        ])
+                    ))
+                ]
+            })
+        ])
+    }
+};
+// 项目实时统计图
+export const Statistics = {
+    name: 'Statistics',
+    setup() {
+        const winInfo = Vue.ref(loadWinSize(winName.Statistics));
+        const chartInstance = Vue.ref(null);
+        const option = {
+            tooltip: {
+                trigger: "axis",
+                axisPointer: {
+                    type: "cross",
+                    crossStyle: {
+                        color: "#999",
+                    },
+                },
+            },
+            legend: {
+                data: ["项目金额", "成本"]
+            },
+            xAxis: [{
+                type: "category",
+                data: ["2月", "3月", "4���", "5月", "6月", "7月", "8月", "9月", "10月", "11月"],
+                axisPointer: {
+                    type: "shadow"
+                },
+            }],
+            yAxis: [{
+                type: "value",
+                name: "单位:万(RMB)",
+                min: 0,
+                max: 1000,
+                interval: 200,
+                axisLabel: {
+                    formatter: "{value} "
+                },
+            }, {
+                type: "value",
+                name: "成本",
+                min: 0,
+                max: 1000,
+                interval: 200,
+                axisLabel: {
+                    formatter: "{value}"
+                },
+            }],
+            series: [{
+                name: "项目金额",
+                type: "bar",
+                tooltip: {
+                    valueFormatter: function (value) {
+                        return value + " ml";
+                    }
+                },
+                data: [250, 850, 500, 650, 450, 850, 780, 450, 700, 850]
+            }, {
+                name: "成本",
+                type: "line",
+                yAxisIndex: 1,
+                tooltip: {
+                    valueFormatter: function (value) {
+                        return value;
+                    }
+                },
+                data: [400, 390, 580, 590, 430, 420, 430, 550, 230, 250]
+            }]
+        }
+        // 初始化echants
+        const initChart = () => {
+            const chartDom = document.getElementById("statistics-chart");
+            if (!chartDom) return;
+            chartInstance.value = echarts.init(chartDom);
+            chartInstance.value.setOption(option);
+        };
+
+        const onSetting = () => console.log("Clicked on settings button");
+        const onRefresh = () => {
+            console.log("Clicked on refresh button");
+            if (chartInstance.value) {
+                chartInstance.value.clear();
+                initChart();  // Reinitialize the chart to reflect any new data or settings
+            }
+        };
+
+        const onSizeChange = (rect) => {
+            const style = {};
+            for (const key in rect) style[key] = rect[key] + "px";
+            saveWinSize(winName.Statistics, style);
+            winInfo.value = style;
+            if (chartInstance.value) {
+                chartInstance.value.resize();
+            }
+        };
+
+        Vue.onMounted(() => {
+            // console.log("StatisticsContainer component is mounted");
+            initChart();  // Initialize the chart when component is mounted
+        });
+
+        // return { winInfo, onSetting, onRefresh, onSizeChange };
+        return () => Vue.h('div', { class: 'statistics-container can-resize-box', style: winInfo.value }, [
+            Vue.h(EditBox, { onSize: onSizeChange }, {
+                default: () => [
+                    Vue.h('div', { class: 'header' }, [
+                        Vue.h(HeaderContainer, { title: "项目实时统计图", icon: "layer", onSetting, onRefresh })
+                    ]),
+                    Vue.h('div', { class: 'body' }, [
+                        Vue.h('div', { id: 'statistics-chart', class: 'chart-container' })
+                    ])
+                ]
+            })
+        ]);
+    },
+
+};

+ 203 - 0
js/vue/tools.js

@@ -0,0 +1,203 @@
+export var camelToKebab = function (camelStr) {
+    const isUpper = camelStr[0].charCodeAt(0) >= 65 && camelStr[0].charCodeAt(0) <= 90;
+    const handleStr = camelStr.replace(/([A-Z])/g, "-$1").toLowerCase();
+    let kebabStr = handleStr;
+    if (isUpper) {
+      kebabStr = handleStr.slice(1);
+    }
+    const newKebabArr = [];
+    const kebabSplitArr = kebabStr.split("-");
+    kebabSplitArr.forEach((item, index) => {
+      if (item.length > 1) {
+        newKebabArr.push(item);
+      } else {
+        let combineStr = "";
+        const subKebabArr = kebabSplitArr.slice(index);
+        for (let i = 0; i < subKebabArr.length; i++) {
+          if (subKebabArr[i].length > 1)
+            break;
+          combineStr += subKebabArr[i];
+        }
+        newKebabArr.push(combineStr);
+        kebabSplitArr.splice(index + 1, combineStr.length - 1);
+      }
+    });
+    return newKebabArr.join("-");
+  };
+export var isNum = function (num) {
+    return typeof num === "number" && !isNaN(num);
+  };
+export var toStyleStr = (styleObj, unit = "rpx") => {
+    if (Array.isArray(styleObj)) {
+      return styleObj.join(";");
+    } else {
+      if (typeof styleObj === "object" && styleObj !== null) {
+        let style = [];
+        for (const k in styleObj) {
+          if (typeof k === "string") {
+            let val = styleObj[k];
+            if (val !== void 0) {
+              if (typeof val === "number") {
+                if (addUnitAttr.includes(k)) {
+                  val = val + unit;
+                }
+              }
+              style.push(`${camelToKebab(k)}:${val}`);
+            }
+          }
+        }
+        return style.length > 0 ? style.join(";") + ";" : "";
+      } else {
+        return styleObj;
+      }
+    }
+  };
+
+function isObject(value) {
+  var type = typeof value;
+  return value != null && (type == 'object' || type == 'function');
+}
+function toNumber(value) {
+  if (typeof value == 'number') {
+    return value;
+  }
+  if (isSymbol(value)) {
+    return NAN;
+  }
+  if (isObject(value)) {
+    var other = typeof value.valueOf == 'function' ? value.valueOf() : value;
+    value = isObject(other) ? (other + '') : other;
+  }
+  if (typeof value != 'string') {
+    return value === 0 ? value : +value;
+  }
+  value = value.replace(reTrim, '');
+  var isBinary = reIsBinary.test(value);
+  return (isBinary || reIsOctal.test(value))
+    ? freeParseInt(value.slice(2), isBinary ? 2 : 8)
+    : (reIsBadHex.test(value) ? NAN : +value);
+}
+export function debounce(func, wait, options) {
+    var lastArgs,
+        lastThis,
+        maxWait,
+        result,
+        timerId,
+        lastCallTime,
+        lastInvokeTime = 0,
+        leading = false,
+        maxing = false,
+        trailing = true;
+
+    if (typeof func != 'function') {
+      throw new TypeError(FUNC_ERROR_TEXT);
+    }
+    wait = toNumber(wait) || 0;
+    if (isObject(options)) {
+      leading = !!options.leading;
+      maxing = 'maxWait' in options;
+      maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
+      trailing = 'trailing' in options ? !!options.trailing : trailing;
+    }
+
+    function invokeFunc(time) {
+      var args = lastArgs,
+          thisArg = lastThis;
+
+      lastArgs = lastThis = undefined;
+      lastInvokeTime = time;
+      result = func.apply(thisArg, args);
+      return result;
+    }
+
+    function leadingEdge(time) {
+      // Reset any `maxWait` timer.
+      lastInvokeTime = time;
+      // Start the timer for the trailing edge.
+      timerId = setTimeout(timerExpired, wait);
+      // Invoke the leading edge.
+      return leading ? invokeFunc(time) : result;
+    }
+
+    function remainingWait(time) {
+      var timeSinceLastCall = time - lastCallTime,
+          timeSinceLastInvoke = time - lastInvokeTime,
+          timeWaiting = wait - timeSinceLastCall;
+
+      return maxing
+        ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
+        : timeWaiting;
+    }
+
+    function shouldInvoke(time) {
+      var timeSinceLastCall = time - lastCallTime,
+          timeSinceLastInvoke = time - lastInvokeTime;
+
+      // Either this is the first call, activity has stopped and we're at the
+      // trailing edge, the system time has gone backwards and we're treating
+      // it as the trailing edge, or we've hit the `maxWait` limit.
+      return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
+        (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
+    }
+
+    function timerExpired() {
+      var time = now();
+      if (shouldInvoke(time)) {
+        return trailingEdge(time);
+      }
+      // Restart the timer.
+      timerId = setTimeout(timerExpired, remainingWait(time));
+    }
+
+    function trailingEdge(time) {
+      timerId = undefined;
+
+      // Only invoke if we have `lastArgs` which means `func` has been
+      // debounced at least once.
+      if (trailing && lastArgs) {
+        return invokeFunc(time);
+      }
+      lastArgs = lastThis = undefined;
+      return result;
+    }
+
+    function cancel() {
+      if (timerId !== undefined) {
+        clearTimeout(timerId);
+      }
+      lastInvokeTime = 0;
+      lastArgs = lastCallTime = lastThis = timerId = undefined;
+    }
+
+    function flush() {
+      return timerId === undefined ? result : trailingEdge(now());
+    }
+
+    function debounced() {
+      var time = now(),
+          isInvoking = shouldInvoke(time);
+
+      lastArgs = arguments;
+      lastThis = this;
+      lastCallTime = time;
+
+      if (isInvoking) {
+        if (timerId === undefined) {
+          return leadingEdge(lastCallTime);
+        }
+        if (maxing) {
+          // Handle invocations in a tight loop.
+          timerId = setTimeout(timerExpired, wait);
+          return invokeFunc(lastCallTime);
+        }
+      }
+      if (timerId === undefined) {
+        timerId = setTimeout(timerExpired, wait);
+      }
+      return result;
+    }
+    debounced.cancel = cancel;
+    debounced.flush = flush;
+    return debounced;
+}
+

+ 3854 - 0
js/vue/vue-router.gloabl.js

@@ -0,0 +1,3854 @@
+/*!
+  * vue-router v4.3.2
+  * (c) 2024 Eduardo San Martin Morote
+  * @license MIT
+  */
+var VueRouter = (function (exports, vue) {
+  'use strict';
+
+  const isBrowser = typeof document !== 'undefined';
+
+  function isESModule(obj) {
+      return obj.__esModule || obj[Symbol.toStringTag] === 'Module';
+  }
+  const assign = Object.assign;
+  function applyToParams(fn, params) {
+      const newParams = {};
+      for (const key in params) {
+          const value = params[key];
+          newParams[key] = isArray(value)
+              ? value.map(fn)
+              : fn(value);
+      }
+      return newParams;
+  }
+  const noop = () => { };
+  /**
+   * Typesafe alternative to Array.isArray
+   * https://github.com/microsoft/TypeScript/pull/48228
+   */
+  const isArray = Array.isArray;
+
+  function warn(msg) {
+      // avoid using ...args as it breaks in older Edge builds
+      const args = Array.from(arguments).slice(1);
+      console.warn.apply(console, ['[Vue Router warn]: ' + msg].concat(args));
+  }
+
+  /**
+   * Encoding Rules (␣ = Space)
+   * - Path: ␣ " < > # ? { }
+   * - Query: ␣ " < > # & =
+   * - Hash: ␣ " < > `
+   *
+   * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2)
+   * defines some extra characters to be encoded. Most browsers do not encode them
+   * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to
+   * also encode `!'()*`. Leaving un-encoded only ASCII alphanumeric(`a-zA-Z0-9`)
+   * plus `-._~`. This extra safety should be applied to query by patching the
+   * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\`
+   * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\`
+   * into a `/` if directly typed in. The _backtick_ (`````) should also be
+   * encoded everywhere because some browsers like FF encode it when directly
+   * written while others don't. Safari and IE don't encode ``"<>{}``` in hash.
+   */
+  // const EXTRA_RESERVED_RE = /[!'()*]/g
+  // const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16)
+  const HASH_RE = /#/g; // %23
+  const AMPERSAND_RE = /&/g; // %26
+  const SLASH_RE = /\//g; // %2F
+  const EQUAL_RE = /=/g; // %3D
+  const IM_RE = /\?/g; // %3F
+  const PLUS_RE = /\+/g; // %2B
+  /**
+   * NOTE: It's not clear to me if we should encode the + symbol in queries, it
+   * seems to be less flexible than not doing so and I can't find out the legacy
+   * systems requiring this for regular requests like text/html. In the standard,
+   * the encoding of the plus character is only mentioned for
+   * application/x-www-form-urlencoded
+   * (https://url.spec.whatwg.org/#urlencoded-parsing) and most browsers seems lo
+   * leave the plus character as is in queries. To be more flexible, we allow the
+   * plus character on the query, but it can also be manually encoded by the user.
+   *
+   * Resources:
+   * - https://url.spec.whatwg.org/#urlencoded-parsing
+   * - https://stackoverflow.com/questions/1634271/url-encoding-the-space-character-or-20
+   */
+  const ENC_BRACKET_OPEN_RE = /%5B/g; // [
+  const ENC_BRACKET_CLOSE_RE = /%5D/g; // ]
+  const ENC_CARET_RE = /%5E/g; // ^
+  const ENC_BACKTICK_RE = /%60/g; // `
+  const ENC_CURLY_OPEN_RE = /%7B/g; // {
+  const ENC_PIPE_RE = /%7C/g; // |
+  const ENC_CURLY_CLOSE_RE = /%7D/g; // }
+  const ENC_SPACE_RE = /%20/g; // }
+  /**
+   * Encode characters that need to be encoded on the path, search and hash
+   * sections of the URL.
+   *
+   * @internal
+   * @param text - string to encode
+   * @returns encoded string
+   */
+  function commonEncode(text) {
+      return encodeURI('' + text)
+          .replace(ENC_PIPE_RE, '|')
+          .replace(ENC_BRACKET_OPEN_RE, '[')
+          .replace(ENC_BRACKET_CLOSE_RE, ']');
+  }
+  /**
+   * Encode characters that need to be encoded on the hash section of the URL.
+   *
+   * @param text - string to encode
+   * @returns encoded string
+   */
+  function encodeHash(text) {
+      return commonEncode(text)
+          .replace(ENC_CURLY_OPEN_RE, '{')
+          .replace(ENC_CURLY_CLOSE_RE, '}')
+          .replace(ENC_CARET_RE, '^');
+  }
+  /**
+   * Encode characters that need to be encoded query values on the query
+   * section of the URL.
+   *
+   * @param text - string to encode
+   * @returns encoded string
+   */
+  function encodeQueryValue(text) {
+      return (commonEncode(text)
+          // Encode the space as +, encode the + to differentiate it from the space
+          .replace(PLUS_RE, '%2B')
+          .replace(ENC_SPACE_RE, '+')
+          .replace(HASH_RE, '%23')
+          .replace(AMPERSAND_RE, '%26')
+          .replace(ENC_BACKTICK_RE, '`')
+          .replace(ENC_CURLY_OPEN_RE, '{')
+          .replace(ENC_CURLY_CLOSE_RE, '}')
+          .replace(ENC_CARET_RE, '^'));
+  }
+  /**
+   * Like `encodeQueryValue` but also encodes the `=` character.
+   *
+   * @param text - string to encode
+   */
+  function encodeQueryKey(text) {
+      return encodeQueryValue(text).replace(EQUAL_RE, '%3D');
+  }
+  /**
+   * Encode characters that need to be encoded on the path section of the URL.
+   *
+   * @param text - string to encode
+   * @returns encoded string
+   */
+  function encodePath(text) {
+      return commonEncode(text).replace(HASH_RE, '%23').replace(IM_RE, '%3F');
+  }
+  /**
+   * Encode characters that need to be encoded on the path section of the URL as a
+   * param. This function encodes everything {@link encodePath} does plus the
+   * slash (`/`) character. If `text` is `null` or `undefined`, returns an empty
+   * string instead.
+   *
+   * @param text - string to encode
+   * @returns encoded string
+   */
+  function encodeParam(text) {
+      return text == null ? '' : encodePath(text).replace(SLASH_RE, '%2F');
+  }
+  /**
+   * Decode text using `decodeURIComponent`. Returns the original text if it
+   * fails.
+   *
+   * @param text - string to decode
+   * @returns decoded string
+   */
+  function decode(text) {
+      try {
+          return decodeURIComponent('' + text);
+      }
+      catch (err) {
+          warn(`Error decoding "${text}". Using original value`);
+      }
+      return '' + text;
+  }
+
+  const TRAILING_SLASH_RE = /\/$/;
+  const removeTrailingSlash = (path) => path.replace(TRAILING_SLASH_RE, '');
+  /**
+   * Transforms a URI into a normalized history location
+   *
+   * @param parseQuery
+   * @param location - URI to normalize
+   * @param currentLocation - current absolute location. Allows resolving relative
+   * paths. Must start with `/`. Defaults to `/`
+   * @returns a normalized history location
+   */
+  function parseURL(parseQuery, location, currentLocation = '/') {
+      let path, query = {}, searchString = '', hash = '';
+      // Could use URL and URLSearchParams but IE 11 doesn't support it
+      // TODO: move to new URL()
+      const hashPos = location.indexOf('#');
+      let searchPos = location.indexOf('?');
+      // the hash appears before the search, so it's not part of the search string
+      if (hashPos < searchPos && hashPos >= 0) {
+          searchPos = -1;
+      }
+      if (searchPos > -1) {
+          path = location.slice(0, searchPos);
+          searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
+          query = parseQuery(searchString);
+      }
+      if (hashPos > -1) {
+          path = path || location.slice(0, hashPos);
+          // keep the # character
+          hash = location.slice(hashPos, location.length);
+      }
+      // no search and no query
+      path = resolveRelativePath(path != null ? path : location, currentLocation);
+      // empty path means a relative query or hash `?foo=f`, `#thing`
+      return {
+          fullPath: path + (searchString && '?') + searchString + hash,
+          path,
+          query,
+          hash: decode(hash),
+      };
+  }
+  /**
+   * Stringifies a URL object
+   *
+   * @param stringifyQuery
+   * @param location
+   */
+  function stringifyURL(stringifyQuery, location) {
+      const query = location.query ? stringifyQuery(location.query) : '';
+      return location.path + (query && '?') + query + (location.hash || '');
+  }
+  /**
+   * Strips off the base from the beginning of a location.pathname in a non-case-sensitive way.
+   *
+   * @param pathname - location.pathname
+   * @param base - base to strip off
+   */
+  function stripBase(pathname, base) {
+      // no base or base is not found at the beginning
+      if (!base || !pathname.toLowerCase().startsWith(base.toLowerCase()))
+          return pathname;
+      return pathname.slice(base.length) || '/';
+  }
+  /**
+   * Checks if two RouteLocation are equal. This means that both locations are
+   * pointing towards the same {@link RouteRecord} and that all `params`, `query`
+   * parameters and `hash` are the same
+   *
+   * @param stringifyQuery - A function that takes a query object of type LocationQueryRaw and returns a string representation of it.
+   * @param a - first {@link RouteLocation}
+   * @param b - second {@link RouteLocation}
+   */
+  function isSameRouteLocation(stringifyQuery, a, b) {
+      const aLastIndex = a.matched.length - 1;
+      const bLastIndex = b.matched.length - 1;
+      return (aLastIndex > -1 &&
+          aLastIndex === bLastIndex &&
+          isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
+          isSameRouteLocationParams(a.params, b.params) &&
+          stringifyQuery(a.query) === stringifyQuery(b.query) &&
+          a.hash === b.hash);
+  }
+  /**
+   * Check if two `RouteRecords` are equal. Takes into account aliases: they are
+   * considered equal to the `RouteRecord` they are aliasing.
+   *
+   * @param a - first {@link RouteRecord}
+   * @param b - second {@link RouteRecord}
+   */
+  function isSameRouteRecord(a, b) {
+      // since the original record has an undefined value for aliasOf
+      // but all aliases point to the original record, this will always compare
+      // the original record
+      return (a.aliasOf || a) === (b.aliasOf || b);
+  }
+  function isSameRouteLocationParams(a, b) {
+      if (Object.keys(a).length !== Object.keys(b).length)
+          return false;
+      for (const key in a) {
+          if (!isSameRouteLocationParamsValue(a[key], b[key]))
+              return false;
+      }
+      return true;
+  }
+  function isSameRouteLocationParamsValue(a, b) {
+      return isArray(a)
+          ? isEquivalentArray(a, b)
+          : isArray(b)
+              ? isEquivalentArray(b, a)
+              : a === b;
+  }
+  /**
+   * Check if two arrays are the same or if an array with one single entry is the
+   * same as another primitive value. Used to check query and parameters
+   *
+   * @param a - array of values
+   * @param b - array of values or a single value
+   */
+  function isEquivalentArray(a, b) {
+      return isArray(b)
+          ? a.length === b.length && a.every((value, i) => value === b[i])
+          : a.length === 1 && a[0] === b;
+  }
+  /**
+   * Resolves a relative path that starts with `.`.
+   *
+   * @param to - path location we are resolving
+   * @param from - currentLocation.path, should start with `/`
+   */
+  function resolveRelativePath(to, from) {
+      if (to.startsWith('/'))
+          return to;
+      if (!from.startsWith('/')) {
+          warn(`Cannot resolve a relative location without an absolute path. Trying to resolve "${to}" from "${from}". It should look like "/${from}".`);
+          return to;
+      }
+      if (!to)
+          return from;
+      const fromSegments = from.split('/');
+      const toSegments = to.split('/');
+      const lastToSegment = toSegments[toSegments.length - 1];
+      // make . and ./ the same (../ === .., ../../ === ../..)
+      // this is the same behavior as new URL()
+      if (lastToSegment === '..' || lastToSegment === '.') {
+          toSegments.push('');
+      }
+      let position = fromSegments.length - 1;
+      let toPosition;
+      let segment;
+      for (toPosition = 0; toPosition < toSegments.length; toPosition++) {
+          segment = toSegments[toPosition];
+          // we stay on the same position
+          if (segment === '.')
+              continue;
+          // go up in the from array
+          if (segment === '..') {
+              // we can't go below zero, but we still need to increment toPosition
+              if (position > 1)
+                  position--;
+              // continue
+          }
+          // we reached a non-relative path, we stop here
+          else
+              break;
+      }
+      return (fromSegments.slice(0, position).join('/') +
+          '/' +
+          toSegments.slice(toPosition).join('/'));
+  }
+
+  var NavigationType;
+  (function (NavigationType) {
+      NavigationType["pop"] = "pop";
+      NavigationType["push"] = "push";
+  })(NavigationType || (NavigationType = {}));
+  var NavigationDirection;
+  (function (NavigationDirection) {
+      NavigationDirection["back"] = "back";
+      NavigationDirection["forward"] = "forward";
+      NavigationDirection["unknown"] = "";
+  })(NavigationDirection || (NavigationDirection = {}));
+  /**
+   * Starting location for Histories
+   */
+  const START = '';
+  // Generic utils
+  /**
+   * Normalizes a base by removing any trailing slash and reading the base tag if
+   * present.
+   *
+   * @param base - base to normalize
+   */
+  function normalizeBase(base) {
+      if (!base) {
+          if (isBrowser) {
+              // respect <base> tag
+              const baseEl = document.querySelector('base');
+              base = (baseEl && baseEl.getAttribute('href')) || '/';
+              // strip full URL origin
+              base = base.replace(/^\w+:\/\/[^\/]+/, '');
+          }
+          else {
+              base = '/';
+          }
+      }
+      // ensure leading slash when it was removed by the regex above avoid leading
+      // slash with hash because the file could be read from the disk like file://
+      // and the leading slash would cause problems
+      if (base[0] !== '/' && base[0] !== '#')
+          base = '/' + base;
+      // remove the trailing slash so all other method can just do `base + fullPath`
+      // to build an href
+      return removeTrailingSlash(base);
+  }
+  // remove any character before the hash
+  const BEFORE_HASH_RE = /^[^#]+#/;
+  function createHref(base, location) {
+      return base.replace(BEFORE_HASH_RE, '#') + location;
+  }
+
+  function getElementPosition(el, offset) {
+      const docRect = document.documentElement.getBoundingClientRect();
+      const elRect = el.getBoundingClientRect();
+      return {
+          behavior: offset.behavior,
+          left: elRect.left - docRect.left - (offset.left || 0),
+          top: elRect.top - docRect.top - (offset.top || 0),
+      };
+  }
+  const computeScrollPosition = () => ({
+      left: window.scrollX,
+      top: window.scrollY,
+  });
+  function scrollToPosition(position) {
+      let scrollToOptions;
+      if ('el' in position) {
+          const positionEl = position.el;
+          const isIdSelector = typeof positionEl === 'string' && positionEl.startsWith('#');
+          /**
+           * `id`s can accept pretty much any characters, including CSS combinators
+           * like `>` or `~`. It's still possible to retrieve elements using
+           * `document.getElementById('~')` but it needs to be escaped when using
+           * `document.querySelector('#\\~')` for it to be valid. The only
+           * requirements for `id`s are them to be unique on the page and to not be
+           * empty (`id=""`). Because of that, when passing an id selector, it should
+           * be properly escaped for it to work with `querySelector`. We could check
+           * for the id selector to be simple (no CSS combinators `+ >~`) but that
+           * would make things inconsistent since they are valid characters for an
+           * `id` but would need to be escaped when using `querySelector`, breaking
+           * their usage and ending up in no selector returned. Selectors need to be
+           * escaped:
+           *
+           * - `#1-thing` becomes `#\31 -thing`
+           * - `#with~symbols` becomes `#with\\~symbols`
+           *
+           * - More information about  the topic can be found at
+           *   https://mathiasbynens.be/notes/html5-id-class.
+           * - Practical example: https://mathiasbynens.be/demo/html5-id
+           */
+          if (typeof position.el === 'string') {
+              if (!isIdSelector || !document.getElementById(position.el.slice(1))) {
+                  try {
+                      const foundEl = document.querySelector(position.el);
+                      if (isIdSelector && foundEl) {
+                          warn(`The selector "${position.el}" should be passed as "el: document.querySelector('${position.el}')" because it starts with "#".`);
+                          // return to avoid other warnings
+                          return;
+                      }
+                  }
+                  catch (err) {
+                      warn(`The selector "${position.el}" is invalid. If you are using an id selector, make sure to escape it. You can find more information about escaping characters in selectors at https://mathiasbynens.be/notes/css-escapes or use CSS.escape (https://developer.mozilla.org/en-US/docs/Web/API/CSS/escape).`);
+                      // return to avoid other warnings
+                      return;
+                  }
+              }
+          }
+          const el = typeof positionEl === 'string'
+              ? isIdSelector
+                  ? document.getElementById(positionEl.slice(1))
+                  : document.querySelector(positionEl)
+              : positionEl;
+          if (!el) {
+              warn(`Couldn't find element using selector "${position.el}" returned by scrollBehavior.`);
+              return;
+          }
+          scrollToOptions = getElementPosition(el, position);
+      }
+      else {
+          scrollToOptions = position;
+      }
+      if ('scrollBehavior' in document.documentElement.style)
+          window.scrollTo(scrollToOptions);
+      else {
+          window.scrollTo(scrollToOptions.left != null ? scrollToOptions.left : window.scrollX, scrollToOptions.top != null ? scrollToOptions.top : window.scrollY);
+      }
+  }
+  function getScrollKey(path, delta) {
+      const position = history.state ? history.state.position - delta : -1;
+      return position + path;
+  }
+  const scrollPositions = new Map();
+  function saveScrollPosition(key, scrollPosition) {
+      scrollPositions.set(key, scrollPosition);
+  }
+  function getSavedScrollPosition(key) {
+      const scroll = scrollPositions.get(key);
+      // consume it so it's not used again
+      scrollPositions.delete(key);
+      return scroll;
+  }
+  // TODO: RFC about how to save scroll position
+  /**
+   * ScrollBehavior instance used by the router to compute and restore the scroll
+   * position when navigating.
+   */
+  // export interface ScrollHandler<ScrollPositionEntry extends HistoryStateValue, ScrollPosition extends ScrollPositionEntry> {
+  //   // returns a scroll position that can be saved in history
+  //   compute(): ScrollPositionEntry
+  //   // can take an extended ScrollPositionEntry
+  //   scroll(position: ScrollPosition): void
+  // }
+  // export const scrollHandler: ScrollHandler<ScrollPosition> = {
+  //   compute: computeScroll,
+  //   scroll: scrollToPosition,
+  // }
+
+  let createBaseLocation = () => location.protocol + '//' + location.host;
+  /**
+   * Creates a normalized history location from a window.location object
+   * @param base - The base path
+   * @param location - The window.location object
+   */
+  function createCurrentLocation(base, location) {
+      const { pathname, search, hash } = location;
+      // allows hash bases like #, /#, #/, #!, #!/, /#!/, or even /folder#end
+      const hashPos = base.indexOf('#');
+      if (hashPos > -1) {
+          let slicePos = hash.includes(base.slice(hashPos))
+              ? base.slice(hashPos).length
+              : 1;
+          let pathFromHash = hash.slice(slicePos);
+          // prepend the starting slash to hash so the url starts with /#
+          if (pathFromHash[0] !== '/')
+              pathFromHash = '/' + pathFromHash;
+          return stripBase(pathFromHash, '');
+      }
+      const path = stripBase(pathname, base);
+      return path + search + hash;
+  }
+  function useHistoryListeners(base, historyState, currentLocation, replace) {
+      let listeners = [];
+      let teardowns = [];
+      // TODO: should it be a stack? a Dict. Check if the popstate listener
+      // can trigger twice
+      let pauseState = null;
+      const popStateHandler = ({ state, }) => {
+          const to = createCurrentLocation(base, location);
+          const from = currentLocation.value;
+          const fromState = historyState.value;
+          let delta = 0;
+          if (state) {
+              currentLocation.value = to;
+              historyState.value = state;
+              // ignore the popstate and reset the pauseState
+              if (pauseState && pauseState === from) {
+                  pauseState = null;
+                  return;
+              }
+              delta = fromState ? state.position - fromState.position : 0;
+          }
+          else {
+              replace(to);
+          }
+          // Here we could also revert the navigation by calling history.go(-delta)
+          // this listener will have to be adapted to not trigger again and to wait for the url
+          // to be updated before triggering the listeners. Some kind of validation function would also
+          // need to be passed to the listeners so the navigation can be accepted
+          // call all listeners
+          listeners.forEach(listener => {
+              listener(currentLocation.value, from, {
+                  delta,
+                  type: NavigationType.pop,
+                  direction: delta
+                      ? delta > 0
+                          ? NavigationDirection.forward
+                          : NavigationDirection.back
+                      : NavigationDirection.unknown,
+              });
+          });
+      };
+      function pauseListeners() {
+          pauseState = currentLocation.value;
+      }
+      function listen(callback) {
+          // set up the listener and prepare teardown callbacks
+          listeners.push(callback);
+          const teardown = () => {
+              const index = listeners.indexOf(callback);
+              if (index > -1)
+                  listeners.splice(index, 1);
+          };
+          teardowns.push(teardown);
+          return teardown;
+      }
+      function beforeUnloadListener() {
+          const { history } = window;
+          if (!history.state)
+              return;
+          history.replaceState(assign({}, history.state, { scroll: computeScrollPosition() }), '');
+      }
+      function destroy() {
+          for (const teardown of teardowns)
+              teardown();
+          teardowns = [];
+          window.removeEventListener('popstate', popStateHandler);
+          window.removeEventListener('beforeunload', beforeUnloadListener);
+      }
+      // set up the listeners and prepare teardown callbacks
+      window.addEventListener('popstate', popStateHandler);
+      // TODO: could we use 'pagehide' or 'visibilitychange' instead?
+      // https://developer.chrome.com/blog/page-lifecycle-api/
+      window.addEventListener('beforeunload', beforeUnloadListener, {
+          passive: true,
+      });
+      return {
+          pauseListeners,
+          listen,
+          destroy,
+      };
+  }
+  /**
+   * Creates a state object
+   */
+  function buildState(back, current, forward, replaced = false, computeScroll = false) {
+      return {
+          back,
+          current,
+          forward,
+          replaced,
+          position: window.history.length,
+          scroll: computeScroll ? computeScrollPosition() : null,
+      };
+  }
+  function useHistoryStateNavigation(base) {
+      const { history, location } = window;
+      // private variables
+      const currentLocation = {
+          value: createCurrentLocation(base, location),
+      };
+      const historyState = { value: history.state };
+      // build current history entry as this is a fresh navigation
+      if (!historyState.value) {
+          changeLocation(currentLocation.value, {
+              back: null,
+              current: currentLocation.value,
+              forward: null,
+              // the length is off by one, we need to decrease it
+              position: history.length - 1,
+              replaced: true,
+              // don't add a scroll as the user may have an anchor, and we want
+              // scrollBehavior to be triggered without a saved position
+              scroll: null,
+          }, true);
+      }
+      function changeLocation(to, state, replace) {
+          /**
+           * if a base tag is provided, and we are on a normal domain, we have to
+           * respect the provided `base` attribute because pushState() will use it and
+           * potentially erase anything before the `#` like at
+           * https://github.com/vuejs/router/issues/685 where a base of
+           * `/folder/#` but a base of `/` would erase the `/folder/` section. If
+           * there is no host, the `<base>` tag makes no sense and if there isn't a
+           * base tag we can just use everything after the `#`.
+           */
+          const hashIndex = base.indexOf('#');
+          const url = hashIndex > -1
+              ? (location.host && document.querySelector('base')
+                  ? base
+                  : base.slice(hashIndex)) + to
+              : createBaseLocation() + base + to;
+          try {
+              // BROWSER QUIRK
+              // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
+              history[replace ? 'replaceState' : 'pushState'](state, '', url);
+              historyState.value = state;
+          }
+          catch (err) {
+              {
+                  warn('Error with push/replace State', err);
+              }
+              // Force the navigation, this also resets the call count
+              location[replace ? 'replace' : 'assign'](url);
+          }
+      }
+      function replace(to, data) {
+          const state = assign({}, history.state, buildState(historyState.value.back, 
+          // keep back and forward entries but override current position
+          to, historyState.value.forward, true), data, { position: historyState.value.position });
+          changeLocation(to, state, true);
+          currentLocation.value = to;
+      }
+      function push(to, data) {
+          // Add to current entry the information of where we are going
+          // as well as saving the current position
+          const currentState = assign({}, 
+          // use current history state to gracefully handle a wrong call to
+          // history.replaceState
+          // https://github.com/vuejs/router/issues/366
+          historyState.value, history.state, {
+              forward: to,
+              scroll: computeScrollPosition(),
+          });
+          if (!history.state) {
+              warn(`history.state seems to have been manually replaced without preserving the necessary values. Make sure to preserve existing history state if you are manually calling history.replaceState:\n\n` +
+                  `history.replaceState(history.state, '', url)\n\n` +
+                  `You can find more information at https://next.router.vuejs.org/guide/migration/#usage-of-history-state.`);
+          }
+          changeLocation(currentState.current, currentState, true);
+          const state = assign({}, buildState(currentLocation.value, to, null), { position: currentState.position + 1 }, data);
+          changeLocation(to, state, false);
+          currentLocation.value = to;
+      }
+      return {
+          location: currentLocation,
+          state: historyState,
+          push,
+          replace,
+      };
+  }
+  /**
+   * Creates an HTML5 history. Most common history for single page applications.
+   *
+   * @param base -
+   */
+  function createWebHistory(base) {
+      base = normalizeBase(base);
+      const historyNavigation = useHistoryStateNavigation(base);
+      const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
+      function go(delta, triggerListeners = true) {
+          if (!triggerListeners)
+              historyListeners.pauseListeners();
+          history.go(delta);
+      }
+      const routerHistory = assign({
+          // it's overridden right after
+          location: '',
+          base,
+          go,
+          createHref: createHref.bind(null, base),
+      }, historyNavigation, historyListeners);
+      Object.defineProperty(routerHistory, 'location', {
+          enumerable: true,
+          get: () => historyNavigation.location.value,
+      });
+      Object.defineProperty(routerHistory, 'state', {
+          enumerable: true,
+          get: () => historyNavigation.state.value,
+      });
+      return routerHistory;
+  }
+
+  /**
+   * Creates an in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere.
+   * It's up to the user to replace that location with the starter location by either calling `router.push` or `router.replace`.
+   *
+   * @param base - Base applied to all urls, defaults to '/'
+   * @returns a history object that can be passed to the router constructor
+   */
+  function createMemoryHistory(base = '') {
+      let listeners = [];
+      let queue = [START];
+      let position = 0;
+      base = normalizeBase(base);
+      function setLocation(location) {
+          position++;
+          if (position !== queue.length) {
+              // we are in the middle, we remove everything from here in the queue
+              queue.splice(position);
+          }
+          queue.push(location);
+      }
+      function triggerListeners(to, from, { direction, delta }) {
+          const info = {
+              direction,
+              delta,
+              type: NavigationType.pop,
+          };
+          for (const callback of listeners) {
+              callback(to, from, info);
+          }
+      }
+      const routerHistory = {
+          // rewritten by Object.defineProperty
+          location: START,
+          // TODO: should be kept in queue
+          state: {},
+          base,
+          createHref: createHref.bind(null, base),
+          replace(to) {
+              // remove current entry and decrement position
+              queue.splice(position--, 1);
+              setLocation(to);
+          },
+          push(to, data) {
+              setLocation(to);
+          },
+          listen(callback) {
+              listeners.push(callback);
+              return () => {
+                  const index = listeners.indexOf(callback);
+                  if (index > -1)
+                      listeners.splice(index, 1);
+              };
+          },
+          destroy() {
+              listeners = [];
+              queue = [START];
+              position = 0;
+          },
+          go(delta, shouldTrigger = true) {
+              const from = this.location;
+              const direction = 
+              // we are considering delta === 0 going forward, but in abstract mode
+              // using 0 for the delta doesn't make sense like it does in html5 where
+              // it reloads the page
+              delta < 0 ? NavigationDirection.back : NavigationDirection.forward;
+              position = Math.max(0, Math.min(position + delta, queue.length - 1));
+              if (shouldTrigger) {
+                  triggerListeners(this.location, from, {
+                      direction,
+                      delta,
+                  });
+              }
+          },
+      };
+      Object.defineProperty(routerHistory, 'location', {
+          enumerable: true,
+          get: () => queue[position],
+      });
+      return routerHistory;
+  }
+
+  /**
+   * Creates a hash history. Useful for web applications with no host (e.g. `file://`) or when configuring a server to
+   * handle any URL is not possible.
+   *
+   * @param base - optional base to provide. Defaults to `location.pathname + location.search` If there is a `<base>` tag
+   * in the `head`, its value will be ignored in favor of this parameter **but note it affects all the history.pushState()
+   * calls**, meaning that if you use a `<base>` tag, it's `href` value **has to match this parameter** (ignoring anything
+   * after the `#`).
+   *
+   * @example
+   * ```js
+   * // at https://example.com/folder
+   * createWebHashHistory() // gives a url of `https://example.com/folder#`
+   * createWebHashHistory('/folder/') // gives a url of `https://example.com/folder/#`
+   * // if the `#` is provided in the base, it won't be added by `createWebHashHistory`
+   * createWebHashHistory('/folder/#/app/') // gives a url of `https://example.com/folder/#/app/`
+   * // you should avoid doing this because it changes the original url and breaks copying urls
+   * createWebHashHistory('/other-folder/') // gives a url of `https://example.com/other-folder/#`
+   *
+   * // at file:///usr/etc/folder/index.html
+   * // for locations with no `host`, the base is ignored
+   * createWebHashHistory('/iAmIgnored') // gives a url of `file:///usr/etc/folder/index.html#`
+   * ```
+   */
+  function createWebHashHistory(base) {
+      // Make sure this implementation is fine in terms of encoding, specially for IE11
+      // for `file://`, directly use the pathname and ignore the base
+      // location.pathname contains an initial `/` even at the root: `https://example.com`
+      base = location.host ? base || location.pathname + location.search : '';
+      // allow the user to provide a `#` in the middle: `/base/#/app`
+      if (!base.includes('#'))
+          base += '#';
+      if (!base.endsWith('#/') && !base.endsWith('#')) {
+          warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`);
+      }
+      return createWebHistory(base);
+  }
+
+  function isRouteLocation(route) {
+      return typeof route === 'string' || (route && typeof route === 'object');
+  }
+  function isRouteName(name) {
+      return typeof name === 'string' || typeof name === 'symbol';
+  }
+
+  /**
+   * Initial route location where the router is. Can be used in navigation guards
+   * to differentiate the initial navigation.
+   *
+   * @example
+   * ```js
+   * import { START_LOCATION } from 'vue-router'
+   *
+   * router.beforeEach((to, from) => {
+   *   if (from === START_LOCATION) {
+   *     // initial navigation
+   *   }
+   * })
+   * ```
+   */
+  const START_LOCATION_NORMALIZED = {
+      path: '/',
+      name: undefined,
+      params: {},
+      query: {},
+      hash: '',
+      fullPath: '/',
+      matched: [],
+      meta: {},
+      redirectedFrom: undefined,
+  };
+
+  const NavigationFailureSymbol = Symbol('navigation failure' );
+  /**
+   * Enumeration with all possible types for navigation failures. Can be passed to
+   * {@link isNavigationFailure} to check for specific failures.
+   */
+  exports.NavigationFailureType = void 0;
+  (function (NavigationFailureType) {
+      /**
+       * An aborted navigation is a navigation that failed because a navigation
+       * guard returned `false` or called `next(false)`
+       */
+      NavigationFailureType[NavigationFailureType["aborted"] = 4] = "aborted";
+      /**
+       * A cancelled navigation is a navigation that failed because a more recent
+       * navigation finished started (not necessarily finished).
+       */
+      NavigationFailureType[NavigationFailureType["cancelled"] = 8] = "cancelled";
+      /**
+       * A duplicated navigation is a navigation that failed because it was
+       * initiated while already being at the exact same location.
+       */
+      NavigationFailureType[NavigationFailureType["duplicated"] = 16] = "duplicated";
+  })(exports.NavigationFailureType || (exports.NavigationFailureType = {}));
+  // DEV only debug messages
+  const ErrorTypeMessages = {
+      [1 /* ErrorTypes.MATCHER_NOT_FOUND */]({ location, currentLocation }) {
+          return `No match for\n ${JSON.stringify(location)}${currentLocation
+            ? '\nwhile being at\n' + JSON.stringify(currentLocation)
+            : ''}`;
+      },
+      [2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */]({ from, to, }) {
+          return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard.`;
+      },
+      [4 /* ErrorTypes.NAVIGATION_ABORTED */]({ from, to }) {
+          return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard.`;
+      },
+      [8 /* ErrorTypes.NAVIGATION_CANCELLED */]({ from, to }) {
+          return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new navigation.`;
+      },
+      [16 /* ErrorTypes.NAVIGATION_DUPLICATED */]({ from, to }) {
+          return `Avoided redundant navigation to current location: "${from.fullPath}".`;
+      },
+  };
+  /**
+   * Creates a typed NavigationFailure object.
+   * @internal
+   * @param type - NavigationFailureType
+   * @param params - { from, to }
+   */
+  function createRouterError(type, params) {
+      // keep full error messages in cjs versions
+      {
+          return assign(new Error(ErrorTypeMessages[type](params)), {
+              type,
+              [NavigationFailureSymbol]: true,
+          }, params);
+      }
+  }
+  function isNavigationFailure(error, type) {
+      return (error instanceof Error &&
+          NavigationFailureSymbol in error &&
+          (type == null || !!(error.type & type)));
+  }
+  const propertiesToLog = ['params', 'query', 'hash'];
+  function stringifyRoute(to) {
+      if (typeof to === 'string')
+          return to;
+      if (to.path != null)
+          return to.path;
+      const location = {};
+      for (const key of propertiesToLog) {
+          if (key in to)
+              location[key] = to[key];
+      }
+      return JSON.stringify(location, null, 2);
+  }
+
+  // default pattern for a param: non-greedy everything but /
+  const BASE_PARAM_PATTERN = '[^/]+?';
+  const BASE_PATH_PARSER_OPTIONS = {
+      sensitive: false,
+      strict: false,
+      start: true,
+      end: true,
+  };
+  // Special Regex characters that must be escaped in static tokens
+  const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
+  /**
+   * Creates a path parser from an array of Segments (a segment is an array of Tokens)
+   *
+   * @param segments - array of segments returned by tokenizePath
+   * @param extraOptions - optional options for the regexp
+   * @returns a PathParser
+   */
+  function tokensToParser(segments, extraOptions) {
+      const options = assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
+      // the amount of scores is the same as the length of segments except for the root segment "/"
+      const score = [];
+      // the regexp as a string
+      let pattern = options.start ? '^' : '';
+      // extracted keys
+      const keys = [];
+      for (const segment of segments) {
+          // the root segment needs special treatment
+          const segmentScores = segment.length ? [] : [90 /* PathScore.Root */];
+          // allow trailing slash
+          if (options.strict && !segment.length)
+              pattern += '/';
+          for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
+              const token = segment[tokenIndex];
+              // resets the score if we are inside a sub-segment /:a-other-:b
+              let subSegmentScore = 40 /* PathScore.Segment */ +
+                  (options.sensitive ? 0.25 /* PathScore.BonusCaseSensitive */ : 0);
+              if (token.type === 0 /* TokenType.Static */) {
+                  // prepend the slash if we are starting a new segment
+                  if (!tokenIndex)
+                      pattern += '/';
+                  pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
+                  subSegmentScore += 40 /* PathScore.Static */;
+              }
+              else if (token.type === 1 /* TokenType.Param */) {
+                  const { value, repeatable, optional, regexp } = token;
+                  keys.push({
+                      name: value,
+                      repeatable,
+                      optional,
+                  });
+                  const re = regexp ? regexp : BASE_PARAM_PATTERN;
+                  // the user provided a custom regexp /:id(\\d+)
+                  if (re !== BASE_PARAM_PATTERN) {
+                      subSegmentScore += 10 /* PathScore.BonusCustomRegExp */;
+                      // make sure the regexp is valid before using it
+                      try {
+                          new RegExp(`(${re})`);
+                      }
+                      catch (err) {
+                          throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
+                              err.message);
+                      }
+                  }
+                  // when we repeat we must take care of the repeating leading slash
+                  let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
+                  // prepend the slash if we are starting a new segment
+                  if (!tokenIndex)
+                      subPattern =
+                          // avoid an optional / if there are more segments e.g. /:p?-static
+                          // or /:p?-:p2
+                          optional && segment.length < 2
+                              ? `(?:/${subPattern})`
+                              : '/' + subPattern;
+                  if (optional)
+                      subPattern += '?';
+                  pattern += subPattern;
+                  subSegmentScore += 20 /* PathScore.Dynamic */;
+                  if (optional)
+                      subSegmentScore += -8 /* PathScore.BonusOptional */;
+                  if (repeatable)
+                      subSegmentScore += -20 /* PathScore.BonusRepeatable */;
+                  if (re === '.*')
+                      subSegmentScore += -50 /* PathScore.BonusWildcard */;
+              }
+              segmentScores.push(subSegmentScore);
+          }
+          // an empty array like /home/ -> [[{home}], []]
+          // if (!segment.length) pattern += '/'
+          score.push(segmentScores);
+      }
+      // only apply the strict bonus to the last score
+      if (options.strict && options.end) {
+          const i = score.length - 1;
+          score[i][score[i].length - 1] += 0.7000000000000001 /* PathScore.BonusStrict */;
+      }
+      // TODO: dev only warn double trailing slash
+      if (!options.strict)
+          pattern += '/?';
+      if (options.end)
+          pattern += '$';
+      // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
+      else if (options.strict)
+          pattern += '(?:/|$)';
+      const re = new RegExp(pattern, options.sensitive ? '' : 'i');
+      function parse(path) {
+          const match = path.match(re);
+          const params = {};
+          if (!match)
+              return null;
+          for (let i = 1; i < match.length; i++) {
+              const value = match[i] || '';
+              const key = keys[i - 1];
+              params[key.name] = value && key.repeatable ? value.split('/') : value;
+          }
+          return params;
+      }
+      function stringify(params) {
+          let path = '';
+          // for optional parameters to allow to be empty
+          let avoidDuplicatedSlash = false;
+          for (const segment of segments) {
+              if (!avoidDuplicatedSlash || !path.endsWith('/'))
+                  path += '/';
+              avoidDuplicatedSlash = false;
+              for (const token of segment) {
+                  if (token.type === 0 /* TokenType.Static */) {
+                      path += token.value;
+                  }
+                  else if (token.type === 1 /* TokenType.Param */) {
+                      const { value, repeatable, optional } = token;
+                      const param = value in params ? params[value] : '';
+                      if (isArray(param) && !repeatable) {
+                          throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
+                      }
+                      const text = isArray(param)
+                          ? param.join('/')
+                          : param;
+                      if (!text) {
+                          if (optional) {
+                              // if we have more than one optional param like /:a?-static we don't need to care about the optional param
+                              if (segment.length < 2) {
+                                  // remove the last slash as we could be at the end
+                                  if (path.endsWith('/'))
+                                      path = path.slice(0, -1);
+                                  // do not append a slash on the next iteration
+                                  else
+                                      avoidDuplicatedSlash = true;
+                              }
+                          }
+                          else
+                              throw new Error(`Missing required param "${value}"`);
+                      }
+                      path += text;
+                  }
+              }
+          }
+          // avoid empty path when we have multiple optional params
+          return path || '/';
+      }
+      return {
+          re,
+          score,
+          keys,
+          parse,
+          stringify,
+      };
+  }
+  /**
+   * Compares an array of numbers as used in PathParser.score and returns a
+   * number. This function can be used to `sort` an array
+   *
+   * @param a - first array of numbers
+   * @param b - second array of numbers
+   * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
+   * should be sorted first
+   */
+  function compareScoreArray(a, b) {
+      let i = 0;
+      while (i < a.length && i < b.length) {
+          const diff = b[i] - a[i];
+          // only keep going if diff === 0
+          if (diff)
+              return diff;
+          i++;
+      }
+      // if the last subsegment was Static, the shorter segments should be sorted first
+      // otherwise sort the longest segment first
+      if (a.length < b.length) {
+          return a.length === 1 && a[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
+              ? -1
+              : 1;
+      }
+      else if (a.length > b.length) {
+          return b.length === 1 && b[0] === 40 /* PathScore.Static */ + 40 /* PathScore.Segment */
+              ? 1
+              : -1;
+      }
+      return 0;
+  }
+  /**
+   * Compare function that can be used with `sort` to sort an array of PathParser
+   *
+   * @param a - first PathParser
+   * @param b - second PathParser
+   * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
+   */
+  function comparePathParserScore(a, b) {
+      let i = 0;
+      const aScore = a.score;
+      const bScore = b.score;
+      while (i < aScore.length && i < bScore.length) {
+          const comp = compareScoreArray(aScore[i], bScore[i]);
+          // do not return if both are equal
+          if (comp)
+              return comp;
+          i++;
+      }
+      if (Math.abs(bScore.length - aScore.length) === 1) {
+          if (isLastScoreNegative(aScore))
+              return 1;
+          if (isLastScoreNegative(bScore))
+              return -1;
+      }
+      // if a and b share the same score entries but b has more, sort b first
+      return bScore.length - aScore.length;
+      // this is the ternary version
+      // return aScore.length < bScore.length
+      //   ? 1
+      //   : aScore.length > bScore.length
+      //   ? -1
+      //   : 0
+  }
+  /**
+   * This allows detecting splats at the end of a path: /home/:id(.*)*
+   *
+   * @param score - score to check
+   * @returns true if the last entry is negative
+   */
+  function isLastScoreNegative(score) {
+      const last = score[score.length - 1];
+      return score.length > 0 && last[last.length - 1] < 0;
+  }
+
+  const ROOT_TOKEN = {
+      type: 0 /* TokenType.Static */,
+      value: '',
+  };
+  const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
+  // After some profiling, the cache seems to be unnecessary because tokenizePath
+  // (the slowest part of adding a route) is very fast
+  // const tokenCache = new Map<string, Token[][]>()
+  function tokenizePath(path) {
+      if (!path)
+          return [[]];
+      if (path === '/')
+          return [[ROOT_TOKEN]];
+      if (!path.startsWith('/')) {
+          throw new Error(`Route paths should start with a "/": "${path}" should be "/${path}".`
+              );
+      }
+      // if (tokenCache.has(path)) return tokenCache.get(path)!
+      function crash(message) {
+          throw new Error(`ERR (${state})/"${buffer}": ${message}`);
+      }
+      let state = 0 /* TokenizerState.Static */;
+      let previousState = state;
+      const tokens = [];
+      // the segment will always be valid because we get into the initial state
+      // with the leading /
+      let segment;
+      function finalizeSegment() {
+          if (segment)
+              tokens.push(segment);
+          segment = [];
+      }
+      // index on the path
+      let i = 0;
+      // char at index
+      let char;
+      // buffer of the value read
+      let buffer = '';
+      // custom regexp for a param
+      let customRe = '';
+      function consumeBuffer() {
+          if (!buffer)
+              return;
+          if (state === 0 /* TokenizerState.Static */) {
+              segment.push({
+                  type: 0 /* TokenType.Static */,
+                  value: buffer,
+              });
+          }
+          else if (state === 1 /* TokenizerState.Param */ ||
+              state === 2 /* TokenizerState.ParamRegExp */ ||
+              state === 3 /* TokenizerState.ParamRegExpEnd */) {
+              if (segment.length > 1 && (char === '*' || char === '+'))
+                  crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`);
+              segment.push({
+                  type: 1 /* TokenType.Param */,
+                  value: buffer,
+                  regexp: customRe,
+                  repeatable: char === '*' || char === '+',
+                  optional: char === '*' || char === '?',
+              });
+          }
+          else {
+              crash('Invalid state to consume buffer');
+          }
+          buffer = '';
+      }
+      function addCharToBuffer() {
+          buffer += char;
+      }
+      while (i < path.length) {
+          char = path[i++];
+          if (char === '\\' && state !== 2 /* TokenizerState.ParamRegExp */) {
+              previousState = state;
+              state = 4 /* TokenizerState.EscapeNext */;
+              continue;
+          }
+          switch (state) {
+              case 0 /* TokenizerState.Static */:
+                  if (char === '/') {
+                      if (buffer) {
+                          consumeBuffer();
+                      }
+                      finalizeSegment();
+                  }
+                  else if (char === ':') {
+                      consumeBuffer();
+                      state = 1 /* TokenizerState.Param */;
+                  }
+                  else {
+                      addCharToBuffer();
+                  }
+                  break;
+              case 4 /* TokenizerState.EscapeNext */:
+                  addCharToBuffer();
+                  state = previousState;
+                  break;
+              case 1 /* TokenizerState.Param */:
+                  if (char === '(') {
+                      state = 2 /* TokenizerState.ParamRegExp */;
+                  }
+                  else if (VALID_PARAM_RE.test(char)) {
+                      addCharToBuffer();
+                  }
+                  else {
+                      consumeBuffer();
+                      state = 0 /* TokenizerState.Static */;
+                      // go back one character if we were not modifying
+                      if (char !== '*' && char !== '?' && char !== '+')
+                          i--;
+                  }
+                  break;
+              case 2 /* TokenizerState.ParamRegExp */:
+                  // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
+                  // it already works by escaping the closing )
+                  // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
+                  // is this really something people need since you can also write
+                  // /prefix_:p()_suffix
+                  if (char === ')') {
+                      // handle the escaped )
+                      if (customRe[customRe.length - 1] == '\\')
+                          customRe = customRe.slice(0, -1) + char;
+                      else
+                          state = 3 /* TokenizerState.ParamRegExpEnd */;
+                  }
+                  else {
+                      customRe += char;
+                  }
+                  break;
+              case 3 /* TokenizerState.ParamRegExpEnd */:
+                  // same as finalizing a param
+                  consumeBuffer();
+                  state = 0 /* TokenizerState.Static */;
+                  // go back one character if we were not modifying
+                  if (char !== '*' && char !== '?' && char !== '+')
+                      i--;
+                  customRe = '';
+                  break;
+              default:
+                  crash('Unknown state');
+                  break;
+          }
+      }
+      if (state === 2 /* TokenizerState.ParamRegExp */)
+          crash(`Unfinished custom RegExp for param "${buffer}"`);
+      consumeBuffer();
+      finalizeSegment();
+      // tokenCache.set(path, tokens)
+      return tokens;
+  }
+
+  function createRouteRecordMatcher(record, parent, options) {
+      const parser = tokensToParser(tokenizePath(record.path), options);
+      // warn against params with the same name
+      {
+          const existingKeys = new Set();
+          for (const key of parser.keys) {
+              if (existingKeys.has(key.name))
+                  warn(`Found duplicated params with name "${key.name}" for path "${record.path}". Only the last one will be available on "$route.params".`);
+              existingKeys.add(key.name);
+          }
+      }
+      const matcher = assign(parser, {
+          record,
+          parent,
+          // these needs to be populated by the parent
+          children: [],
+          alias: [],
+      });
+      if (parent) {
+          // both are aliases or both are not aliases
+          // we don't want to mix them because the order is used when
+          // passing originalRecord in Matcher.addRoute
+          if (!matcher.record.aliasOf === !parent.record.aliasOf)
+              parent.children.push(matcher);
+      }
+      return matcher;
+  }
+
+  /**
+   * Creates a Router Matcher.
+   *
+   * @internal
+   * @param routes - array of initial routes
+   * @param globalOptions - global route options
+   */
+  function createRouterMatcher(routes, globalOptions) {
+      // normalized ordered array of matchers
+      const matchers = [];
+      const matcherMap = new Map();
+      globalOptions = mergeOptions({ strict: false, end: true, sensitive: false }, globalOptions);
+      function getRecordMatcher(name) {
+          return matcherMap.get(name);
+      }
+      function addRoute(record, parent, originalRecord) {
+          // used later on to remove by name
+          const isRootAdd = !originalRecord;
+          const mainNormalizedRecord = normalizeRouteRecord(record);
+          {
+              checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent);
+          }
+          // we might be the child of an alias
+          mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record;
+          const options = mergeOptions(globalOptions, record);
+          // generate an array of records to correctly handle aliases
+          const normalizedRecords = [
+              mainNormalizedRecord,
+          ];
+          if ('alias' in record) {
+              const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
+              for (const alias of aliases) {
+                  normalizedRecords.push(assign({}, mainNormalizedRecord, {
+                      // this allows us to hold a copy of the `components` option
+                      // so that async components cache is hold on the original record
+                      components: originalRecord
+                          ? originalRecord.record.components
+                          : mainNormalizedRecord.components,
+                      path: alias,
+                      // we might be the child of an alias
+                      aliasOf: originalRecord
+                          ? originalRecord.record
+                          : mainNormalizedRecord,
+                      // the aliases are always of the same kind as the original since they
+                      // are defined on the same record
+                  }));
+              }
+          }
+          let matcher;
+          let originalMatcher;
+          for (const normalizedRecord of normalizedRecords) {
+              const { path } = normalizedRecord;
+              // Build up the path for nested routes if the child isn't an absolute
+              // route. Only add the / delimiter if the child path isn't empty and if the
+              // parent path doesn't have a trailing slash
+              if (parent && path[0] !== '/') {
+                  const parentPath = parent.record.path;
+                  const connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
+                  normalizedRecord.path =
+                      parent.record.path + (path && connectingSlash + path);
+              }
+              if (normalizedRecord.path === '*') {
+                  throw new Error('Catch all routes ("*") must now be defined using a param with a custom regexp.\n' +
+                      'See more at https://next.router.vuejs.org/guide/migration/#removed-star-or-catch-all-routes.');
+              }
+              // create the object beforehand, so it can be passed to children
+              matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
+              if (parent && path[0] === '/')
+                  checkMissingParamsInAbsolutePath(matcher, parent);
+              // if we are an alias we must tell the original record that we exist,
+              // so we can be removed
+              if (originalRecord) {
+                  originalRecord.alias.push(matcher);
+                  {
+                      checkSameParams(originalRecord, matcher);
+                  }
+              }
+              else {
+                  // otherwise, the first record is the original and others are aliases
+                  originalMatcher = originalMatcher || matcher;
+                  if (originalMatcher !== matcher)
+                      originalMatcher.alias.push(matcher);
+                  // remove the route if named and only for the top record (avoid in nested calls)
+                  // this works because the original record is the first one
+                  if (isRootAdd && record.name && !isAliasRecord(matcher))
+                      removeRoute(record.name);
+              }
+              if (mainNormalizedRecord.children) {
+                  const children = mainNormalizedRecord.children;
+                  for (let i = 0; i < children.length; i++) {
+                      addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
+                  }
+              }
+              // if there was no original record, then the first one was not an alias and all
+              // other aliases (if any) need to reference this record when adding children
+              originalRecord = originalRecord || matcher;
+              // TODO: add normalized records for more flexibility
+              // if (parent && isAliasRecord(originalRecord)) {
+              //   parent.children.push(originalRecord)
+              // }
+              // Avoid adding a record that doesn't display anything. This allows passing through records without a component to
+              // not be reached and pass through the catch all route
+              if ((matcher.record.components &&
+                  Object.keys(matcher.record.components).length) ||
+                  matcher.record.name ||
+                  matcher.record.redirect) {
+                  insertMatcher(matcher);
+              }
+          }
+          return originalMatcher
+              ? () => {
+                  // since other matchers are aliases, they should be removed by the original matcher
+                  removeRoute(originalMatcher);
+              }
+              : noop;
+      }
+      function removeRoute(matcherRef) {
+          if (isRouteName(matcherRef)) {
+              const matcher = matcherMap.get(matcherRef);
+              if (matcher) {
+                  matcherMap.delete(matcherRef);
+                  matchers.splice(matchers.indexOf(matcher), 1);
+                  matcher.children.forEach(removeRoute);
+                  matcher.alias.forEach(removeRoute);
+              }
+          }
+          else {
+              const index = matchers.indexOf(matcherRef);
+              if (index > -1) {
+                  matchers.splice(index, 1);
+                  if (matcherRef.record.name)
+                      matcherMap.delete(matcherRef.record.name);
+                  matcherRef.children.forEach(removeRoute);
+                  matcherRef.alias.forEach(removeRoute);
+              }
+          }
+      }
+      function getRoutes() {
+          return matchers;
+      }
+      function insertMatcher(matcher) {
+          let i = 0;
+          while (i < matchers.length &&
+              comparePathParserScore(matcher, matchers[i]) >= 0 &&
+              // Adding children with empty path should still appear before the parent
+              // https://github.com/vuejs/router/issues/1124
+              (matcher.record.path !== matchers[i].record.path ||
+                  !isRecordChildOf(matcher, matchers[i])))
+              i++;
+          matchers.splice(i, 0, matcher);
+          // only add the original record to the name map
+          if (matcher.record.name && !isAliasRecord(matcher))
+              matcherMap.set(matcher.record.name, matcher);
+      }
+      function resolve(location, currentLocation) {
+          let matcher;
+          let params = {};
+          let path;
+          let name;
+          if ('name' in location && location.name) {
+              matcher = matcherMap.get(location.name);
+              if (!matcher)
+                  throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, {
+                      location,
+                  });
+              // warn if the user is passing invalid params so they can debug it better when they get removed
+              {
+                  const invalidParams = Object.keys(location.params || {}).filter(paramName => !matcher.keys.find(k => k.name === paramName));
+                  if (invalidParams.length) {
+                      warn(`Discarded invalid param(s) "${invalidParams.join('", "')}" when navigating. See https://github.com/vuejs/router/blob/main/packages/router/CHANGELOG.md#414-2022-08-22 for more details.`);
+                  }
+              }
+              name = matcher.record.name;
+              params = assign(
+              // paramsFromLocation is a new object
+              paramsFromLocation(currentLocation.params, 
+              // only keep params that exist in the resolved location
+              // only keep optional params coming from a parent record
+              matcher.keys
+                  .filter(k => !k.optional)
+                  .concat(matcher.parent ? matcher.parent.keys.filter(k => k.optional) : [])
+                  .map(k => k.name)), 
+              // discard any existing params in the current location that do not exist here
+              // #1497 this ensures better active/exact matching
+              location.params &&
+                  paramsFromLocation(location.params, matcher.keys.map(k => k.name)));
+              // throws if cannot be stringified
+              path = matcher.stringify(params);
+          }
+          else if (location.path != null) {
+              // no need to resolve the path with the matcher as it was provided
+              // this also allows the user to control the encoding
+              path = location.path;
+              if (!path.startsWith('/')) {
+                  warn(`The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://github.com/vuejs/router/issues/new/choose.`);
+              }
+              matcher = matchers.find(m => m.re.test(path));
+              // matcher should have a value after the loop
+              if (matcher) {
+                  // we know the matcher works because we tested the regexp
+                  params = matcher.parse(path);
+                  name = matcher.record.name;
+              }
+              // location is a relative path
+          }
+          else {
+              // match by name or path of current route
+              matcher = currentLocation.name
+                  ? matcherMap.get(currentLocation.name)
+                  : matchers.find(m => m.re.test(currentLocation.path));
+              if (!matcher)
+                  throw createRouterError(1 /* ErrorTypes.MATCHER_NOT_FOUND */, {
+                      location,
+                      currentLocation,
+                  });
+              name = matcher.record.name;
+              // since we are navigating to the same location, we don't need to pick the
+              // params like when `name` is provided
+              params = assign({}, currentLocation.params, location.params);
+              path = matcher.stringify(params);
+          }
+          const matched = [];
+          let parentMatcher = matcher;
+          while (parentMatcher) {
+              // reversed order so parents are at the beginning
+              matched.unshift(parentMatcher.record);
+              parentMatcher = parentMatcher.parent;
+          }
+          return {
+              name,
+              path,
+              params,
+              matched,
+              meta: mergeMetaFields(matched),
+          };
+      }
+      // add initial routes
+      routes.forEach(route => addRoute(route));
+      return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
+  }
+  function paramsFromLocation(params, keys) {
+      const newParams = {};
+      for (const key of keys) {
+          if (key in params)
+              newParams[key] = params[key];
+      }
+      return newParams;
+  }
+  /**
+   * Normalizes a RouteRecordRaw. Creates a copy
+   *
+   * @param record
+   * @returns the normalized version
+   */
+  function normalizeRouteRecord(record) {
+      return {
+          path: record.path,
+          redirect: record.redirect,
+          name: record.name,
+          meta: record.meta || {},
+          aliasOf: undefined,
+          beforeEnter: record.beforeEnter,
+          props: normalizeRecordProps(record),
+          children: record.children || [],
+          instances: {},
+          leaveGuards: new Set(),
+          updateGuards: new Set(),
+          enterCallbacks: {},
+          components: 'components' in record
+              ? record.components || null
+              : record.component && { default: record.component },
+      };
+  }
+  /**
+   * Normalize the optional `props` in a record to always be an object similar to
+   * components. Also accept a boolean for components.
+   * @param record
+   */
+  function normalizeRecordProps(record) {
+      const propsObject = {};
+      // props does not exist on redirect records, but we can set false directly
+      const props = record.props || false;
+      if ('component' in record) {
+          propsObject.default = props;
+      }
+      else {
+          // NOTE: we could also allow a function to be applied to every component.
+          // Would need user feedback for use cases
+          for (const name in record.components)
+              propsObject[name] = typeof props === 'object' ? props[name] : props;
+      }
+      return propsObject;
+  }
+  /**
+   * Checks if a record or any of its parent is an alias
+   * @param record
+   */
+  function isAliasRecord(record) {
+      while (record) {
+          if (record.record.aliasOf)
+              return true;
+          record = record.parent;
+      }
+      return false;
+  }
+  /**
+   * Merge meta fields of an array of records
+   *
+   * @param matched - array of matched records
+   */
+  function mergeMetaFields(matched) {
+      return matched.reduce((meta, record) => assign(meta, record.meta), {});
+  }
+  function mergeOptions(defaults, partialOptions) {
+      const options = {};
+      for (const key in defaults) {
+          options[key] = key in partialOptions ? partialOptions[key] : defaults[key];
+      }
+      return options;
+  }
+  function isSameParam(a, b) {
+      return (a.name === b.name &&
+          a.optional === b.optional &&
+          a.repeatable === b.repeatable);
+  }
+  /**
+   * Check if a path and its alias have the same required params
+   *
+   * @param a - original record
+   * @param b - alias record
+   */
+  function checkSameParams(a, b) {
+      for (const key of a.keys) {
+          if (!key.optional && !b.keys.find(isSameParam.bind(null, key)))
+              return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" must have the exact same param named "${key.name}"`);
+      }
+      for (const key of b.keys) {
+          if (!key.optional && !a.keys.find(isSameParam.bind(null, key)))
+              return warn(`Alias "${b.record.path}" and the original record: "${a.record.path}" must have the exact same param named "${key.name}"`);
+      }
+  }
+  /**
+   * A route with a name and a child with an empty path without a name should warn when adding the route
+   *
+   * @param mainNormalizedRecord - RouteRecordNormalized
+   * @param parent - RouteRecordMatcher
+   */
+  function checkChildMissingNameWithEmptyPath(mainNormalizedRecord, parent) {
+      if (parent &&
+          parent.record.name &&
+          !mainNormalizedRecord.name &&
+          !mainNormalizedRecord.path) {
+          warn(`The route named "${String(parent.record.name)}" has a child without a name and an empty path. Using that name won't render the empty path child so you probably want to move the name to the child instead. If this is intentional, add a name to the child route to remove the warning.`);
+      }
+  }
+  function checkMissingParamsInAbsolutePath(record, parent) {
+      for (const key of parent.keys) {
+          if (!record.keys.find(isSameParam.bind(null, key)))
+              return warn(`Absolute path "${record.record.path}" must have the exact same param named "${key.name}" as its parent "${parent.record.path}".`);
+      }
+  }
+  function isRecordChildOf(record, parent) {
+      return parent.children.some(child => child === record || isRecordChildOf(record, child));
+  }
+
+  /**
+   * Transforms a queryString into a {@link LocationQuery} object. Accept both, a
+   * version with the leading `?` and without Should work as URLSearchParams
+
+   * @internal
+   *
+   * @param search - search string to parse
+   * @returns a query object
+   */
+  function parseQuery(search) {
+      const query = {};
+      // avoid creating an object with an empty key and empty value
+      // because of split('&')
+      if (search === '' || search === '?')
+          return query;
+      const hasLeadingIM = search[0] === '?';
+      const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&');
+      for (let i = 0; i < searchParams.length; ++i) {
+          // pre decode the + into space
+          const searchParam = searchParams[i].replace(PLUS_RE, ' ');
+          // allow the = character
+          const eqPos = searchParam.indexOf('=');
+          const key = decode(eqPos < 0 ? searchParam : searchParam.slice(0, eqPos));
+          const value = eqPos < 0 ? null : decode(searchParam.slice(eqPos + 1));
+          if (key in query) {
+              // an extra variable for ts types
+              let currentValue = query[key];
+              if (!isArray(currentValue)) {
+                  currentValue = query[key] = [currentValue];
+              }
+              currentValue.push(value);
+          }
+          else {
+              query[key] = value;
+          }
+      }
+      return query;
+  }
+  /**
+   * Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it
+   * doesn't prepend a `?`
+   *
+   * @internal
+   *
+   * @param query - query object to stringify
+   * @returns string version of the query without the leading `?`
+   */
+  function stringifyQuery(query) {
+      let search = '';
+      for (let key in query) {
+          const value = query[key];
+          key = encodeQueryKey(key);
+          if (value == null) {
+              // only null adds the value
+              if (value !== undefined) {
+                  search += (search.length ? '&' : '') + key;
+              }
+              continue;
+          }
+          // keep null values
+          const values = isArray(value)
+              ? value.map(v => v && encodeQueryValue(v))
+              : [value && encodeQueryValue(value)];
+          values.forEach(value => {
+              // skip undefined values in arrays as if they were not present
+              // smaller code than using filter
+              if (value !== undefined) {
+                  // only append & with non-empty search
+                  search += (search.length ? '&' : '') + key;
+                  if (value != null)
+                      search += '=' + value;
+              }
+          });
+      }
+      return search;
+  }
+  /**
+   * Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting
+   * numbers into strings, removing keys with an undefined value and replacing
+   * undefined with null in arrays
+   *
+   * @param query - query object to normalize
+   * @returns a normalized query object
+   */
+  function normalizeQuery(query) {
+      const normalizedQuery = {};
+      for (const key in query) {
+          const value = query[key];
+          if (value !== undefined) {
+              normalizedQuery[key] = isArray(value)
+                  ? value.map(v => (v == null ? null : '' + v))
+                  : value == null
+                      ? value
+                      : '' + value;
+          }
+      }
+      return normalizedQuery;
+  }
+
+  /**
+   * RouteRecord being rendered by the closest ancestor Router View. Used for
+   * `onBeforeRouteUpdate` and `onBeforeRouteLeave`. rvlm stands for Router View
+   * Location Matched
+   *
+   * @internal
+   */
+  const matchedRouteKey = Symbol('router view location matched' );
+  /**
+   * Allows overriding the router view depth to control which component in
+   * `matched` is rendered. rvd stands for Router View Depth
+   *
+   * @internal
+   */
+  const viewDepthKey = Symbol('router view depth' );
+  /**
+   * Allows overriding the router instance returned by `useRouter` in tests. r
+   * stands for router
+   *
+   * @internal
+   */
+  const routerKey = Symbol('router' );
+  /**
+   * Allows overriding the current route returned by `useRoute` in tests. rl
+   * stands for route location
+   *
+   * @internal
+   */
+  const routeLocationKey = Symbol('route location' );
+  /**
+   * Allows overriding the current route used by router-view. Internally this is
+   * used when the `route` prop is passed.
+   *
+   * @internal
+   */
+  const routerViewLocationKey = Symbol('router view location' );
+
+  /**
+   * Create a list of callbacks that can be reset. Used to create before and after navigation guards list
+   */
+  function useCallbacks() {
+      let handlers = [];
+      function add(handler) {
+          handlers.push(handler);
+          return () => {
+              const i = handlers.indexOf(handler);
+              if (i > -1)
+                  handlers.splice(i, 1);
+          };
+      }
+      function reset() {
+          handlers = [];
+      }
+      return {
+          add,
+          list: () => handlers.slice(),
+          reset,
+      };
+  }
+
+  function registerGuard(record, name, guard) {
+      const removeFromList = () => {
+          record[name].delete(guard);
+      };
+      vue.onUnmounted(removeFromList);
+      vue.onDeactivated(removeFromList);
+      vue.onActivated(() => {
+          record[name].add(guard);
+      });
+      record[name].add(guard);
+  }
+  /**
+   * Add a navigation guard that triggers whenever the component for the current
+   * location is about to be left. Similar to {@link beforeRouteLeave} but can be
+   * used in any component. The guard is removed when the component is unmounted.
+   *
+   * @param leaveGuard - {@link NavigationGuard}
+   */
+  function onBeforeRouteLeave(leaveGuard) {
+      if (!vue.getCurrentInstance()) {
+          warn('getCurrentInstance() returned null. onBeforeRouteLeave() must be called at the top of a setup function');
+          return;
+      }
+      const activeRecord = vue.inject(matchedRouteKey, 
+      // to avoid warning
+      {}).value;
+      if (!activeRecord) {
+          warn('No active route record was found when calling `onBeforeRouteLeave()`. Make sure you call this function inside a component child of <router-view>. Maybe you called it inside of App.vue?');
+          return;
+      }
+      registerGuard(activeRecord, 'leaveGuards', leaveGuard);
+  }
+  /**
+   * Add a navigation guard that triggers whenever the current location is about
+   * to be updated. Similar to {@link beforeRouteUpdate} but can be used in any
+   * component. The guard is removed when the component is unmounted.
+   *
+   * @param updateGuard - {@link NavigationGuard}
+   */
+  function onBeforeRouteUpdate(updateGuard) {
+      if (!vue.getCurrentInstance()) {
+          warn('getCurrentInstance() returned null. onBeforeRouteUpdate() must be called at the top of a setup function');
+          return;
+      }
+      const activeRecord = vue.inject(matchedRouteKey, 
+      // to avoid warning
+      {}).value;
+      if (!activeRecord) {
+          warn('No active route record was found when calling `onBeforeRouteUpdate()`. Make sure you call this function inside a component child of <router-view>. Maybe you called it inside of App.vue?');
+          return;
+      }
+      registerGuard(activeRecord, 'updateGuards', updateGuard);
+  }
+  function guardToPromiseFn(guard, to, from, record, name, runWithContext = fn => fn()) {
+      // keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
+      const enterCallbackArray = record &&
+          // name is defined if record is because of the function overload
+          (record.enterCallbacks[name] = record.enterCallbacks[name] || []);
+      return () => new Promise((resolve, reject) => {
+          const next = (valid) => {
+              if (valid === false) {
+                  reject(createRouterError(4 /* ErrorTypes.NAVIGATION_ABORTED */, {
+                      from,
+                      to,
+                  }));
+              }
+              else if (valid instanceof Error) {
+                  reject(valid);
+              }
+              else if (isRouteLocation(valid)) {
+                  reject(createRouterError(2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */, {
+                      from: to,
+                      to: valid,
+                  }));
+              }
+              else {
+                  if (enterCallbackArray &&
+                      // since enterCallbackArray is truthy, both record and name also are
+                      record.enterCallbacks[name] === enterCallbackArray &&
+                      typeof valid === 'function') {
+                      enterCallbackArray.push(valid);
+                  }
+                  resolve();
+              }
+          };
+          // wrapping with Promise.resolve allows it to work with both async and sync guards
+          const guardReturn = runWithContext(() => guard.call(record && record.instances[name], to, from, canOnlyBeCalledOnce(next, to, from) ));
+          let guardCall = Promise.resolve(guardReturn);
+          if (guard.length < 3)
+              guardCall = guardCall.then(next);
+          if (guard.length > 2) {
+              const message = `The "next" callback was never called inside of ${guard.name ? '"' + guard.name + '"' : ''}:\n${guard.toString()}\n. If you are returning a value instead of calling "next", make sure to remove the "next" parameter from your function.`;
+              if (typeof guardReturn === 'object' && 'then' in guardReturn) {
+                  guardCall = guardCall.then(resolvedValue => {
+                      // @ts-expect-error: _called is added at canOnlyBeCalledOnce
+                      if (!next._called) {
+                          warn(message);
+                          return Promise.reject(new Error('Invalid navigation guard'));
+                      }
+                      return resolvedValue;
+                  });
+              }
+              else if (guardReturn !== undefined) {
+                  // @ts-expect-error: _called is added at canOnlyBeCalledOnce
+                  if (!next._called) {
+                      warn(message);
+                      reject(new Error('Invalid navigation guard'));
+                      return;
+                  }
+              }
+          }
+          guardCall.catch(err => reject(err));
+      });
+  }
+  function canOnlyBeCalledOnce(next, to, from) {
+      let called = 0;
+      return function () {
+          if (called++ === 1)
+              warn(`The "next" callback was called more than once in one navigation guard when going from "${from.fullPath}" to "${to.fullPath}". It should be called exactly one time in each navigation guard. This will fail in production.`);
+          // @ts-expect-error: we put it in the original one because it's easier to check
+          next._called = true;
+          if (called === 1)
+              next.apply(null, arguments);
+      };
+  }
+  function extractComponentsGuards(matched, guardType, to, from, runWithContext = fn => fn()) {
+      const guards = [];
+      for (const record of matched) {
+          if (!record.components && !record.children.length) {
+              warn(`Record with path "${record.path}" is either missing a "component(s)"` +
+                  ` or "children" property.`);
+          }
+          for (const name in record.components) {
+              let rawComponent = record.components[name];
+              {
+                  if (!rawComponent ||
+                      (typeof rawComponent !== 'object' &&
+                          typeof rawComponent !== 'function')) {
+                      warn(`Component "${name}" in record with path "${record.path}" is not` +
+                          ` a valid component. Received "${String(rawComponent)}".`);
+                      // throw to ensure we stop here but warn to ensure the message isn't
+                      // missed by the user
+                      throw new Error('Invalid route component');
+                  }
+                  else if ('then' in rawComponent) {
+                      // warn if user wrote import('/component.vue') instead of () =>
+                      // import('./component.vue')
+                      warn(`Component "${name}" in record with path "${record.path}" is a ` +
+                          `Promise instead of a function that returns a Promise. Did you ` +
+                          `write "import('./MyPage.vue')" instead of ` +
+                          `"() => import('./MyPage.vue')" ? This will break in ` +
+                          `production if not fixed.`);
+                      const promise = rawComponent;
+                      rawComponent = () => promise;
+                  }
+                  else if (rawComponent.__asyncLoader &&
+                      // warn only once per component
+                      !rawComponent.__warnedDefineAsync) {
+                      rawComponent.__warnedDefineAsync = true;
+                      warn(`Component "${name}" in record with path "${record.path}" is defined ` +
+                          `using "defineAsyncComponent()". ` +
+                          `Write "() => import('./MyPage.vue')" instead of ` +
+                          `"defineAsyncComponent(() => import('./MyPage.vue'))".`);
+                  }
+              }
+              // skip update and leave guards if the route component is not mounted
+              if (guardType !== 'beforeRouteEnter' && !record.instances[name])
+                  continue;
+              if (isRouteComponent(rawComponent)) {
+                  // __vccOpts is added by vue-class-component and contain the regular options
+                  const options = rawComponent.__vccOpts || rawComponent;
+                  const guard = options[guardType];
+                  guard &&
+                      guards.push(guardToPromiseFn(guard, to, from, record, name, runWithContext));
+              }
+              else {
+                  // start requesting the chunk already
+                  let componentPromise = rawComponent();
+                  if (!('catch' in componentPromise)) {
+                      warn(`Component "${name}" in record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`);
+                      componentPromise = Promise.resolve(componentPromise);
+                  }
+                  guards.push(() => componentPromise.then(resolved => {
+                      if (!resolved)
+                          return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}"`));
+                      const resolvedComponent = isESModule(resolved)
+                          ? resolved.default
+                          : resolved;
+                      // replace the function with the resolved component
+                      // cannot be null or undefined because we went into the for loop
+                      record.components[name] = resolvedComponent;
+                      // __vccOpts is added by vue-class-component and contain the regular options
+                      const options = resolvedComponent.__vccOpts || resolvedComponent;
+                      const guard = options[guardType];
+                      return (guard &&
+                          guardToPromiseFn(guard, to, from, record, name, runWithContext)());
+                  }));
+              }
+          }
+      }
+      return guards;
+  }
+  /**
+   * Allows differentiating lazy components from functional components and vue-class-component
+   * @internal
+   *
+   * @param component
+   */
+  function isRouteComponent(component) {
+      return (typeof component === 'object' ||
+          'displayName' in component ||
+          'props' in component ||
+          '__vccOpts' in component);
+  }
+  /**
+   * Ensures a route is loaded, so it can be passed as o prop to `<RouterView>`.
+   *
+   * @param route - resolved route to load
+   */
+  function loadRouteLocation(route) {
+      return route.matched.every(record => record.redirect)
+          ? Promise.reject(new Error('Cannot load a route that redirects.'))
+          : Promise.all(route.matched.map(record => record.components &&
+              Promise.all(Object.keys(record.components).reduce((promises, name) => {
+                  const rawComponent = record.components[name];
+                  if (typeof rawComponent === 'function' &&
+                      !('displayName' in rawComponent)) {
+                      promises.push(rawComponent().then(resolved => {
+                          if (!resolved)
+                              return Promise.reject(new Error(`Couldn't resolve component "${name}" at "${record.path}". Ensure you passed a function that returns a promise.`));
+                          const resolvedComponent = isESModule(resolved)
+                              ? resolved.default
+                              : resolved;
+                          // replace the function with the resolved component
+                          // cannot be null or undefined because we went into the for loop
+                          record.components[name] = resolvedComponent;
+                          return;
+                      }));
+                  }
+                  return promises;
+              }, [])))).then(() => route);
+  }
+
+  // TODO: we could allow currentRoute as a prop to expose `isActive` and
+  // `isExactActive` behavior should go through an RFC
+  function useLink(props) {
+      const router = vue.inject(routerKey);
+      const currentRoute = vue.inject(routeLocationKey);
+      let hasPrevious = false;
+      let previousTo = null;
+      const route = vue.computed(() => {
+          const to = vue.unref(props.to);
+          if ((!hasPrevious || to !== previousTo)) {
+              if (!isRouteLocation(to)) {
+                  if (hasPrevious) {
+                      warn(`Invalid value for prop "to" in useLink()\n- to:`, to, `\n- previous to:`, previousTo, `\n- props:`, props);
+                  }
+                  else {
+                      warn(`Invalid value for prop "to" in useLink()\n- to:`, to, `\n- props:`, props);
+                  }
+              }
+              previousTo = to;
+              hasPrevious = true;
+          }
+          return router.resolve(to);
+      });
+      const activeRecordIndex = vue.computed(() => {
+          const { matched } = route.value;
+          const { length } = matched;
+          const routeMatched = matched[length - 1];
+          const currentMatched = currentRoute.matched;
+          if (!routeMatched || !currentMatched.length)
+              return -1;
+          const index = currentMatched.findIndex(isSameRouteRecord.bind(null, routeMatched));
+          if (index > -1)
+              return index;
+          // possible parent record
+          const parentRecordPath = getOriginalPath(matched[length - 2]);
+          return (
+          // we are dealing with nested routes
+          length > 1 &&
+              // if the parent and matched route have the same path, this link is
+              // referring to the empty child. Or we currently are on a different
+              // child of the same parent
+              getOriginalPath(routeMatched) === parentRecordPath &&
+              // avoid comparing the child with its parent
+              currentMatched[currentMatched.length - 1].path !== parentRecordPath
+              ? currentMatched.findIndex(isSameRouteRecord.bind(null, matched[length - 2]))
+              : index);
+      });
+      const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
+          includesParams(currentRoute.params, route.value.params));
+      const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
+          activeRecordIndex.value === currentRoute.matched.length - 1 &&
+          isSameRouteLocationParams(currentRoute.params, route.value.params));
+      function navigate(e = {}) {
+          if (guardEvent(e)) {
+              return router[vue.unref(props.replace) ? 'replace' : 'push'](vue.unref(props.to)
+              // avoid uncaught errors are they are logged anyway
+              ).catch(noop);
+          }
+          return Promise.resolve();
+      }
+      // devtools only
+      if (isBrowser) {
+          const instance = vue.getCurrentInstance();
+          if (instance) {
+              const linkContextDevtools = {
+                  route: route.value,
+                  isActive: isActive.value,
+                  isExactActive: isExactActive.value,
+                  error: null,
+              };
+              // @ts-expect-error: this is internal
+              instance.__vrl_devtools = instance.__vrl_devtools || [];
+              // @ts-expect-error: this is internal
+              instance.__vrl_devtools.push(linkContextDevtools);
+              vue.watchEffect(() => {
+                  linkContextDevtools.route = route.value;
+                  linkContextDevtools.isActive = isActive.value;
+                  linkContextDevtools.isExactActive = isExactActive.value;
+                  linkContextDevtools.error = isRouteLocation(vue.unref(props.to))
+                      ? null
+                      : 'Invalid "to" value';
+              }, { flush: 'post' });
+          }
+      }
+      /**
+       * NOTE: update {@link _RouterLinkI}'s `$slots` type when updating this
+       */
+      return {
+          route,
+          href: vue.computed(() => route.value.href),
+          isActive,
+          isExactActive,
+          navigate,
+      };
+  }
+  const RouterLinkImpl = /*#__PURE__*/ vue.defineComponent({
+      name: 'RouterLink',
+      compatConfig: { MODE: 3 },
+      props: {
+          to: {
+              type: [String, Object],
+              required: true,
+          },
+          replace: Boolean,
+          activeClass: String,
+          // inactiveClass: String,
+          exactActiveClass: String,
+          custom: Boolean,
+          ariaCurrentValue: {
+              type: String,
+              default: 'page',
+          },
+      },
+      useLink,
+      setup(props, { slots }) {
+          const link = vue.reactive(useLink(props));
+          const { options } = vue.inject(routerKey);
+          const elClass = vue.computed(() => ({
+              [getLinkClass(props.activeClass, options.linkActiveClass, 'router-link-active')]: link.isActive,
+              // [getLinkClass(
+              //   props.inactiveClass,
+              //   options.linkInactiveClass,
+              //   'router-link-inactive'
+              // )]: !link.isExactActive,
+              [getLinkClass(props.exactActiveClass, options.linkExactActiveClass, 'router-link-exact-active')]: link.isExactActive,
+          }));
+          return () => {
+              const children = slots.default && slots.default(link);
+              return props.custom
+                  ? children
+                  : vue.h('a', {
+                      'aria-current': link.isExactActive
+                          ? props.ariaCurrentValue
+                          : null,
+                      href: link.href,
+                      // this would override user added attrs but Vue will still add
+                      // the listener, so we end up triggering both
+                      onClick: link.navigate,
+                      class: elClass.value,
+                  }, children);
+          };
+      },
+  });
+  // export the public type for h/tsx inference
+  // also to avoid inline import() in generated d.ts files
+  /**
+   * Component to render a link that triggers a navigation on click.
+   */
+  const RouterLink = RouterLinkImpl;
+  function guardEvent(e) {
+      // don't redirect with control keys
+      if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
+          return;
+      // don't redirect when preventDefault called
+      if (e.defaultPrevented)
+          return;
+      // don't redirect on right click
+      if (e.button !== undefined && e.button !== 0)
+          return;
+      // don't redirect if `target="_blank"`
+      // @ts-expect-error getAttribute does exist
+      if (e.currentTarget && e.currentTarget.getAttribute) {
+          // @ts-expect-error getAttribute exists
+          const target = e.currentTarget.getAttribute('target');
+          if (/\b_blank\b/i.test(target))
+              return;
+      }
+      // this may be a Weex event which doesn't have this method
+      if (e.preventDefault)
+          e.preventDefault();
+      return true;
+  }
+  function includesParams(outer, inner) {
+      for (const key in inner) {
+          const innerValue = inner[key];
+          const outerValue = outer[key];
+          if (typeof innerValue === 'string') {
+              if (innerValue !== outerValue)
+                  return false;
+          }
+          else {
+              if (!isArray(outerValue) ||
+                  outerValue.length !== innerValue.length ||
+                  innerValue.some((value, i) => value !== outerValue[i]))
+                  return false;
+          }
+      }
+      return true;
+  }
+  /**
+   * Get the original path value of a record by following its aliasOf
+   * @param record
+   */
+  function getOriginalPath(record) {
+      return record ? (record.aliasOf ? record.aliasOf.path : record.path) : '';
+  }
+  /**
+   * Utility class to get the active class based on defaults.
+   * @param propClass
+   * @param globalClass
+   * @param defaultClass
+   */
+  const getLinkClass = (propClass, globalClass, defaultClass) => propClass != null
+      ? propClass
+      : globalClass != null
+          ? globalClass
+          : defaultClass;
+
+  const RouterViewImpl = /*#__PURE__*/ vue.defineComponent({
+      name: 'RouterView',
+      // #674 we manually inherit them
+      inheritAttrs: false,
+      props: {
+          name: {
+              type: String,
+              default: 'default',
+          },
+          route: Object,
+      },
+      // Better compat for @vue/compat users
+      // https://github.com/vuejs/router/issues/1315
+      compatConfig: { MODE: 3 },
+      setup(props, { attrs, slots }) {
+          warnDeprecatedUsage();
+          const injectedRoute = vue.inject(routerViewLocationKey);
+          const routeToDisplay = vue.computed(() => props.route || injectedRoute.value);
+          const injectedDepth = vue.inject(viewDepthKey, 0);
+          // The depth changes based on empty components option, which allows passthrough routes e.g. routes with children
+          // that are used to reuse the `path` property
+          const depth = vue.computed(() => {
+              let initialDepth = vue.unref(injectedDepth);
+              const { matched } = routeToDisplay.value;
+              let matchedRoute;
+              while ((matchedRoute = matched[initialDepth]) &&
+                  !matchedRoute.components) {
+                  initialDepth++;
+              }
+              return initialDepth;
+          });
+          const matchedRouteRef = vue.computed(() => routeToDisplay.value.matched[depth.value]);
+          vue.provide(viewDepthKey, vue.computed(() => depth.value + 1));
+          vue.provide(matchedRouteKey, matchedRouteRef);
+          vue.provide(routerViewLocationKey, routeToDisplay);
+          const viewRef = vue.ref();
+          // watch at the same time the component instance, the route record we are
+          // rendering, and the name
+          vue.watch(() => [viewRef.value, matchedRouteRef.value, props.name], ([instance, to, name], [oldInstance, from, oldName]) => {
+              // copy reused instances
+              if (to) {
+                  // this will update the instance for new instances as well as reused
+                  // instances when navigating to a new route
+                  to.instances[name] = instance;
+                  // the component instance is reused for a different route or name, so
+                  // we copy any saved update or leave guards. With async setup, the
+                  // mounting component will mount before the matchedRoute changes,
+                  // making instance === oldInstance, so we check if guards have been
+                  // added before. This works because we remove guards when
+                  // unmounting/deactivating components
+                  if (from && from !== to && instance && instance === oldInstance) {
+                      if (!to.leaveGuards.size) {
+                          to.leaveGuards = from.leaveGuards;
+                      }
+                      if (!to.updateGuards.size) {
+                          to.updateGuards = from.updateGuards;
+                      }
+                  }
+              }
+              // trigger beforeRouteEnter next callbacks
+              if (instance &&
+                  to &&
+                  // if there is no instance but to and from are the same this might be
+                  // the first visit
+                  (!from || !isSameRouteRecord(to, from) || !oldInstance)) {
+                  (to.enterCallbacks[name] || []).forEach(callback => callback(instance));
+              }
+          }, { flush: 'post' });
+          return () => {
+              const route = routeToDisplay.value;
+              // we need the value at the time we render because when we unmount, we
+              // navigated to a different location so the value is different
+              const currentName = props.name;
+              const matchedRoute = matchedRouteRef.value;
+              const ViewComponent = matchedRoute && matchedRoute.components[currentName];
+              if (!ViewComponent) {
+                  return normalizeSlot(slots.default, { Component: ViewComponent, route });
+              }
+              // props from route configuration
+              const routePropsOption = matchedRoute.props[currentName];
+              const routeProps = routePropsOption
+                  ? routePropsOption === true
+                      ? route.params
+                      : typeof routePropsOption === 'function'
+                          ? routePropsOption(route)
+                          : routePropsOption
+                  : null;
+              const onVnodeUnmounted = vnode => {
+                  // remove the instance reference to prevent leak
+                  if (vnode.component.isUnmounted) {
+                      matchedRoute.instances[currentName] = null;
+                  }
+              };
+              const component = vue.h(ViewComponent, assign({}, routeProps, attrs, {
+                  onVnodeUnmounted,
+                  ref: viewRef,
+              }));
+              if (isBrowser &&
+                  component.ref) {
+                  // TODO: can display if it's an alias, its props
+                  const info = {
+                      depth: depth.value,
+                      name: matchedRoute.name,
+                      path: matchedRoute.path,
+                      meta: matchedRoute.meta,
+                  };
+                  const internalInstances = isArray(component.ref)
+                      ? component.ref.map(r => r.i)
+                      : [component.ref.i];
+                  internalInstances.forEach(instance => {
+                      // @ts-expect-error
+                      instance.__vrv_devtools = info;
+                  });
+              }
+              return (
+              // pass the vnode to the slot as a prop.
+              // h and <component :is="..."> both accept vnodes
+              normalizeSlot(slots.default, { Component: component, route }) ||
+                  component);
+          };
+      },
+  });
+  function normalizeSlot(slot, data) {
+      if (!slot)
+          return null;
+      const slotContent = slot(data);
+      return slotContent.length === 1 ? slotContent[0] : slotContent;
+  }
+  // export the public type for h/tsx inference
+  // also to avoid inline import() in generated d.ts files
+  /**
+   * Component to display the current route the user is at.
+   */
+  const RouterView = RouterViewImpl;
+  // warn against deprecated usage with <transition> & <keep-alive>
+  // due to functional component being no longer eager in Vue 3
+  function warnDeprecatedUsage() {
+      const instance = vue.getCurrentInstance();
+      const parentName = instance.parent && instance.parent.type.name;
+      const parentSubTreeType = instance.parent && instance.parent.subTree && instance.parent.subTree.type;
+      if (parentName &&
+          (parentName === 'KeepAlive' || parentName.includes('Transition')) &&
+          typeof parentSubTreeType === 'object' &&
+          parentSubTreeType.name === 'RouterView') {
+          const comp = parentName === 'KeepAlive' ? 'keep-alive' : 'transition';
+          warn(`<router-view> can no longer be used directly inside <transition> or <keep-alive>.\n` +
+              `Use slot props instead:\n\n` +
+              `<router-view v-slot="{ Component }">\n` +
+              `  <${comp}>\n` +
+              `    <component :is="Component" />\n` +
+              `  </${comp}>\n` +
+              `</router-view>`);
+      }
+  }
+
+  function getDevtoolsGlobalHook() {
+      return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;
+  }
+  function getTarget() {
+      // @ts-ignore
+      return (typeof navigator !== 'undefined' && typeof window !== 'undefined')
+          ? window
+          : typeof global !== 'undefined'
+              ? global
+              : {};
+  }
+  const isProxyAvailable = typeof Proxy === 'function';
+
+  const HOOK_SETUP = 'devtools-plugin:setup';
+  const HOOK_PLUGIN_SETTINGS_SET = 'plugin:settings:set';
+
+  let supported;
+  let perf;
+  function isPerformanceSupported() {
+      var _a;
+      if (supported !== undefined) {
+          return supported;
+      }
+      if (typeof window !== 'undefined' && window.performance) {
+          supported = true;
+          perf = window.performance;
+      }
+      else if (typeof global !== 'undefined' && ((_a = global.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {
+          supported = true;
+          perf = global.perf_hooks.performance;
+      }
+      else {
+          supported = false;
+      }
+      return supported;
+  }
+  function now() {
+      return isPerformanceSupported() ? perf.now() : Date.now();
+  }
+
+  class ApiProxy {
+      constructor(plugin, hook) {
+          this.target = null;
+          this.targetQueue = [];
+          this.onQueue = [];
+          this.plugin = plugin;
+          this.hook = hook;
+          const defaultSettings = {};
+          if (plugin.settings) {
+              for (const id in plugin.settings) {
+                  const item = plugin.settings[id];
+                  defaultSettings[id] = item.defaultValue;
+              }
+          }
+          const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;
+          let currentSettings = Object.assign({}, defaultSettings);
+          try {
+              const raw = localStorage.getItem(localSettingsSaveId);
+              const data = JSON.parse(raw);
+              Object.assign(currentSettings, data);
+          }
+          catch (e) {
+              // noop
+          }
+          this.fallbacks = {
+              getSettings() {
+                  return currentSettings;
+              },
+              setSettings(value) {
+                  try {
+                      localStorage.setItem(localSettingsSaveId, JSON.stringify(value));
+                  }
+                  catch (e) {
+                      // noop
+                  }
+                  currentSettings = value;
+              },
+              now() {
+                  return now();
+              },
+          };
+          if (hook) {
+              hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {
+                  if (pluginId === this.plugin.id) {
+                      this.fallbacks.setSettings(value);
+                  }
+              });
+          }
+          this.proxiedOn = new Proxy({}, {
+              get: (_target, prop) => {
+                  if (this.target) {
+                      return this.target.on[prop];
+                  }
+                  else {
+                      return (...args) => {
+                          this.onQueue.push({
+                              method: prop,
+                              args,
+                          });
+                      };
+                  }
+              },
+          });
+          this.proxiedTarget = new Proxy({}, {
+              get: (_target, prop) => {
+                  if (this.target) {
+                      return this.target[prop];
+                  }
+                  else if (prop === 'on') {
+                      return this.proxiedOn;
+                  }
+                  else if (Object.keys(this.fallbacks).includes(prop)) {
+                      return (...args) => {
+                          this.targetQueue.push({
+                              method: prop,
+                              args,
+                              resolve: () => { },
+                          });
+                          return this.fallbacks[prop](...args);
+                      };
+                  }
+                  else {
+                      return (...args) => {
+                          return new Promise(resolve => {
+                              this.targetQueue.push({
+                                  method: prop,
+                                  args,
+                                  resolve,
+                              });
+                          });
+                      };
+                  }
+              },
+          });
+      }
+      async setRealTarget(target) {
+          this.target = target;
+          for (const item of this.onQueue) {
+              this.target.on[item.method](...item.args);
+          }
+          for (const item of this.targetQueue) {
+              item.resolve(await this.target[item.method](...item.args));
+          }
+      }
+  }
+
+  function setupDevtoolsPlugin(pluginDescriptor, setupFn) {
+      const descriptor = pluginDescriptor;
+      const target = getTarget();
+      const hook = getDevtoolsGlobalHook();
+      const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;
+      if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {
+          hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);
+      }
+      else {
+          const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;
+          const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];
+          list.push({
+              pluginDescriptor: descriptor,
+              setupFn,
+              proxy,
+          });
+          if (proxy)
+              setupFn(proxy.proxiedTarget);
+      }
+  }
+
+  /**
+   * Copies a route location and removes any problematic properties that cannot be shown in devtools (e.g. Vue instances).
+   *
+   * @param routeLocation - routeLocation to format
+   * @param tooltip - optional tooltip
+   * @returns a copy of the routeLocation
+   */
+  function formatRouteLocation(routeLocation, tooltip) {
+      const copy = assign({}, routeLocation, {
+          // remove variables that can contain vue instances
+          matched: routeLocation.matched.map(matched => omit(matched, ['instances', 'children', 'aliasOf'])),
+      });
+      return {
+          _custom: {
+              type: null,
+              readOnly: true,
+              display: routeLocation.fullPath,
+              tooltip,
+              value: copy,
+          },
+      };
+  }
+  function formatDisplay(display) {
+      return {
+          _custom: {
+              display,
+          },
+      };
+  }
+  // to support multiple router instances
+  let routerId = 0;
+  function addDevtools(app, router, matcher) {
+      // Take over router.beforeEach and afterEach
+      // make sure we are not registering the devtool twice
+      if (router.__hasDevtools)
+          return;
+      router.__hasDevtools = true;
+      // increment to support multiple router instances
+      const id = routerId++;
+      setupDevtoolsPlugin({
+          id: 'org.vuejs.router' + (id ? '.' + id : ''),
+          label: 'Vue Router',
+          packageName: 'vue-router',
+          homepage: 'https://router.vuejs.org',
+          logo: 'https://router.vuejs.org/logo.png',
+          componentStateTypes: ['Routing'],
+          app,
+      }, api => {
+          if (typeof api.now !== 'function') {
+              console.warn('[Vue Router]: You seem to be using an outdated version of Vue Devtools. Are you still using the Beta release instead of the stable one? You can find the links at https://devtools.vuejs.org/guide/installation.html.');
+          }
+          // display state added by the router
+          api.on.inspectComponent((payload, ctx) => {
+              if (payload.instanceData) {
+                  payload.instanceData.state.push({
+                      type: 'Routing',
+                      key: '$route',
+                      editable: false,
+                      value: formatRouteLocation(router.currentRoute.value, 'Current Route'),
+                  });
+              }
+          });
+          // mark router-link as active and display tags on router views
+          api.on.visitComponentTree(({ treeNode: node, componentInstance }) => {
+              if (componentInstance.__vrv_devtools) {
+                  const info = componentInstance.__vrv_devtools;
+                  node.tags.push({
+                      label: (info.name ? `${info.name.toString()}: ` : '') + info.path,
+                      textColor: 0,
+                      tooltip: 'This component is rendered by &lt;router-view&gt;',
+                      backgroundColor: PINK_500,
+                  });
+              }
+              // if multiple useLink are used
+              if (isArray(componentInstance.__vrl_devtools)) {
+                  componentInstance.__devtoolsApi = api;
+                  componentInstance.__vrl_devtools.forEach(devtoolsData => {
+                      let label = devtoolsData.route.path;
+                      let backgroundColor = ORANGE_400;
+                      let tooltip = '';
+                      let textColor = 0;
+                      if (devtoolsData.error) {
+                          label = devtoolsData.error;
+                          backgroundColor = RED_100;
+                          textColor = RED_700;
+                      }
+                      else if (devtoolsData.isExactActive) {
+                          backgroundColor = LIME_500;
+                          tooltip = 'This is exactly active';
+                      }
+                      else if (devtoolsData.isActive) {
+                          backgroundColor = BLUE_600;
+                          tooltip = 'This link is active';
+                      }
+                      node.tags.push({
+                          label,
+                          textColor,
+                          tooltip,
+                          backgroundColor,
+                      });
+                  });
+              }
+          });
+          vue.watch(router.currentRoute, () => {
+              // refresh active state
+              refreshRoutesView();
+              api.notifyComponentUpdate();
+              api.sendInspectorTree(routerInspectorId);
+              api.sendInspectorState(routerInspectorId);
+          });
+          const navigationsLayerId = 'router:navigations:' + id;
+          api.addTimelineLayer({
+              id: navigationsLayerId,
+              label: `Router${id ? ' ' + id : ''} Navigations`,
+              color: 0x40a8c4,
+          });
+          // const errorsLayerId = 'router:errors'
+          // api.addTimelineLayer({
+          //   id: errorsLayerId,
+          //   label: 'Router Errors',
+          //   color: 0xea5455,
+          // })
+          router.onError((error, to) => {
+              api.addTimelineEvent({
+                  layerId: navigationsLayerId,
+                  event: {
+                      title: 'Error during Navigation',
+                      subtitle: to.fullPath,
+                      logType: 'error',
+                      time: api.now(),
+                      data: { error },
+                      groupId: to.meta.__navigationId,
+                  },
+              });
+          });
+          // attached to `meta` and used to group events
+          let navigationId = 0;
+          router.beforeEach((to, from) => {
+              const data = {
+                  guard: formatDisplay('beforeEach'),
+                  from: formatRouteLocation(from, 'Current Location during this navigation'),
+                  to: formatRouteLocation(to, 'Target location'),
+              };
+              // Used to group navigations together, hide from devtools
+              Object.defineProperty(to.meta, '__navigationId', {
+                  value: navigationId++,
+              });
+              api.addTimelineEvent({
+                  layerId: navigationsLayerId,
+                  event: {
+                      time: api.now(),
+                      title: 'Start of navigation',
+                      subtitle: to.fullPath,
+                      data,
+                      groupId: to.meta.__navigationId,
+                  },
+              });
+          });
+          router.afterEach((to, from, failure) => {
+              const data = {
+                  guard: formatDisplay('afterEach'),
+              };
+              if (failure) {
+                  data.failure = {
+                      _custom: {
+                          type: Error,
+                          readOnly: true,
+                          display: failure ? failure.message : '',
+                          tooltip: 'Navigation Failure',
+                          value: failure,
+                      },
+                  };
+                  data.status = formatDisplay('❌');
+              }
+              else {
+                  data.status = formatDisplay('✅');
+              }
+              // we set here to have the right order
+              data.from = formatRouteLocation(from, 'Current Location during this navigation');
+              data.to = formatRouteLocation(to, 'Target location');
+              api.addTimelineEvent({
+                  layerId: navigationsLayerId,
+                  event: {
+                      title: 'End of navigation',
+                      subtitle: to.fullPath,
+                      time: api.now(),
+                      data,
+                      logType: failure ? 'warning' : 'default',
+                      groupId: to.meta.__navigationId,
+                  },
+              });
+          });
+          /**
+           * Inspector of Existing routes
+           */
+          const routerInspectorId = 'router-inspector:' + id;
+          api.addInspector({
+              id: routerInspectorId,
+              label: 'Routes' + (id ? ' ' + id : ''),
+              icon: 'book',
+              treeFilterPlaceholder: 'Search routes',
+          });
+          function refreshRoutesView() {
+              // the routes view isn't active
+              if (!activeRoutesPayload)
+                  return;
+              const payload = activeRoutesPayload;
+              // children routes will appear as nested
+              let routes = matcher.getRoutes().filter(route => !route.parent ||
+                  // these routes have a parent with no component which will not appear in the view
+                  // therefore we still need to include them
+                  !route.parent.record.components);
+              // reset match state to false
+              routes.forEach(resetMatchStateOnRouteRecord);
+              // apply a match state if there is a payload
+              if (payload.filter) {
+                  routes = routes.filter(route => 
+                  // save matches state based on the payload
+                  isRouteMatching(route, payload.filter.toLowerCase()));
+              }
+              // mark active routes
+              routes.forEach(route => markRouteRecordActive(route, router.currentRoute.value));
+              payload.rootNodes = routes.map(formatRouteRecordForInspector);
+          }
+          let activeRoutesPayload;
+          api.on.getInspectorTree(payload => {
+              activeRoutesPayload = payload;
+              if (payload.app === app && payload.inspectorId === routerInspectorId) {
+                  refreshRoutesView();
+              }
+          });
+          /**
+           * Display information about the currently selected route record
+           */
+          api.on.getInspectorState(payload => {
+              if (payload.app === app && payload.inspectorId === routerInspectorId) {
+                  const routes = matcher.getRoutes();
+                  const route = routes.find(route => route.record.__vd_id === payload.nodeId);
+                  if (route) {
+                      payload.state = {
+                          options: formatRouteRecordMatcherForStateInspector(route),
+                      };
+                  }
+              }
+          });
+          api.sendInspectorTree(routerInspectorId);
+          api.sendInspectorState(routerInspectorId);
+      });
+  }
+  function modifierForKey(key) {
+      if (key.optional) {
+          return key.repeatable ? '*' : '?';
+      }
+      else {
+          return key.repeatable ? '+' : '';
+      }
+  }
+  function formatRouteRecordMatcherForStateInspector(route) {
+      const { record } = route;
+      const fields = [
+          { editable: false, key: 'path', value: record.path },
+      ];
+      if (record.name != null) {
+          fields.push({
+              editable: false,
+              key: 'name',
+              value: record.name,
+          });
+      }
+      fields.push({ editable: false, key: 'regexp', value: route.re });
+      if (route.keys.length) {
+          fields.push({
+              editable: false,
+              key: 'keys',
+              value: {
+                  _custom: {
+                      type: null,
+                      readOnly: true,
+                      display: route.keys
+                          .map(key => `${key.name}${modifierForKey(key)}`)
+                          .join(' '),
+                      tooltip: 'Param keys',
+                      value: route.keys,
+                  },
+              },
+          });
+      }
+      if (record.redirect != null) {
+          fields.push({
+              editable: false,
+              key: 'redirect',
+              value: record.redirect,
+          });
+      }
+      if (route.alias.length) {
+          fields.push({
+              editable: false,
+              key: 'aliases',
+              value: route.alias.map(alias => alias.record.path),
+          });
+      }
+      if (Object.keys(route.record.meta).length) {
+          fields.push({
+              editable: false,
+              key: 'meta',
+              value: route.record.meta,
+          });
+      }
+      fields.push({
+          key: 'score',
+          editable: false,
+          value: {
+              _custom: {
+                  type: null,
+                  readOnly: true,
+                  display: route.score.map(score => score.join(', ')).join(' | '),
+                  tooltip: 'Score used to sort routes',
+                  value: route.score,
+              },
+          },
+      });
+      return fields;
+  }
+  /**
+   * Extracted from tailwind palette
+   */
+  const PINK_500 = 0xec4899;
+  const BLUE_600 = 0x2563eb;
+  const LIME_500 = 0x84cc16;
+  const CYAN_400 = 0x22d3ee;
+  const ORANGE_400 = 0xfb923c;
+  // const GRAY_100 = 0xf4f4f5
+  const DARK = 0x666666;
+  const RED_100 = 0xfee2e2;
+  const RED_700 = 0xb91c1c;
+  function formatRouteRecordForInspector(route) {
+      const tags = [];
+      const { record } = route;
+      if (record.name != null) {
+          tags.push({
+              label: String(record.name),
+              textColor: 0,
+              backgroundColor: CYAN_400,
+          });
+      }
+      if (record.aliasOf) {
+          tags.push({
+              label: 'alias',
+              textColor: 0,
+              backgroundColor: ORANGE_400,
+          });
+      }
+      if (route.__vd_match) {
+          tags.push({
+              label: 'matches',
+              textColor: 0,
+              backgroundColor: PINK_500,
+          });
+      }
+      if (route.__vd_exactActive) {
+          tags.push({
+              label: 'exact',
+              textColor: 0,
+              backgroundColor: LIME_500,
+          });
+      }
+      if (route.__vd_active) {
+          tags.push({
+              label: 'active',
+              textColor: 0,
+              backgroundColor: BLUE_600,
+          });
+      }
+      if (record.redirect) {
+          tags.push({
+              label: typeof record.redirect === 'string'
+                  ? `redirect: ${record.redirect}`
+                  : 'redirects',
+              textColor: 0xffffff,
+              backgroundColor: DARK,
+          });
+      }
+      // add an id to be able to select it. Using the `path` is not possible because
+      // empty path children would collide with their parents
+      let id = record.__vd_id;
+      if (id == null) {
+          id = String(routeRecordId++);
+          record.__vd_id = id;
+      }
+      return {
+          id,
+          label: record.path,
+          tags,
+          children: route.children.map(formatRouteRecordForInspector),
+      };
+  }
+  //  incremental id for route records and inspector state
+  let routeRecordId = 0;
+  const EXTRACT_REGEXP_RE = /^\/(.*)\/([a-z]*)$/;
+  function markRouteRecordActive(route, currentRoute) {
+      // no route will be active if matched is empty
+      // reset the matching state
+      const isExactActive = currentRoute.matched.length &&
+          isSameRouteRecord(currentRoute.matched[currentRoute.matched.length - 1], route.record);
+      route.__vd_exactActive = route.__vd_active = isExactActive;
+      if (!isExactActive) {
+          route.__vd_active = currentRoute.matched.some(match => isSameRouteRecord(match, route.record));
+      }
+      route.children.forEach(childRoute => markRouteRecordActive(childRoute, currentRoute));
+  }
+  function resetMatchStateOnRouteRecord(route) {
+      route.__vd_match = false;
+      route.children.forEach(resetMatchStateOnRouteRecord);
+  }
+  function isRouteMatching(route, filter) {
+      const found = String(route.re).match(EXTRACT_REGEXP_RE);
+      route.__vd_match = false;
+      if (!found || found.length < 3) {
+          return false;
+      }
+      // use a regexp without $ at the end to match nested routes better
+      const nonEndingRE = new RegExp(found[1].replace(/\$$/, ''), found[2]);
+      if (nonEndingRE.test(filter)) {
+          // mark children as matches
+          route.children.forEach(child => isRouteMatching(child, filter));
+          // exception case: `/`
+          if (route.record.path !== '/' || filter === '/') {
+              route.__vd_match = route.re.test(filter);
+              return true;
+          }
+          // hide the / route
+          return false;
+      }
+      const path = route.record.path.toLowerCase();
+      const decodedPath = decode(path);
+      // also allow partial matching on the path
+      if (!filter.startsWith('/') &&
+          (decodedPath.includes(filter) || path.includes(filter)))
+          return true;
+      if (decodedPath.startsWith(filter) || path.startsWith(filter))
+          return true;
+      if (route.record.name && String(route.record.name).includes(filter))
+          return true;
+      return route.children.some(child => isRouteMatching(child, filter));
+  }
+  function omit(obj, keys) {
+      const ret = {};
+      for (const key in obj) {
+          if (!keys.includes(key)) {
+              // @ts-expect-error
+              ret[key] = obj[key];
+          }
+      }
+      return ret;
+  }
+
+  /**
+   * Creates a Router instance that can be used by a Vue app.
+   *
+   * @param options - {@link RouterOptions}
+   */
+  function createRouter(options) {
+      const matcher = createRouterMatcher(options.routes, options);
+      const parseQuery$1 = options.parseQuery || parseQuery;
+      const stringifyQuery$1 = options.stringifyQuery || stringifyQuery;
+      const routerHistory = options.history;
+      if (!routerHistory)
+          throw new Error('Provide the "history" option when calling "createRouter()":' +
+              ' https://next.router.vuejs.org/api/#history.');
+      const beforeGuards = useCallbacks();
+      const beforeResolveGuards = useCallbacks();
+      const afterGuards = useCallbacks();
+      const currentRoute = vue.shallowRef(START_LOCATION_NORMALIZED);
+      let pendingLocation = START_LOCATION_NORMALIZED;
+      // leave the scrollRestoration if no scrollBehavior is provided
+      if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
+          history.scrollRestoration = 'manual';
+      }
+      const normalizeParams = applyToParams.bind(null, paramValue => '' + paramValue);
+      const encodeParams = applyToParams.bind(null, encodeParam);
+      const decodeParams = 
+      // @ts-expect-error: intentionally avoid the type check
+      applyToParams.bind(null, decode);
+      function addRoute(parentOrRoute, route) {
+          let parent;
+          let record;
+          if (isRouteName(parentOrRoute)) {
+              parent = matcher.getRecordMatcher(parentOrRoute);
+              if (!parent) {
+                  warn(`Parent route "${String(parentOrRoute)}" not found when adding child route`, route);
+              }
+              record = route;
+          }
+          else {
+              record = parentOrRoute;
+          }
+          return matcher.addRoute(record, parent);
+      }
+      function removeRoute(name) {
+          const recordMatcher = matcher.getRecordMatcher(name);
+          if (recordMatcher) {
+              matcher.removeRoute(recordMatcher);
+          }
+          else {
+              warn(`Cannot remove non-existent route "${String(name)}"`);
+          }
+      }
+      function getRoutes() {
+          return matcher.getRoutes().map(routeMatcher => routeMatcher.record);
+      }
+      function hasRoute(name) {
+          return !!matcher.getRecordMatcher(name);
+      }
+      function resolve(rawLocation, currentLocation) {
+          // const objectLocation = routerLocationAsObject(rawLocation)
+          // we create a copy to modify it later
+          currentLocation = assign({}, currentLocation || currentRoute.value);
+          if (typeof rawLocation === 'string') {
+              const locationNormalized = parseURL(parseQuery$1, rawLocation, currentLocation.path);
+              const matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
+              const href = routerHistory.createHref(locationNormalized.fullPath);
+              {
+                  if (href.startsWith('//'))
+                      warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`);
+                  else if (!matchedRoute.matched.length) {
+                      warn(`No match found for location with path "${rawLocation}"`);
+                  }
+              }
+              // locationNormalized is always a new object
+              return assign(locationNormalized, matchedRoute, {
+                  params: decodeParams(matchedRoute.params),
+                  hash: decode(locationNormalized.hash),
+                  redirectedFrom: undefined,
+                  href,
+              });
+          }
+          if (!isRouteLocation(rawLocation)) {
+              warn(`router.resolve() was passed an invalid location. This will fail in production.\n- Location:`, rawLocation);
+              rawLocation = {};
+          }
+          let matcherLocation;
+          // path could be relative in object as well
+          if (rawLocation.path != null) {
+              if ('params' in rawLocation &&
+                  !('name' in rawLocation) &&
+                  // @ts-expect-error: the type is never
+                  Object.keys(rawLocation.params).length) {
+                  warn(`Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`);
+              }
+              matcherLocation = assign({}, rawLocation, {
+                  path: parseURL(parseQuery$1, rawLocation.path, currentLocation.path).path,
+              });
+          }
+          else {
+              // remove any nullish param
+              const targetParams = assign({}, rawLocation.params);
+              for (const key in targetParams) {
+                  if (targetParams[key] == null) {
+                      delete targetParams[key];
+                  }
+              }
+              // pass encoded values to the matcher, so it can produce encoded path and fullPath
+              matcherLocation = assign({}, rawLocation, {
+                  params: encodeParams(targetParams),
+              });
+              // current location params are decoded, we need to encode them in case the
+              // matcher merges the params
+              currentLocation.params = encodeParams(currentLocation.params);
+          }
+          const matchedRoute = matcher.resolve(matcherLocation, currentLocation);
+          const hash = rawLocation.hash || '';
+          if (hash && !hash.startsWith('#')) {
+              warn(`A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`);
+          }
+          // the matcher might have merged current location params, so
+          // we need to run the decoding again
+          matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params));
+          const fullPath = stringifyURL(stringifyQuery$1, assign({}, rawLocation, {
+              hash: encodeHash(hash),
+              path: matchedRoute.path,
+          }));
+          const href = routerHistory.createHref(fullPath);
+          {
+              if (href.startsWith('//')) {
+                  warn(`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`);
+              }
+              else if (!matchedRoute.matched.length) {
+                  warn(`No match found for location with path "${rawLocation.path != null ? rawLocation.path : rawLocation}"`);
+              }
+          }
+          return assign({
+              fullPath,
+              // keep the hash encoded so fullPath is effectively path + encodedQuery +
+              // hash
+              hash,
+              query: 
+              // if the user is using a custom query lib like qs, we might have
+              // nested objects, so we keep the query as is, meaning it can contain
+              // numbers at `$route.query`, but at the point, the user will have to
+              // use their own type anyway.
+              // https://github.com/vuejs/router/issues/328#issuecomment-649481567
+              stringifyQuery$1 === stringifyQuery
+                  ? normalizeQuery(rawLocation.query)
+                  : (rawLocation.query || {}),
+          }, matchedRoute, {
+              redirectedFrom: undefined,
+              href,
+          });
+      }
+      function locationAsObject(to) {
+          return typeof to === 'string'
+              ? parseURL(parseQuery$1, to, currentRoute.value.path)
+              : assign({}, to);
+      }
+      function checkCanceledNavigation(to, from) {
+          if (pendingLocation !== to) {
+              return createRouterError(8 /* ErrorTypes.NAVIGATION_CANCELLED */, {
+                  from,
+                  to,
+              });
+          }
+      }
+      function push(to) {
+          return pushWithRedirect(to);
+      }
+      function replace(to) {
+          return push(assign(locationAsObject(to), { replace: true }));
+      }
+      function handleRedirectRecord(to) {
+          const lastMatched = to.matched[to.matched.length - 1];
+          if (lastMatched && lastMatched.redirect) {
+              const { redirect } = lastMatched;
+              let newTargetLocation = typeof redirect === 'function' ? redirect(to) : redirect;
+              if (typeof newTargetLocation === 'string') {
+                  newTargetLocation =
+                      newTargetLocation.includes('?') || newTargetLocation.includes('#')
+                          ? (newTargetLocation = locationAsObject(newTargetLocation))
+                          : // force empty params
+                              { path: newTargetLocation };
+                  // @ts-expect-error: force empty params when a string is passed to let
+                  // the router parse them again
+                  newTargetLocation.params = {};
+              }
+              if (newTargetLocation.path == null &&
+                  !('name' in newTargetLocation)) {
+                  warn(`Invalid redirect found:\n${JSON.stringify(newTargetLocation, null, 2)}\n when navigating to "${to.fullPath}". A redirect must contain a name or path. This will break in production.`);
+                  throw new Error('Invalid redirect');
+              }
+              return assign({
+                  query: to.query,
+                  hash: to.hash,
+                  // avoid transferring params if the redirect has a path
+                  params: newTargetLocation.path != null ? {} : to.params,
+              }, newTargetLocation);
+          }
+      }
+      function pushWithRedirect(to, redirectedFrom) {
+          const targetLocation = (pendingLocation = resolve(to));
+          const from = currentRoute.value;
+          const data = to.state;
+          const force = to.force;
+          // to could be a string where `replace` is a function
+          const replace = to.replace === true;
+          const shouldRedirect = handleRedirectRecord(targetLocation);
+          if (shouldRedirect)
+              return pushWithRedirect(assign(locationAsObject(shouldRedirect), {
+                  state: typeof shouldRedirect === 'object'
+                      ? assign({}, data, shouldRedirect.state)
+                      : data,
+                  force,
+                  replace,
+              }), 
+              // keep original redirectedFrom if it exists
+              redirectedFrom || targetLocation);
+          // if it was a redirect we already called `pushWithRedirect` above
+          const toLocation = targetLocation;
+          toLocation.redirectedFrom = redirectedFrom;
+          let failure;
+          if (!force && isSameRouteLocation(stringifyQuery$1, from, targetLocation)) {
+              failure = createRouterError(16 /* ErrorTypes.NAVIGATION_DUPLICATED */, { to: toLocation, from });
+              // trigger scroll to allow scrolling to the same anchor
+              handleScroll(from, from, 
+              // this is a push, the only way for it to be triggered from a
+              // history.listen is with a redirect, which makes it become a push
+              true, 
+              // This cannot be the first navigation because the initial location
+              // cannot be manually navigated to
+              false);
+          }
+          return (failure ? Promise.resolve(failure) : navigate(toLocation, from))
+              .catch((error) => isNavigationFailure(error)
+              ? // navigation redirects still mark the router as ready
+                  isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)
+                      ? error
+                      : markAsReady(error) // also returns the error
+              : // reject any unknown error
+                  triggerError(error, toLocation, from))
+              .then((failure) => {
+              if (failure) {
+                  if (isNavigationFailure(failure, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {
+                      if (// we are redirecting to the same location we were already at
+                          isSameRouteLocation(stringifyQuery$1, resolve(failure.to), toLocation) &&
+                          // and we have done it a couple of times
+                          redirectedFrom &&
+                          // @ts-expect-error: added only in dev
+                          (redirectedFrom._count = redirectedFrom._count
+                              ? // @ts-expect-error
+                                  redirectedFrom._count + 1
+                              : 1) > 30) {
+                          warn(`Detected a possibly infinite redirection in a navigation guard when going from "${from.fullPath}" to "${toLocation.fullPath}". Aborting to avoid a Stack Overflow.\n Are you always returning a new location within a navigation guard? That would lead to this error. Only return when redirecting or aborting, that should fix this. This might break in production if not fixed.`);
+                          return Promise.reject(new Error('Infinite redirect in navigation guard'));
+                      }
+                      return pushWithRedirect(
+                      // keep options
+                      assign({
+                          // preserve an existing replacement but allow the redirect to override it
+                          replace,
+                      }, locationAsObject(failure.to), {
+                          state: typeof failure.to === 'object'
+                              ? assign({}, data, failure.to.state)
+                              : data,
+                          force,
+                      }), 
+                      // preserve the original redirectedFrom if any
+                      redirectedFrom || toLocation);
+                  }
+              }
+              else {
+                  // if we fail we don't finalize the navigation
+                  failure = finalizeNavigation(toLocation, from, true, replace, data);
+              }
+              triggerAfterEach(toLocation, from, failure);
+              return failure;
+          });
+      }
+      /**
+       * Helper to reject and skip all navigation guards if a new navigation happened
+       * @param to
+       * @param from
+       */
+      function checkCanceledNavigationAndReject(to, from) {
+          const error = checkCanceledNavigation(to, from);
+          return error ? Promise.reject(error) : Promise.resolve();
+      }
+      function runWithContext(fn) {
+          const app = installedApps.values().next().value;
+          // support Vue < 3.3
+          return app && typeof app.runWithContext === 'function'
+              ? app.runWithContext(fn)
+              : fn();
+      }
+      // TODO: refactor the whole before guards by internally using router.beforeEach
+      function navigate(to, from) {
+          let guards;
+          const [leavingRecords, updatingRecords, enteringRecords] = extractChangingRecords(to, from);
+          // all components here have been resolved once because we are leaving
+          guards = extractComponentsGuards(leavingRecords.reverse(), 'beforeRouteLeave', to, from);
+          // leavingRecords is already reversed
+          for (const record of leavingRecords) {
+              record.leaveGuards.forEach(guard => {
+                  guards.push(guardToPromiseFn(guard, to, from));
+              });
+          }
+          const canceledNavigationCheck = checkCanceledNavigationAndReject.bind(null, to, from);
+          guards.push(canceledNavigationCheck);
+          // run the queue of per route beforeRouteLeave guards
+          return (runGuardQueue(guards)
+              .then(() => {
+              // check global guards beforeEach
+              guards = [];
+              for (const guard of beforeGuards.list()) {
+                  guards.push(guardToPromiseFn(guard, to, from));
+              }
+              guards.push(canceledNavigationCheck);
+              return runGuardQueue(guards);
+          })
+              .then(() => {
+              // check in components beforeRouteUpdate
+              guards = extractComponentsGuards(updatingRecords, 'beforeRouteUpdate', to, from);
+              for (const record of updatingRecords) {
+                  record.updateGuards.forEach(guard => {
+                      guards.push(guardToPromiseFn(guard, to, from));
+                  });
+              }
+              guards.push(canceledNavigationCheck);
+              // run the queue of per route beforeEnter guards
+              return runGuardQueue(guards);
+          })
+              .then(() => {
+              // check the route beforeEnter
+              guards = [];
+              for (const record of enteringRecords) {
+                  // do not trigger beforeEnter on reused views
+                  if (record.beforeEnter) {
+                      if (isArray(record.beforeEnter)) {
+                          for (const beforeEnter of record.beforeEnter)
+                              guards.push(guardToPromiseFn(beforeEnter, to, from));
+                      }
+                      else {
+                          guards.push(guardToPromiseFn(record.beforeEnter, to, from));
+                      }
+                  }
+              }
+              guards.push(canceledNavigationCheck);
+              // run the queue of per route beforeEnter guards
+              return runGuardQueue(guards);
+          })
+              .then(() => {
+              // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
+              // clear existing enterCallbacks, these are added by extractComponentsGuards
+              to.matched.forEach(record => (record.enterCallbacks = {}));
+              // check in-component beforeRouteEnter
+              guards = extractComponentsGuards(enteringRecords, 'beforeRouteEnter', to, from, runWithContext);
+              guards.push(canceledNavigationCheck);
+              // run the queue of per route beforeEnter guards
+              return runGuardQueue(guards);
+          })
+              .then(() => {
+              // check global guards beforeResolve
+              guards = [];
+              for (const guard of beforeResolveGuards.list()) {
+                  guards.push(guardToPromiseFn(guard, to, from));
+              }
+              guards.push(canceledNavigationCheck);
+              return runGuardQueue(guards);
+          })
+              // catch any navigation canceled
+              .catch(err => isNavigationFailure(err, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)
+              ? err
+              : Promise.reject(err)));
+      }
+      function triggerAfterEach(to, from, failure) {
+          // navigation is confirmed, call afterGuards
+          // TODO: wrap with error handlers
+          afterGuards
+              .list()
+              .forEach(guard => runWithContext(() => guard(to, from, failure)));
+      }
+      /**
+       * - Cleans up any navigation guards
+       * - Changes the url if necessary
+       * - Calls the scrollBehavior
+       */
+      function finalizeNavigation(toLocation, from, isPush, replace, data) {
+          // a more recent navigation took place
+          const error = checkCanceledNavigation(toLocation, from);
+          if (error)
+              return error;
+          // only consider as push if it's not the first navigation
+          const isFirstNavigation = from === START_LOCATION_NORMALIZED;
+          const state = !isBrowser ? {} : history.state;
+          // change URL only if the user did a push/replace and if it's not the initial navigation because
+          // it's just reflecting the url
+          if (isPush) {
+              // on the initial navigation, we want to reuse the scroll position from
+              // history state if it exists
+              if (replace || isFirstNavigation)
+                  routerHistory.replace(toLocation.fullPath, assign({
+                      scroll: isFirstNavigation && state && state.scroll,
+                  }, data));
+              else
+                  routerHistory.push(toLocation.fullPath, data);
+          }
+          // accept current navigation
+          currentRoute.value = toLocation;
+          handleScroll(toLocation, from, isPush, isFirstNavigation);
+          markAsReady();
+      }
+      let removeHistoryListener;
+      // attach listener to history to trigger navigations
+      function setupListeners() {
+          // avoid setting up listeners twice due to an invalid first navigation
+          if (removeHistoryListener)
+              return;
+          removeHistoryListener = routerHistory.listen((to, _from, info) => {
+              if (!router.listening)
+                  return;
+              // cannot be a redirect route because it was in history
+              const toLocation = resolve(to);
+              // due to dynamic routing, and to hash history with manual navigation
+              // (manually changing the url or calling history.hash = '#/somewhere'),
+              // there could be a redirect record in history
+              const shouldRedirect = handleRedirectRecord(toLocation);
+              if (shouldRedirect) {
+                  pushWithRedirect(assign(shouldRedirect, { replace: true }), toLocation).catch(noop);
+                  return;
+              }
+              pendingLocation = toLocation;
+              const from = currentRoute.value;
+              // TODO: should be moved to web history?
+              if (isBrowser) {
+                  saveScrollPosition(getScrollKey(from.fullPath, info.delta), computeScrollPosition());
+              }
+              navigate(toLocation, from)
+                  .catch((error) => {
+                  if (isNavigationFailure(error, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {
+                      return error;
+                  }
+                  if (isNavigationFailure(error, 2 /* ErrorTypes.NAVIGATION_GUARD_REDIRECT */)) {
+                      // Here we could call if (info.delta) routerHistory.go(-info.delta,
+                      // false) but this is bug prone as we have no way to wait the
+                      // navigation to be finished before calling pushWithRedirect. Using
+                      // a setTimeout of 16ms seems to work but there is no guarantee for
+                      // it to work on every browser. So instead we do not restore the
+                      // history entry and trigger a new navigation as requested by the
+                      // navigation guard.
+                      // the error is already handled by router.push we just want to avoid
+                      // logging the error
+                      pushWithRedirect(error.to, toLocation
+                      // avoid an uncaught rejection, let push call triggerError
+                      )
+                          .then(failure => {
+                          // manual change in hash history #916 ending up in the URL not
+                          // changing, but it was changed by the manual url change, so we
+                          // need to manually change it ourselves
+                          if (isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ |
+                              16 /* ErrorTypes.NAVIGATION_DUPLICATED */) &&
+                              !info.delta &&
+                              info.type === NavigationType.pop) {
+                              routerHistory.go(-1, false);
+                          }
+                      })
+                          .catch(noop);
+                      // avoid the then branch
+                      return Promise.reject();
+                  }
+                  // do not restore history on unknown direction
+                  if (info.delta) {
+                      routerHistory.go(-info.delta, false);
+                  }
+                  // unrecognized error, transfer to the global handler
+                  return triggerError(error, toLocation, from);
+              })
+                  .then((failure) => {
+                  failure =
+                      failure ||
+                          finalizeNavigation(
+                          // after navigation, all matched components are resolved
+                          toLocation, from, false);
+                  // revert the navigation
+                  if (failure) {
+                      if (info.delta &&
+                          // a new navigation has been triggered, so we do not want to revert, that will change the current history
+                          // entry while a different route is displayed
+                          !isNavigationFailure(failure, 8 /* ErrorTypes.NAVIGATION_CANCELLED */)) {
+                          routerHistory.go(-info.delta, false);
+                      }
+                      else if (info.type === NavigationType.pop &&
+                          isNavigationFailure(failure, 4 /* ErrorTypes.NAVIGATION_ABORTED */ | 16 /* ErrorTypes.NAVIGATION_DUPLICATED */)) {
+                          // manual change in hash history #916
+                          // it's like a push but lacks the information of the direction
+                          routerHistory.go(-1, false);
+                      }
+                  }
+                  triggerAfterEach(toLocation, from, failure);
+              })
+                  // avoid warnings in the console about uncaught rejections, they are logged by triggerErrors
+                  .catch(noop);
+          });
+      }
+      // Initialization and Errors
+      let readyHandlers = useCallbacks();
+      let errorListeners = useCallbacks();
+      let ready;
+      /**
+       * Trigger errorListeners added via onError and throws the error as well
+       *
+       * @param error - error to throw
+       * @param to - location we were navigating to when the error happened
+       * @param from - location we were navigating from when the error happened
+       * @returns the error as a rejected promise
+       */
+      function triggerError(error, to, from) {
+          markAsReady(error);
+          const list = errorListeners.list();
+          if (list.length) {
+              list.forEach(handler => handler(error, to, from));
+          }
+          else {
+              {
+                  warn('uncaught error during route navigation:');
+              }
+              console.error(error);
+          }
+          // reject the error no matter there were error listeners or not
+          return Promise.reject(error);
+      }
+      function isReady() {
+          if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
+              return Promise.resolve();
+          return new Promise((resolve, reject) => {
+              readyHandlers.add([resolve, reject]);
+          });
+      }
+      function markAsReady(err) {
+          if (!ready) {
+              // still not ready if an error happened
+              ready = !err;
+              setupListeners();
+              readyHandlers
+                  .list()
+                  .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
+              readyHandlers.reset();
+          }
+          return err;
+      }
+      // Scroll behavior
+      function handleScroll(to, from, isPush, isFirstNavigation) {
+          const { scrollBehavior } = options;
+          if (!isBrowser || !scrollBehavior)
+              return Promise.resolve();
+          const scrollPosition = (!isPush && getSavedScrollPosition(getScrollKey(to.fullPath, 0))) ||
+              ((isFirstNavigation || !isPush) &&
+                  history.state &&
+                  history.state.scroll) ||
+              null;
+          return vue.nextTick()
+              .then(() => scrollBehavior(to, from, scrollPosition))
+              .then(position => position && scrollToPosition(position))
+              .catch(err => triggerError(err, to, from));
+      }
+      const go = (delta) => routerHistory.go(delta);
+      let started;
+      const installedApps = new Set();
+      const router = {
+          currentRoute,
+          listening: true,
+          addRoute,
+          removeRoute,
+          hasRoute,
+          getRoutes,
+          resolve,
+          options,
+          push,
+          replace,
+          go,
+          back: () => go(-1),
+          forward: () => go(1),
+          beforeEach: beforeGuards.add,
+          beforeResolve: beforeResolveGuards.add,
+          afterEach: afterGuards.add,
+          onError: errorListeners.add,
+          isReady,
+          install(app) {
+              const router = this;
+              app.component('RouterLink', RouterLink);
+              app.component('RouterView', RouterView);
+              app.config.globalProperties.$router = router;
+              Object.defineProperty(app.config.globalProperties, '$route', {
+                  enumerable: true,
+                  get: () => vue.unref(currentRoute),
+              });
+              // this initial navigation is only necessary on client, on server it doesn't
+              // make sense because it will create an extra unnecessary navigation and could
+              // lead to problems
+              if (isBrowser &&
+                  // used for the initial navigation client side to avoid pushing
+                  // multiple times when the router is used in multiple apps
+                  !started &&
+                  currentRoute.value === START_LOCATION_NORMALIZED) {
+                  // see above
+                  started = true;
+                  push(routerHistory.location).catch(err => {
+                      warn('Unexpected error when starting the router:', err);
+                  });
+              }
+              const reactiveRoute = {};
+              for (const key in START_LOCATION_NORMALIZED) {
+                  Object.defineProperty(reactiveRoute, key, {
+                      get: () => currentRoute.value[key],
+                      enumerable: true,
+                  });
+              }
+              app.provide(routerKey, router);
+              app.provide(routeLocationKey, vue.shallowReactive(reactiveRoute));
+              app.provide(routerViewLocationKey, currentRoute);
+              const unmountApp = app.unmount;
+              installedApps.add(app);
+              app.unmount = function () {
+                  installedApps.delete(app);
+                  // the router is not attached to an app anymore
+                  if (installedApps.size < 1) {
+                      // invalidate the current navigation
+                      pendingLocation = START_LOCATION_NORMALIZED;
+                      removeHistoryListener && removeHistoryListener();
+                      removeHistoryListener = null;
+                      currentRoute.value = START_LOCATION_NORMALIZED;
+                      started = false;
+                      ready = false;
+                  }
+                  unmountApp();
+              };
+              // TODO: this probably needs to be updated so it can be used by vue-termui
+              if (isBrowser) {
+                  addDevtools(app, router, matcher);
+              }
+          },
+      };
+      // TODO: type this as NavigationGuardReturn or similar instead of any
+      function runGuardQueue(guards) {
+          return guards.reduce((promise, guard) => promise.then(() => runWithContext(guard)), Promise.resolve());
+      }
+      return router;
+  }
+  function extractChangingRecords(to, from) {
+      const leavingRecords = [];
+      const updatingRecords = [];
+      const enteringRecords = [];
+      const len = Math.max(from.matched.length, to.matched.length);
+      for (let i = 0; i < len; i++) {
+          const recordFrom = from.matched[i];
+          if (recordFrom) {
+              if (to.matched.find(record => isSameRouteRecord(record, recordFrom)))
+                  updatingRecords.push(recordFrom);
+              else
+                  leavingRecords.push(recordFrom);
+          }
+          const recordTo = to.matched[i];
+          if (recordTo) {
+              // the type doesn't matter because we are comparing per reference
+              if (!from.matched.find(record => isSameRouteRecord(record, recordTo))) {
+                  enteringRecords.push(recordTo);
+              }
+          }
+      }
+      return [leavingRecords, updatingRecords, enteringRecords];
+  }
+
+  /**
+   * Returns the router instance. Equivalent to using `$router` inside
+   * templates.
+   */
+  function useRouter() {
+      return vue.inject(routerKey);
+  }
+  /**
+   * Returns the current route location. Equivalent to using `$route` inside
+   * templates.
+   */
+  function useRoute() {
+      return vue.inject(routeLocationKey);
+  }
+
+  exports.RouterLink = RouterLink;
+  exports.RouterView = RouterView;
+  exports.START_LOCATION = START_LOCATION_NORMALIZED;
+  exports.createMemoryHistory = createMemoryHistory;
+  exports.createRouter = createRouter;
+  exports.createRouterMatcher = createRouterMatcher;
+  exports.createWebHashHistory = createWebHashHistory;
+  exports.createWebHistory = createWebHistory;
+  exports.isNavigationFailure = isNavigationFailure;
+  exports.loadRouteLocation = loadRouteLocation;
+  exports.matchedRouteKey = matchedRouteKey;
+  exports.onBeforeRouteLeave = onBeforeRouteLeave;
+  exports.onBeforeRouteUpdate = onBeforeRouteUpdate;
+  exports.parseQuery = parseQuery;
+  exports.routeLocationKey = routeLocationKey;
+  exports.routerKey = routerKey;
+  exports.routerViewLocationKey = routerViewLocationKey;
+  exports.stringifyQuery = stringifyQuery;
+  exports.useLink = useLink;
+  exports.useRoute = useRoute;
+  exports.useRouter = useRouter;
+  exports.viewDepthKey = viewDepthKey;
+
+  return exports;
+
+})({}, Vue);

File diff suppressed because it is too large
+ 268 - 0
js/vue/vue3.js


File diff suppressed because it is too large
+ 0 - 0
js/wdDrag.js


+ 441 - 0
page/autoLogin.html

@@ -0,0 +1,441 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>自动登录</title>
+    <script src="/js/mp_base/base.js"></script>
+</head>
+<body>
+    <!-- 页面加载状态 -->
+    <div id="page-loading" class="page-loading">
+        <div class="loading-content">
+            <div class="loading-spinner"></div>
+            <div class="loading-text">正在自动登录...</div>
+        </div>
+    </div>
+
+    <!-- 主要内容 -->
+    <div id="app" v-cloak>
+        <!-- 自动登录状态 -->
+        <div class="auto-login-container">
+            <div class="status-icon">
+                <div v-if="status === 'loading'" class="loading-spinner"></div>
+                <div v-else-if="status === 'success'" class="success-icon">✓</div>
+                <div v-else-if="status === 'error'" class="error-icon">✗</div>
+            </div>
+            
+            <div class="status-text">{{ statusText }}</div>
+            
+            <div v-if="status === 'error'" class="retry-section">
+                <button @click="handleRetry" class="retry-btn">重试</button>
+                <button @click="handleManualLogin" class="manual-btn">手动登录</button>
+            </div>
+        </div>
+
+        <!-- Loading 遮罩 -->
+        <div v-if="showLoading" class="loading-mask">
+            <div class="loading-content">
+                <div class="loading-spinner"></div>
+                <div class="loading-text">{{ loadingText }}</div>
+            </div>
+        </div>
+
+        <!-- Toast 提示 -->
+        <div v-if="toast.show" class="toast" :class="toast.type">
+            {{ toast.message }}
+        </div>
+    </div>
+
+    <script>
+        // 获取yhsbToken的函数
+        function getYhsbToken() {
+            // 从localStorage获取用户信息
+            const userInfo = localStorage.getItem('userInfo');
+            if (userInfo) {
+                try {
+                    const userData = JSON.parse(userInfo);
+                    return userData.yhsbToken || userData.onlineToken || '';
+                } catch (e) {
+                    console.error('解析用户信息失败:', e);
+                }
+            }
+
+            // 从其他可能的存储位置获取
+            return localStorage.getItem('yhsbToken') || '';
+        }
+
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        status: 'loading', // loading, success, error
+                        statusText: '正在自动登录...',
+                        showLoading: false,
+                        loadingText: '',
+                        toast: {
+                            show: false,
+                            message: '',
+                            type: 'info'
+                        }
+                    }
+                },
+
+                mounted() {
+                    // 隐藏页面加载状态
+                    const pageLoading = document.getElementById('page-loading')
+                    if (pageLoading) {
+                        pageLoading.style.display = 'none'
+                    }
+
+                    // 初始化页面,使用自定义结果处理
+                    if (typeof initPage === 'function') {
+                        initPage('autoLogin', this.handleResult)
+                    }
+
+                    // 获取URL参数
+                    const urlParams = this.getUrlParams()
+                    console.log('🔗 H5自动登录页面参数:', urlParams)
+
+                    // 开始自动登录
+                    this.startAutoLogin()
+                },
+
+                methods: {
+                    // 获取URL参数
+                    getUrlParams() {
+                        const params = {}
+                        const urlSearchParams = new URLSearchParams(window.location.search)
+                        for (const [key, value] of urlSearchParams) {
+                            params[key] = decodeURIComponent(value)
+                        }
+                        return params
+                    },
+
+                    // 处理操作结果(来自小程序的回调)
+                    handleResult(data) {
+                        console.log('🎯 收到小程序回调:', data)
+                    },
+
+                    // 显示Loading
+                    showLoadingMask(text = '加载中...') {
+                        this.showLoading = true
+                        this.loadingText = text
+                    },
+
+                    // 隐藏Loading
+                    hideLoadingMask() {
+                        this.showLoading = false
+                        this.loadingText = ''
+                    },
+
+                    
+
+                    // 开始自动登录
+                    async startAutoLogin() {
+                        try {
+                            this.status = 'loading'
+                            this.statusText = '正在自动登录...'
+
+                            // 从URL参数或localStorage获取yhsbToken进行自动登录
+                            const urlParams = this.getUrlParams()
+                            const yhsbToken = urlParams.yhsbToken || getYhsbToken()
+
+                            console.log('🔄 开始自动登录,yhsbToken:', yhsbToken)
+
+                            if (!yhsbToken) {
+                                throw new Error('未找到yhsbToken,无法自动登录')
+                            }
+
+                            // 使用自动登录接口(参考小程序的实现)
+                            const response = await request.post(
+                                `/service?ssServ=ssLogin&wdConfirmationCaptchaService=0&mdToken=${yhsbToken}`,
+                                { mdToken: yhsbToken },
+                                { loading: false }
+                            )
+                            if (response && response.data) {
+                                console.log('✅ 自动登录成功:', JSON.stringify( response.data))
+
+                                // 构建用户数据(参考登录页面的格式)
+                                const userData = {
+                                    devId: response.data.devId,
+                                    sbmc: response.data.sbmc,
+                                    sessId: response.data.sessId ,
+                                    xm: response.data.xm,
+                                    yhsbToken: response.data.yhsbToken,
+                                    onlineToken: response.data.onlineToken,
+                                    syList:response.data.sylist,
+                                    yhid:response.data.yhid,
+                                    yhm:response.data.yhm
+                                }
+
+                                // 保存用户信息到H5本地存储(和登录页面一样)
+                                h5UserApi.saveUserInfo(userData)
+
+                                // 准备返回数据给小程序(和登录页面一样,只是多了isAutoLogin标记)
+                                const loginResult = {
+                                    success: true,
+                                    userInfo: userData,
+                                    isAutoLogin: true // 标记为自动登录,用于区分返回逻辑
+                                }
+
+                                this.status = 'success'
+                                this.statusText = '自动登录成功!'
+
+                                console.log('✅ 自动登录成功,准备返回数据:', loginResult)
+
+                                // 延迟返回结果
+                                setTimeout(() => {
+                                    this.returnLoginResult(loginResult)
+                                }, 1500)
+
+                            } else {
+                                throw new Error('自动登录响应数据无效')
+                            }
+
+                        } catch (error) {
+                            console.error('❌ 自动登录失败:', error)
+                            this.status = 'error'
+                            this.statusText = '自动登录失败: ' + error.message
+                        }
+                    },
+
+                    // 返回登录结果给小程序
+                    returnLoginResult(result) {
+                        console.log('🔄 返回自动登录结果给小程序:', result)
+                        callNative('loginSuccess', '自动登录成功', result)
+                    },
+
+                    // 重试自动登录
+                    handleRetry() {
+                        console.log('🔄 重试自动登录')
+                        this.startAutoLogin()
+                    },
+
+                    // 跳转手动登录
+                    handleManualLogin() {
+                        console.log('👤 跳转手动登录')
+                        // 跳转到登录页面
+                        window.location.href = '/pages/login.html' + window.location.search
+                    }
+                }
+            })
+
+            console.log('✅ 自动登录页面初始化完成')
+        })
+    </script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        /* 页面加载状态 */
+        .page-loading {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: #f5f5f5;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            z-index: 9999;
+        }
+
+        .page-loading .loading-content {
+            text-align: center;
+        }
+
+        .page-loading .loading-spinner {
+            width: 40px;
+            height: 40px;
+            border: 4px solid #f3f3f3;
+            border-top: 4px solid #40ac6d;
+            border-radius: 50%;
+            animation: page-spin 1s linear infinite;
+            margin: 0 auto 15px;
+        }
+
+        @keyframes page-spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+
+        .page-loading .loading-text {
+            color: #666;
+            font-size: 14px;
+        }
+
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: #f5f5f5;
+            min-height: 100vh;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+        }
+
+        .auto-login-container {
+            background: white;
+            border-radius: 12px;
+            padding: 40px 30px;
+            text-align: center;
+            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
+            max-width: 300px;
+            width: 90%;
+        }
+
+        .status-icon {
+            margin-bottom: 20px;
+        }
+
+        .loading-spinner {
+            width: 50px;
+            height: 50px;
+            border: 4px solid #f3f3f3;
+            border-top: 4px solid #40ac6d;
+            border-radius: 50%;
+            animation: spin 1s linear infinite;
+            margin: 0 auto;
+        }
+
+        .success-icon {
+            width: 50px;
+            height: 50px;
+            background: #40ac6d;
+            border-radius: 50%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: white;
+            font-size: 24px;
+            font-weight: bold;
+            margin: 0 auto;
+        }
+
+        .error-icon {
+            width: 50px;
+            height: 50px;
+            background: #dc3545;
+            border-radius: 50%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            color: white;
+            font-size: 24px;
+            font-weight: bold;
+            margin: 0 auto;
+        }
+
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+
+        .status-text {
+            font-size: 16px;
+            color: #333;
+            margin-bottom: 20px;
+            line-height: 1.5;    
+            word-break: keep-all;
+            
+        }
+
+        .retry-section {
+            display: flex;
+            gap: 10px;
+            justify-content: center;
+        }
+
+        .retry-btn, .manual-btn {
+            padding: 10px 20px;
+            border: none;
+            border-radius: 6px;
+            font-size: 14px;
+            cursor: pointer;
+            transition: all 0.3s ease;
+        }
+
+        .retry-btn {
+            background: #40ac6d;
+            color: white;
+        }
+
+        .retry-btn:hover {
+            background: #369a5a;
+        }
+
+        .manual-btn {
+            background: #6c757d;
+            color: white;
+        }
+
+        .manual-btn:hover {
+            background: #5a6268;
+        }
+
+        /* Loading 遮罩 */
+        .loading-mask {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.5);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            z-index: 10000;
+        }
+
+        .loading-mask .loading-content {
+            background: white;
+            padding: 20px;
+            border-radius: 8px;
+            text-align: center;
+            min-width: 120px;
+        }
+
+        .loading-mask .loading-text {
+            color: #333;
+            font-size: 14px;
+        }
+
+        /* Toast 提示 */
+        .toast {
+            position: fixed;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            background: rgba(0, 0, 0, 0.8);
+            color: white;
+            padding: 12px 20px;
+            border-radius: 6px;
+            font-size: 14px;
+            z-index: 10001;
+            max-width: 80%;
+            text-align: center;
+        }
+
+        .toast.success {
+            background: rgba(40, 167, 69, 0.9);
+        }
+
+        .toast.error {
+            background: rgba(220, 53, 69, 0.9);
+        }
+    </style>
+</body>
+</html>

+ 848 - 0
page/bjdm_bzrDm.html

@@ -0,0 +1,848 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>班主任点名</title>
+    <!-- 引入基础依赖 -->
+
+    <script src="/js/mp_base/base.js"></script>
+
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+            display: flex;
+            flex-direction: column;
+        }
+
+        /* 固定头部区域 */
+        .fixed-header {
+            background: #f5f5f5;
+            padding: 10px 0;
+            position: sticky;
+            top: 0;
+            z-index: 100;
+        }
+
+      
+
+        .filter-area.simple-mode {
+            /* padding: 10px 16px; */
+            /* background: #f8f9fa; */
+            /* border-radius: 6px; */
+            transition: all 0.3s ease;
+        }
+
+        .simple-display {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            height: 40px;
+        }
+
+        .simple-text {
+            font-size: 16px;
+            color: #333;
+        }
+
+
+
+        /* 统计区域 */
+        .stats-area {
+            display: flex;
+            justify-content: flex-start;
+            padding: 10px 16px;
+            background: #ffffff;
+            border-radius: 6px;
+            gap: 8px;
+        }
+
+        .stat-item {
+            text-align: center;
+            font-size: 14px;
+        }
+
+        .stat-absent { color: #ff0000; }
+        .stat-sick { color: #ee9700; }
+        .stat-parent { color: #00a0e9; }
+
+        /* 学生列表区域 */
+        .student-list-area {
+            flex: 1;
+            /* padding: 8px; */
+            padding-bottom: 80px; /* 给底部按钮留出空间 */
+            overflow-y: auto;
+            width: calc(100% - 16px);
+            margin: 8px auto;
+        }
+
+        /* 学生表格 */
+        .student-table {
+            background: white;
+            overflow: hidden;
+            border: 1px solid #d2d2d2;
+        }
+
+        .table-header {
+            display: flex;
+            background: #f8f9fa;
+            font-weight: 400;
+            border-bottom: 1px solid #d2d2d2;
+        }
+
+        .table-row {
+            display: flex;
+            border-bottom: 1px solid #d2d2d2;
+            cursor: pointer;
+            transition: background-color 0.2s;
+        }
+        .table-row:last-child {
+            border-bottom: none;
+        }
+
+      
+
+        .table-row.absent {
+            background: #ff0000;
+            color: white !important;
+        }
+
+        .table-row.absent .col-number,
+        .table-row.absent .col-name,
+        .table-row.absent .col-status {
+            color: white !important;
+        }
+
+        .table-row.sick {
+            background: #ffa502;
+            color: white !important;
+        }
+
+        .table-row.sick .col-number,
+        .table-row.sick .col-name,
+        .table-row.sick .col-status {
+            color: white !important;
+        }
+
+        .table-row.leave {
+            background: #ee9700;
+            color: white !important;
+        }
+
+        .table-row.leave .col-number,
+        .table-row.leave .col-name,
+        .table-row.leave .col-status {
+            color: white !important;
+        }
+
+        .table-row.parent {
+            background: #00a0e9;
+            color: white !important;
+        }
+
+        .table-row.parent .col-number,
+        .table-row.parent .col-name,
+        .table-row.parent .col-status {
+            color: white !important;
+        }
+
+        .col-number {
+            width: 60px;
+            padding: 12px 8px;
+            text-align: center;
+            font-size: 16px;
+            color: #333;
+        }
+
+        .col-name {
+            flex: 1;
+            padding: 12px;
+            font-size: 16px;
+            color: #333;
+        }
+
+        .col-status {
+            width: 80px;
+            padding: 12px 8px;
+            text-align: center;
+            font-size: 16px;
+            font-weight: 400;
+            color: #333;
+        }
+
+
+
+        /* 加载状态 */
+        .loading {
+            text-align: center;
+            padding: 40px;
+            color: #666;
+        }
+
+        /* 周几显示 */
+        .weekday-text {
+            margin-left: 10px;
+            font-size: 12px;
+            color: #666;
+            position:absolute;
+            right: 16px;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <!-- 固定头部区域 -->
+        <div class="fixed-header">
+            <!-- 筛选条件区域 -->
+            <div class="filter-area" :class="{ 'simple-mode': isSimpleMode }">
+                <!-- 简化模式:显示回显文本 -->
+                <div v-if="isSimpleMode" class="simple-display">
+                    <span class="simple-text">{{ displayText }}</span>
+                </div>
+
+                <!-- 完整模式:显示表单 -->
+                <div v-if="!isSimpleMode" class="full-form">
+                    <table class="form-table">
+                        <tr>
+                            <th>班级</th>
+                            <td>
+                                <ss-select
+                                      v-model="formData.bjid"
+                                      :options="bjOptions"
+                                      :mapping="{ text: 'label', value: 'value' }"
+                                      placeholder="请选择班级"
+                                      @change="loadStudentList"
+                                >
+                                </ss-select>
+                            </td>
+                        </tr>
+                        <tr>
+                            <th>日期</th>
+                            <td>
+                                <ss-datetime-picker
+                                      v-model="formData.rq"
+                                      mode="date"
+                                      :max-date="new Date().toISOString().slice(0,10)"
+                                      placeholder="请选择日期"
+                                      @change="onDateChange"
+                                      :disabled="true"
+                                >
+                                </ss-datetime-picker>
+                                <span v-if="weekdayText" class="weekday-text">{{ weekdayText }}</span>
+                            </td>
+                        </tr>
+                        <tr>
+                            <th>节次</th>
+                            <td>
+                                <ss-onoff-button v-model="formData.jc" name="jc" label="上午" value="1" :disabled="true"></ss-onoff-button>
+                                <ss-onoff-button v-model="formData.jc" name="jc" label="下午" value="2" :disabled="true"></ss-onoff-button>
+                                <ss-onoff-button v-model="formData.jc" name="jc" label="晚上" value="3" :disabled="true"></ss-onoff-button>
+
+                            </td>
+                        </tr>
+
+                    </table>
+                </div>
+            </div>
+        </div>
+        <!-- 考勤统计 -->
+        <div class="stats-area">
+            <div class="stat-item">
+                <span class="stat-absent">缺勤:</span>{{ stats.absent }}人
+            </div>
+            <div class="stat-item">
+                <span class="stat-sick">请假:</span>{{ stats.sick + stats.leave }}人
+            </div>
+            <div class="stat-item">
+                <span class="stat-parent">家长请假:</span>{{ stats.parentLeave }}人
+            </div>
+        </div>
+        <!-- 学生列表区域 -->
+        <div class="student-list-area">
+            <div v-if="loading" class="loading">
+                加载中...
+            </div>
+
+            <div v-else-if="studentList.length === 0" class="loading">
+                暂无学生数据
+            </div>
+
+            <div v-else class="student-table">
+                <!-- 表头 -->
+                <div class="table-header">
+                    <span class="col-number">序号</span>
+                    <span class="col-name">姓名</span>
+                    <span class="col-status">状态</span>
+                </div>
+
+                <!-- 学生行 -->
+                <div
+                    v-for="(student, index) in studentList"
+                    :key="student.id"
+                    class="table-row"
+                    :class="getRowClass(student.kqlbm, student.rcid)"
+                    @click="handleStudentClick(student)"
+                >
+                    <span class="col-number">{{ String(index + 1).padStart(2, '0') }}</span>
+                    <span class="col-name">{{ student.xm }}</span>
+                    <span class="col-status">{{ getStatusText(student.kqlbm, student.rcid) }}</span>
+                </div>
+            </div>
+        </div>
+
+        <!-- 使用SsBottom组件 -->
+        <ss-bottom
+            :show-shyj="false"
+            :buttons="bottomButtons"
+            @button-click="handleBottomAction"
+        ></ss-bottom>
+
+            <!-- 确认弹窗组件 -->
+            <ss-confirm v-model="showConfirm" title="确认提交" @confirm="submitAttendance">
+                <div style="text-align: center; padding: 20px;">
+                    
+                    <div style="display: flex; flex-direction: column; gap: 10px;">
+                        <div>出勤:<span >{{ stats.present }}人</span></div>
+                        <div>缺勤:<span >{{ stats.absent }}人</span></div>
+                        <div>病假:<span >{{ stats.sick }}人</span></div>
+                        <div>事假:<span >{{ stats.leave }}人</span></div>
+                        <div>家长请假:<span >{{ stats.parentLeave }}人</span></div>
+                    </div>
+                </div>
+            </ss-confirm>
+
+
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        // 页面参数
+                        pageParams: {},
+
+                        // 加载状态
+                        loading: true,
+
+                        // 表单数据
+                        formData: {
+                            bjid: '', // 班级ID
+                            rq: '',   // 日期
+                            jc: '1'   // 节次:1-上午,2-下午,3-晚上
+                        },
+
+                        // 显示模式
+                        isSimpleMode: false,
+                        displayText: '',
+                        weekdayText: '',
+
+                        // 滚动相关
+                        pageScrollTop: 0,
+                        initialFilterHeight: 0,
+
+                        // 统计数据
+                        stats: {
+                            absent: 0,  // 缺勤
+                            leave: 0,   // 请假
+                            parent: 0   // 家长请假
+                        },
+
+                        // 学生列表
+                        studentList: [],
+
+                        // 班级选项
+                        bjOptions: [],
+
+                        
+
+                        // 确认弹窗相关
+                        showConfirm: false,
+                        confirmStatsHtml: '',
+
+                        // 底部按钮
+                        bottomButtons: [
+                            { text: '取消', action: 'cancel' },
+                            { text: '保存点名', action: 'save' }
+                        ]
+                    }
+                },
+
+                async mounted() {
+                    try {
+                        // 获取页面参数
+                        this.pageParams = NavigationManager.getUrlParam();
+                        console.log('📋 页面参数:', this.pageParams);
+                        // 设置页面标题
+                        if (this.pageParams.title) {
+                            document.title = this.pageParams.title;
+                        }
+
+                        // 初始化页面
+                        await this.initPage();
+
+                    } catch (error) {
+                        console.error('❌ 页面初始化失败:', error);
+                    }
+                },
+
+                methods: {
+                    // 初始化页面
+                    async initPage() {
+                        try {
+
+                            // 设置默认日期为今天
+                            this.formData.rq = new Date().toISOString().slice(0, 10);
+                            this.updateWeekdayText();
+
+                            // 根据当前时间设置默认节次
+                            this.setDefaultPeriod();
+
+                            // 初始化回显文本
+                            this.updateDisplayText();
+
+                            // 初始化筛选区域高度
+                            this.$nextTick(() => {
+                                const filterArea = document.querySelector('.filter-area');
+                                if (filterArea) {
+                                    this.initialFilterHeight = filterArea.offsetHeight;
+                                    console.log('初始筛选区域高度:', this.initialFilterHeight);
+                                }
+                            });
+
+                            // 添加滚动监听
+                            window.addEventListener('scroll', this.handlePageScroll);
+
+                            // 加载班级选项
+                            await this.loadBjOptions();
+
+                            // 如果有默认班级,加载学生数据
+                            if (this.formData.bjid) {
+                                await this.loadStudentList();
+                            }
+
+                        } catch (error) {
+                            console.error('❌ 页面初始化失败:', error);
+                            throw error;
+                        } finally {
+                            this.loading = false;
+                        }
+                    },
+
+                    // 获取开始时间
+                    getKssj() {
+                        const jc = this.formData.jc;
+                        const rq = this.formData.rq;
+
+                        let time = '';
+                        if (jc === '1') {
+                            time = '08:00:00'; // 上午
+                        } else if (jc === '2') {
+                            time = '14:00:00'; // 下午
+                        } else if (jc === '3') {
+                            time = '19:00:00'; // 晚上
+                        }
+
+                        return `${rq} ${time}`;
+                    },
+
+                    // 获取结束时间
+                    getJssj() {
+                        const jc = this.formData.jc;
+                        const rq = this.formData.rq;
+
+                        let time = '';
+                        if (jc === '1') {
+                            time = '12:00:00'; // 上午
+                        } else if (jc === '2') {
+                            time = '18:00:00'; // 下午
+                        } else if (jc === '3') {
+                            time = '22:00:00'; // 晚上
+                        }
+
+                        return `${rq} ${time}`;
+                    },
+
+                    // 页面销毁时移除滚动监听
+                    beforeUnmount() {
+                        window.removeEventListener('scroll', this.handlePageScroll);
+                    },
+
+                    // 组件初始化逻辑已改为模板中直接使用 Vue 组件,无需此方法
+                    initFormComponents() {},
+
+                    // 日期变化处理
+                    onDateChange(value) {
+                        this.formData.rq = value;
+                        this.updateWeekdayText();
+                        this.updateDisplayText();
+                        this.loadStudentList();
+                    },
+
+                    // 更新星期几显示
+                    updateWeekdayText() {
+                        if (!this.formData.rq) {
+                            this.weekdayText = '';
+                            return;
+                        }
+
+                        try {
+                            const date = new Date(this.formData.rq);
+                            const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
+                            this.weekdayText = weekdays[date.getDay()];
+                            // 更新回显文本
+                            this.updateDisplayText();
+                        } catch (error) {
+                            this.weekdayText = '';
+                        }
+                    },
+
+                    // 根据当前时间设置默认节次
+                    setDefaultPeriod() {
+                        const currentTime = new Date();
+                        const currentHour = currentTime.getHours();
+
+                        let currentPeriod = '';
+                        let jcValue = '1';
+
+                        if (currentHour >= 0 && currentHour < 12) {
+                            // 0-12点:上午
+                            currentPeriod = '上午';
+                            jcValue = '1';
+                        } else if (currentHour >= 12 && currentHour < 18) {
+                            // 12-18点:下午
+                            currentPeriod = '下午';
+                            jcValue = '2';
+                        } else {
+                            // 18-24点:晚上
+                            currentPeriod = '晚上';
+                            jcValue = '3';
+                        }
+
+                        this.formData.jc = jcValue;
+                        console.log(`🕐 当前时间 ${currentHour}:xx,自动设置为${currentPeriod}(${jcValue})`);
+                    },
+
+                    // 计算回显文本
+                    updateDisplayText() {
+                        const jcText = this.formData.jc === '1' ? '上午' : this.formData.jc === '2' ? '下午' : '晚上';
+                        this.displayText = `${this.formData.rq}  ${this.weekdayText}  ${jcText}`;
+                    },
+
+                    // 处理页面滚动事件
+                    handlePageScroll() {
+                        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+                        const previousScrollTop = this.pageScrollTop;
+                        this.pageScrollTop = scrollTop;
+
+                        console.log('页面滚动位置:', scrollTop, '初始高度:', this.initialFilterHeight);
+
+                        // 检测滚动方向和位置
+                        const isScrollingDown = scrollTop > previousScrollTop;
+                        const isScrollingUp = scrollTop < previousScrollTop;
+
+                        // 三行变一行:向下滚动超过筛选区域高度的30px时触发
+                        if (isScrollingDown &&
+                            scrollTop > (this.initialFilterHeight + 30) &&
+                            !this.isSimpleMode) {
+                            console.log('向下滚动超过阈值,切换到简化模式');
+                            this.isSimpleMode = true;
+                        }
+
+                        // 一行变三行:向上滚动回到接近顶部时触发
+                        if (isScrollingUp &&
+                            scrollTop <= 10 &&
+                            this.isSimpleMode) {
+                            console.log('向上滚动回到顶部,切换到完整模式');
+                            this.isSimpleMode = false;
+                        }
+                    },
+
+                    // 加载班级选项
+                    async loadBjOptions() {
+                        try {
+
+                            // 使用已封装的 getDictOptions 获取班级数据
+                            const bjOptions = await window.getDictOptions('bj');
+
+                            if (bjOptions && bjOptions.length > 0) {
+                                this.bjOptions = bjOptions.map(item => ({
+                                    value: item.v,
+                                    label: item.n
+                                }));
+
+                                console.log('📚 班级选项加载成功:', this.bjOptions);
+
+                                // 设置默认选中第一个班级
+                                if (this.bjOptions.length > 0) {
+                                    this.formData.bjid = this.bjOptions[0].value;
+
+                                }
+                            } else {
+                                console.warn('⚠️ 没有获取到班级数据');
+                            }
+                        } catch (error) {
+                            console.error('❌ 加载班级选项失败:', error);
+                        }
+                    },
+
+                    // 加载学生列表
+                    async loadStudentList() {
+                        if (!this.formData.bjid) {
+                            console.warn('⚠️ 班级ID为空,无法加载学生列表');
+                            return;
+                        }
+
+                        try {
+                            this.loading = true;
+                            console.log('👥 加载学生列表...', { bjid: this.formData.bjid });
+
+                            // 调用真实的 bjdm_initBzrDm 接口
+                            const response = await window.request.post(
+                                `/service?ssServ=bjdm_initBzrdm`,
+                                { bjid: this.formData.bjid },
+                                {
+                                    loading: true,
+                                    formData: true
+                                }
+                            );
+
+                            console.log('👥 学生数据返回:', response);
+
+                            if (response.data && response.data.bjList && response.data.bjList.length > 0) {
+                                // 取第一个班级的学生列表
+                                this.studentList = response.data.bjList[0];
+                                this.updateStats();
+                            } else {
+                                console.log('⚠️ 没有获取到学生数据:', response.data?.msg);
+                                this.studentList = [];
+                            }
+
+                        } catch (error) {
+                            console.log('❌ 加载学生列表失败:', error);
+                            
+                            // this.updateStats();
+
+                        } finally {
+                            this.loading = false;
+                        }
+                    },
+
+                    // 更新统计数据
+                    updateStats() {
+                        const stats = {
+                            present: 0,      // 出勤
+                            absent: 0,       // 缺勤
+                            sick: 0,         // 病假
+                            leave: 0,        // 事假
+                            parentLeave: 0   // 家长请假
+                        };
+
+                        this.studentList.forEach(student => {
+                            if (student.rcid > 0) {
+                                stats.parentLeave++ // 家长请假(优先判断)
+                            } else if (student.kqlbm === 81) {
+                                stats.present++  // 出勤
+                            } else if (student.kqlbm === 1) {
+                                stats.absent++   // 缺勤
+                            } else if (student.kqlbm === 31) {
+                                stats.leave++    // 事假
+                            } else if (student.kqlbm === 41) {
+                                stats.sick++     // 病假
+                            } else {
+                                stats.present++  // 其他情况默认为出勤(学生一开始都是出勤状态)
+                            }
+                        });
+
+                        this.stats = stats;
+                    },
+
+                    // 获取行样式类
+                    getRowClass(kqlbm, rcid) {
+                        if (rcid > 0) return 'parent';      // 家长请假(优先判断)
+                        if (kqlbm === 81) return '';        // 出勤(默认样式)
+                        if (kqlbm === 1) return 'absent';   // 缺勤
+                        if (kqlbm === 41) return 'sick';    // 病假
+                        if (kqlbm === 31) return 'leave';   // 事假
+                        return '';
+                    },
+
+                    // 获取状态文本
+                    getStatusText(kqlbm, rcid) {
+                        if (rcid > 0) return '家长请假';     // 家长请假(优先判断)
+                        if (kqlbm === 81) return '出勤';
+                        if (kqlbm === 1) return '缺勤';
+                        if (kqlbm === 41) return '病假';
+                        if (kqlbm === 31) return '事假';
+                        return '出勤'; // 默认为出勤,不存在未知状态
+                    },
+
+                    // 学生点击事件
+                    handleStudentClick(student) {
+                        console.log('👤 学生点击:', student);
+
+                        // 家长请假不可点击
+                        if (student.rcid > 0) {
+                            console.log('家长请假状态,不可点击');
+                            return;
+                        }
+
+                        // 切换学生状态
+                        this.toggleStudentStatus(student);
+                    },
+
+                    // 切换学生状态
+                    toggleStudentStatus(student) {
+                        /**
+                         * 状态切换逻辑:出勤(81) → 缺勤(1) → 病假(41) → 事假(31) → 出勤(81)
+                         * 只在这四个状态间轮流切换,不存在其他状态
+                         */
+                        let newStatus;
+                        switch (student.kqlbm) {
+                            case 81: // 出勤 → 缺勤
+                                newStatus = 1;
+                                break;
+                            case 1: // 缺勤 → 病假
+                                newStatus = 41;
+                                break;
+                            case 41: // 病假 → 事假
+                                newStatus = 31;
+                                break;
+                            case 31: // 事假 → 出勤
+                                newStatus = 81;
+                                break;
+                            default: // 任何其他状态都重置为出勤
+                                newStatus = 81;
+                        }
+
+                        student.kqlbm = newStatus;
+                        student.rcid = null; // 清除家长请假标记
+
+                        console.log(`学生 ${student.xm} 状态切换为:`, newStatus);
+                        this.updateStats();
+                    },
+
+                    // 保存点名数据
+                    async handleSave() {
+                        try {
+
+                            // 显示确认弹窗
+                            this.showConfirmDialog();
+
+                        } catch (error) {
+                            console.error('❌ 保存失败:', error);
+                        }
+                    },
+
+                    // 显示确认弹窗
+                    showConfirmDialog() {
+                        this.showConfirm = true;
+                    },
+
+                    // 提交考勤数据
+                    async submitAttendance() {
+                        try {
+
+                            // 构建提交参数,参考小程序版本
+                            const params = {
+                                bjid: this.formData.bjid,
+                                kkrs: this.stats.absent, // 缺勤人数
+                                qjrs: this.stats.sick + this.stats.leave, // 请假人数(病假+事假)
+                                jkssj: this.getKssj(), // 开始时间
+                                jjssj: this.getJssj(), // 结束时间
+                                ryList: JSON.stringify(this.studentList
+                                    .filter(student => student.kqlbm !== 81) // 只提交非出勤的学生
+                                    .map(student => ({
+                                        ryid: student.ryid,
+                                        kqlbm: student.kqlbm
+                                    })))
+                            };
+
+
+                            // 调用真实的 bjdm_saveBzrDm 接口,使用表单格式提交
+                            const response = await window.request.post(`/service?ssServ=bjdm_saveBzrdm`, params, {
+                                loading: true,
+                                formData: true // 使用表单格式提交
+                            });
+
+                            if (response.data && response.data.ssCode === 0) {
+                                NavigationManager.goBack({ refreshParent: true });
+                            } 
+
+                        } catch (error) {
+                            console.error('❌ 提交失败:', error);
+                        }
+                    },
+
+                    // 获取开始时间
+                    getKssj() {
+                        const jc = this.formData.jc;
+                        const rq = this.formData.rq;
+
+                        let time = '';
+                        if (jc === '1') {
+                            time = '08:00:00'; // 上午
+                        } else if (jc === '2') {
+                            time = '14:00:00'; // 下午
+                        } else if (jc === '3') {
+                            time = '19:00:00'; // 晚上
+                        }
+
+                        return `${rq} ${time}`;
+                    },
+
+                    // 获取结束时间
+                    getJssj() {
+                        const jc = this.formData.jc;
+                        const rq = this.formData.rq;
+
+                        let time = '';
+                        if (jc === '1') {
+                            time = '12:00:00'; // 上午
+                        } else if (jc === '2') {
+                            time = '18:00:00'; // 下午
+                        } else if (jc === '3') {
+                            time = '22:00:00'; // 晚上
+                        }
+
+                        return `${rq} ${time}`;
+                    },
+
+                    // 处理底部按钮点击
+                    handleBottomAction(data) {
+                        console.log('底部按钮操作:', data);
+
+                        switch(data.action) {
+                            case 'cancel':
+                                this.handleCancel();
+                                break;
+                            case 'save':
+                                this.handleSave();
+                                break;
+                            default:
+                                console.warn('未知的按钮操作:', data.action);
+                        }
+                    },
+
+                    // 取消返回
+                    handleCancel() {
+                        NavigationManager.goBack({ refreshParent: false });
+                    },
+
+                    
+                }
+            })
+        })
+    </script>
+</body>
+</html>

+ 949 - 0
page/login.html

@@ -0,0 +1,949 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>登录</title>
+    <!-- 引入基础依赖 -->
+    <script src="/js/mp_base/base.js"></script>
+</head>
+<body>
+    <!-- 页面加载状态 -->
+    <div id="page-loading" class="page-loading">
+        <div class="loading-content">
+            <div class="loading-spinner"></div>
+            <div class="loading-text">加载中...</div>
+        </div>
+    </div>
+
+    <div id="app" v-cloak>
+        <div class="login-popup show">
+            <div class="mask" ></div>
+            <div class="popup-content">
+                <!-- <div class="popup-header">
+                    <span class="title">登录</span>
+                </div> -->
+                <!-- 用户协议 -->
+                <div class="agreement">
+                    <input 
+                        type="checkbox" 
+                        v-model="agreed" 
+                        id="agreement-checkbox"
+                        class="agreement-checkbox"
+                    />
+                    <label for="agreement-checkbox" class="agreement-text">
+                        我已阅读并同意
+                        <span class="link" @click="openAgreement('user')">《用户协议》</span>与<span class="link" @click="openAgreement('privacy')">《隐私协议》</span>
+                    </label>
+                </div>
+                <!-- 账号密码登录 -->
+                <div class="account-login">
+                    <div class="input-item">
+                        <Icon name="icon-renyuan" size="32" color="#575d6d" class="input-icon"></Icon>
+                        <input
+                            type="text"
+                            v-model="form.username"
+                            placeholder="请输入您的账号"
+                            class="login-input"
+                        />
+                    </div>
+                    <div class="input-item">
+                        <Icon name="icon-kaisuo" size="32" color="#575d6d" class="input-icon"></Icon>
+                        <input
+                            :type="showPassword ? 'text' : 'password'"
+                            v-model="form.password"
+                            placeholder="请输入您的密码"
+                            class="login-input"
+                        />
+                        <div class="eye-icon" @click="showPassword = !showPassword">
+                            <Icon :name="showPassword ? 'icon-yanjing-kai' : 'icon-yanjing-bi'" size="32" color="#575d6d"></Icon>
+                        </div>
+                    </div>
+                    <button
+                        class="account-btn"
+                        :class="{ 'disabled': isLogging }"
+                        :disabled="isLogging"
+                        @click="handleAccountLogin"
+                    >
+                        <Icon name="icon-tuichu" size="32" color="#fff" class="btn-icon"></Icon>
+                        <span>{{ isLogging ? '登录中...' : '登录' }}</span>
+                    </button>
+                </div>
+
+                
+            </div>
+        </div>
+
+        <!-- Loading 遮罩 -->
+        <div v-if="showLoading" class="loading-mask">
+            <div class="loading-content">
+                <div class="loading-spinner"></div>
+                <div class="loading-text">{{ loadingText }}</div>
+            </div>
+        </div>
+
+        <!-- Toast 提示 -->
+        <div v-if="toast.show" class="toast" :class="toast.type">
+            {{ toast.message }}
+        </div>
+
+        <!-- 自定义确认弹窗 -->
+        <div v-if="confirmDialog.show" class="confirm-dialog-mask">
+            <div class="confirm-dialog">
+                <div class="confirm-content">
+                    <div class="confirm-message">{{ confirmDialog.message }}</div>
+                    <div class="confirm-buttons">
+                        <button class="confirm-btn cancel-btn" @click="handleConfirmCancel">取消</button>
+                        <button class="confirm-btn confirm-btn-primary" @click="handleConfirmOk">确认</button>
+                    </div>
+                </div>
+            </div>
+        </div>
+
+        <!-- 底部抽屉:显示协议内容 -->
+        <div v-if="agreementDrawer.show" class="drawer-mask" @click="closeAgreementDrawer">
+            <div class="drawer-content" @click.stop>
+                <div class="drawer-header">
+                    <div class="drawer-title">{{ agreementDrawer.title }}</div>
+                    <div class="drawer-close" @click="closeAgreementDrawer">
+                        <Icon name="icon-X-xi" size="32" color="#666"></Icon>
+                    </div>
+                </div>
+                <div class="drawer-body">
+                    <div v-html="agreementDrawer.content"></div>
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        form: {
+                            username: '',
+                            password: ''
+                        },
+                        showPassword: false,
+                        agreed: false,
+                        isLogging: false,
+                        showLoading: false,
+                        loadingText: '',
+                        toast: {
+                            show: false,
+                            message: '',
+                            type: 'info'
+                        },
+                        confirmDialog: {
+                            show: false,
+                            message: '',
+                            resolve: null
+                        },
+                        agreementDrawer: {
+                            show: false,
+                            title: '',
+                            content: ''
+                        }
+                    }
+                },
+
+                mounted() {
+                    // 隐藏页面加载状态
+                    const pageLoading = document.getElementById('page-loading')
+                    if (pageLoading) {
+                        pageLoading.style.display = 'none'
+                    }
+
+                    // 初始化页面,使用自定义结果处理
+                    if (typeof initPage === 'function') {
+                        initPage('login', this.handleResult)
+                    }
+
+                    // 获取URL参数,用于调试
+                    const urlParams = this.getUrlParams()
+                    console.log('🔗 H5登录页面参数:', urlParams)
+                },
+
+                methods: {
+                // 获取URL参数
+                getUrlParams() {
+                    const params = {}
+                    const urlSearchParams = new URLSearchParams(window.location.search)
+                    for (const [key, value] of urlSearchParams) {
+                        params[key] = decodeURIComponent(value)
+                    }
+                    return params
+                },
+
+                // 处理操作结果(来自小程序的回调)
+                handleResult(data) {
+                    console.log('🎯 收到小程序回调:', data)
+                    // 这里可以处理来自小程序的各种回调
+                },
+
+                // 显示Loading
+                showLoadingMask(text = '加载中...') {
+                    this.showLoading = true
+                    this.loadingText = text
+                },
+
+                // 隐藏Loading
+                hideLoadingMask() {
+                    this.showLoading = false
+                    this.loadingText = ''
+                },
+
+                // 显示Toast
+                showToast(message, type = 'info', duration = 3000) {
+                    this.toast = {
+                        show: true,
+                        message,
+                        type
+                    }
+                    setTimeout(() => {
+                        this.toast.show = false
+                    }, duration)
+                },
+
+                // 自定义确认对话框
+                showConfirm(message) {
+                    return new Promise((resolve) => {
+                        this.confirmDialog = {
+                            show: true,
+                            message: message,
+                            resolve: resolve
+                        }
+                    })
+                },
+
+                // 处理确认对话框 - 取消
+                handleConfirmCancel() {
+                    this.confirmDialog.show = false
+                    if (this.confirmDialog.resolve) {
+                        this.confirmDialog.resolve(false)
+                    }
+                    this.confirmDialog.resolve = null
+                },
+
+                // 处理确认对话框 - 确认
+                handleConfirmOk() {
+                    this.confirmDialog.show = false
+                    if (this.confirmDialog.resolve) {
+                        this.confirmDialog.resolve(true)
+                    }
+                    this.confirmDialog.resolve = null
+                },
+
+               
+
+                // 账号密码登录
+                async handleAccountLogin() {
+                    if (this.isLogging) {
+                        console.log('正在登录中,请勿重复点击')
+                        return
+                    }
+
+                    if (!this.agreed) {
+                        // 使用自定义确认对话框替换原生confirm
+                        const userConfirmed = await this.showConfirm('是否同意用户协议和隐私协议?');
+                        if (userConfirmed) {
+                            this.agreed = true; // 用户点击了“确定”,视为同意
+                            // 继续执行后续逻辑
+                        } else {
+                            return; // 用户点击了“取消”,中断操作
+                        }
+                        
+                    }
+
+                    if (!this.form.username || !this.form.password) {
+                        this.showToast('请输入用户名和密码', 'warning')
+                        return
+                    }
+
+                    try {
+                        this.isLogging = true
+                        this.showLoadingMask('登录中...')
+
+                        // 获取URL参数中的wechatCode
+                        const urlParams = this.getUrlParams()
+                        const wechatCode = urlParams.wechatCode || ''
+                        console.log('🔗 使用wechatCode:', wechatCode)
+
+                        // 使用真实的API进行登录
+                        const response = await h5UserApi.accountLogin({
+                            yhm: this.form.username,
+                            mm: this.form.password,
+                            wdConfirmationCaptchaService: "0",
+                            wechatCode: wechatCode // 传递wechatCode
+                        })
+
+                        // 检查是否有错误信息
+                        if (response && response.data && response.data.msg) {
+                            // 登录失败,显示错误信息
+                            console.error('❌ 登录失败:', response.data.msg)
+                            this.showToast(response.data.msg, 'error')
+                            return
+                        }
+
+                        console.log("response:",response)
+                        // 检查登录是否成功
+                        if (response && response.data ) {
+                            if (typeof response.data === 'string' && response.data.includes('页面执行时错误')) {
+                                throw new Error('服务器处理错误')
+                                return
+                            }
+                            const userData = {
+                                devId:response.data.devId,
+                                sbmc:response.data.sbmc,
+                                sessId:response.data.sessId,
+                                xm:response.data.xm,
+                                yhsbToken:response.data.yhsbToken,
+                                onlineToken:response.data.onlineToken,
+                                syList:response.data.sylist,
+                                yhid:response.data.yhid,
+                                yhm:response.data.yhm
+                            }
+
+                            // 保存用户信息到H5本地存储
+                            h5UserApi.saveUserInfo(userData)
+
+                            // 准备返回数据给小程序
+                            const loginResult = {
+                                success: true,
+                                userInfo: userData,
+                            }
+
+                            console.log('✅ 登录成功,准备返回数据:', loginResult)
+                            // this.showToast('登录成功!', 'success')
+
+                            // 延迟一下让用户看到成功提示
+                            setTimeout(() => {
+                                // 通过h5-bridge返回登录结果给小程序
+                                this.returnLoginResult(loginResult)
+                            }, 1000)
+                        } else {
+                            throw new Error('登录响应数据无效')
+                        }
+
+                    } catch (error) {
+                        console.error('❌ 登录失败:', error)
+                        this.showToast('登录失败: ' + error.message, 'error')
+                    } finally {
+                        this.isLogging = false
+                        this.hideLoadingMask()
+                    }
+                },
+
+                // 返回登录结果给小程序
+                returnLoginResult(result) {
+                    // console.log('🔄 返回登录结果给小程序:', result)
+                    
+                    // 使用h5-bridge的数据传递功能
+                    // 这里需要跳转到h5-controller中转页来传递数据
+                    callNative('loginSuccess', '登录成功', result)
+                },
+
+                // 打开协议页面
+                openAgreement(type) {
+                    const agreementData = {
+                        user: {
+                            title: '用户协议',
+                            content: `
+                                <h3>用户协议</h3>
+                                <p>欢迎使用本平台服务。在使用本平台服务前,请您仔细阅读并充分理解本协议内容。</p>
+                                <h4>1. 服务条款的接受</h4>
+                                <p>您使用本平台服务即视为您已阅读并同意接受本协议的全部条款。</p>
+                                <h4>2. 账号注册和使用</h4>
+                                <p>用户在注册账号时,应提供真实、准确、完整的个人信息。</p>
+                                <h4>3. 用户权利和义务</h4>
+                                <p>用户有权使用本平台提供的各项服务,但应遵守相关法律法规。</p>
+                                <h4>4. 隐私保护</h4>
+                                <p>我们承诺保护用户的个人信息安全,不会泄露用户隐私。</p>
+                                <p style="margin-top: 20px; color: #999;">本协议最终解释权归本平台所有。</p>
+                            `
+                        },
+                        privacy: {
+                            title: '隐私协议',
+                            content: `
+                                <h3>隐私协议</h3>
+                                <p>本指引是海丰县德成中英文学校小程序开发者 海丰县德成中英文学校(以下简称"开发者")为处理你的个人信息而制定。</p>
+
+                                <h4>开发者处理的信息</h4>
+                                <p>根据法律规定,开发者仅处理实现小程序功能所必要的信息。</p>
+                                <p>开发者收集你的设备信息,用于识别你的账号登录设备。</p>
+                                <p>开发者将在获取你的明示同意后,收集你的微信昵称、头像,用途是【更新OA系统中你的头像信息】</p>
+
+                                <h4>你的权益</h4>
+                                <p>关于你的个人信息,你可以通过以下方式与开发者联系,行使查阅、复制、更正、删除等法定权利。</p>
+                                <p>若你在小程序中注,不会对外公开披露你的信息,如必须公开披露时,开发者应当向你告知公开披露的目的、披露信息的类型及可能涉及的信息,并征得你的单独同意。</p>
+                                <p>你认为开发者未遵守上述约定,或有其他的投诉建议、或未成年人个人信息保护相关问题,可通过以下方式与开发者联系;或者向微信进行投诉。</p>
+                                <p><strong>邮箱:</strong> DC1612356478@163.com</p>
+
+                                <p style="margin-top: 20px; color: #999;">更新日期:2025-10-11</p>
+                            `
+                        }
+                    };
+
+                    const agreement = agreementData[type];
+                    if (agreement) {
+                        this.agreementDrawer = {
+                            show: true,
+                            title: agreement.title,
+                            content: agreement.content
+                        };
+                    }
+                },
+
+                // 关闭协议抽屉
+                closeAgreementDrawer() {
+                    this.agreementDrawer.show = false;
+                }
+            }
+        })
+
+        console.log('✅ 登录页面初始化完成')
+        })
+    </script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        /* 页面加载状态 */
+        .page-loading {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: #f5f5f5;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            z-index: 9999;
+        }
+
+        .page-loading .loading-content {
+            text-align: center;
+        }
+
+        .page-loading .loading-spinner {
+            width: 40px;
+            height: 40px;
+            border: 4px solid #f3f3f3;
+            border-top: 4px solid #40ac6d;
+            border-radius: 50%;
+            animation: page-spin 1s linear infinite;
+            margin: 0 auto 15px;
+        }
+
+        @keyframes page-spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+
+        .page-loading .loading-text {
+            color: #666;
+            font-size: 14px;
+        }
+
+        * {
+            margin: 0;
+            padding: 0;
+            box-sizing: border-box;
+        }
+
+        body {
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+            background: #f5f5f5;
+            overflow: hidden;
+        }
+
+        .login-popup {
+            position: fixed;
+            left: 0;
+            right: 0;
+            top: 0;
+            bottom: 0;
+            z-index: 9999;
+            visibility: hidden;
+        }
+
+        .login-popup.show {
+            visibility: visible;
+        }
+
+        .mask {
+            position: absolute;
+            left: 0;
+            right: 0;
+            top: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.6);
+        }
+
+        .popup-content {
+            width: calc(100% - 20px);
+            position: absolute;
+            left: 50%;
+        
+            top: 50%;
+            background: #fff;
+            border-radius: 4px;
+            padding: 30px 20px;
+            transform: translate(-50%, -50%);
+            transition: transform 0.3s ease-out;
+            border: 1px solid #d3d4d5;
+            max-height: 80vh;
+            overflow-y: auto;
+        }
+
+        .popup-header {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            margin-bottom: 20px;
+        }
+
+        .title {
+            font-size: 18px;
+            font-weight: bold;
+            color: #333;
+        }
+
+        .close-btn {
+            font-size: 24px;
+            color: #999;
+            padding: 10px;
+            margin: -10px;
+            cursor: pointer;
+            user-select: none;
+        }
+
+        .close-btn:hover {
+            color: #666;
+        }
+
+        
+
+        .input-item {
+            position: relative;
+            margin-bottom: 15px;
+            display: flex;
+        }
+
+        .input-icon {
+            position: absolute;
+            left: 18px;
+            top: 50%;
+            transform: translateY(-50%);
+            z-index: 1;
+        }
+
+        .login-input {
+            width: 100%;
+            height: 44px;
+            /* background: #f5f5f5; */
+            border-radius: 4px;
+            padding: 0 40px 0 50px;
+            font-size: 16px;
+            border: none;
+            outline: none;
+            border: 1px solid #d5d8dc;
+        }
+
+        .login-input:focus {
+            /* background: #f0f0f0; */
+            border: 1px solid #d0d2d6;
+        }
+
+        .eye-icon {
+            position: absolute;
+            right: 10px;
+            top: 50%;
+            transform: translateY(-50%);
+            padding: 10px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            z-index: 1000;
+        }
+
+        .account-btn {
+            width: 100%;
+            height: 44px;
+            line-height: 44px;
+            background: #575d6d;
+            color: #fff;
+            border-radius: 4px;
+            font-size: 16px;
+            border: none;
+            margin-top: 35px;
+            transition: all 0.3s ease;
+            cursor: pointer;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            gap: 8px;
+        }
+
+        .btn-icon {
+            display: inline-block;
+        }
+
+        .account-btn span {
+            line-height: 1;
+        }
+
+        .account-btn:hover {
+            background: #555;
+        }
+
+        .account-btn.disabled {
+            background: #cccccc !important;
+            color: #999999 !important;
+            opacity: 0.6;
+            cursor: not-allowed;
+        }
+
+        /* 用户协议 */
+        .agreement {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-bottom: 25px;
+        }
+
+        /* 自定义checkbox样式 */
+        .agreement-checkbox {
+            position: relative;
+            margin-right: 8px;
+            width: 20px;
+            height: 20px;
+            appearance: none;
+            -webkit-appearance: none;
+            border: 2px solid #d5d8dc;
+            border-radius: 4px;
+            outline: none;
+            cursor: pointer;
+            transition: all 0.2s;
+        }
+
+        .agreement-checkbox:hover {
+            border-color: #575d6d;
+        }
+
+        .agreement-checkbox:checked {
+            background-color: #575d6d;
+            border-color: #575d6d;
+        }
+
+        .agreement-checkbox:checked::after {
+            content: '';
+            position: absolute;
+            left: 5px;
+            top: 0;
+            width: 5px;
+            height: 10px;
+            border: solid white;
+            border-width: 0 2px 2px 0;
+            transform: rotate(45deg);
+        }
+
+        .agreement-text {
+            font-size: 0.372rem;
+            font-weight: 500;
+            color: #999;
+            cursor: pointer;
+        }
+
+        .link {
+            /* color: #40ac6d; */
+            cursor: pointer;
+        }
+
+        .link:hover {
+            text-decoration: underline;
+        }
+
+        /* Loading 遮罩 */
+        .loading-mask {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.5);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            z-index: 10000;
+        }
+
+        .loading-content {
+            background: white;
+            padding: 20px;
+            border-radius: 4px;
+            text-align: center;
+            min-width: 120px;
+        }
+
+        .loading-spinner {
+            width: 30px;
+            height: 30px;
+            border: 3px solid #f3f3f3;
+            border-top: 3px solid #40ac6d;
+            border-radius: 50%;
+            animation: spin 1s linear infinite;
+            margin: 0 auto 10px;
+        }
+
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+
+        .loading-text {
+            color: #333;
+            font-size: 14px;
+        }
+
+        /* Toast 提示 */
+        .toast {
+            position: fixed;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+            background: rgba(0, 0, 0, 0.8);
+            color: white;
+            padding: 12px 20px;
+            border-radius: 4px;
+            font-size: 14px;
+            z-index: 10001;
+            max-width: 80%;
+            text-align: center;
+        }
+
+        .toast.success {
+            background: rgba(40, 167, 69, 0.9);
+        }
+
+        .toast.warning {
+            background: rgba(255, 193, 7, 0.9);
+            color: #212529;
+        }
+
+        .toast.error {
+            background: rgba(220, 53, 69, 0.9);
+        }
+
+        /* 自定义确认对话框 - 优化样式 */
+        .confirm-dialog-mask {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.6);
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            z-index: 10002;
+        }
+
+        .confirm-dialog {
+            background: white;
+            border-radius: 4px;
+            min-width: 280px;
+            max-width: 90%;
+            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.15);
+            overflow: hidden;
+            animation: confirmFadeIn 0.3s ease-out;
+        }
+
+        @keyframes confirmFadeIn {
+            from {
+                opacity: 0;
+                transform: scale(0.95) translateY(-20px);
+            }
+            to {
+                opacity: 1;
+                transform: scale(1) translateY(0);
+            }
+        }
+
+        .confirm-content {
+            padding: 24px 20px 20px;
+        }
+
+        .confirm-message {
+            font-size: 16px;
+            color: #333;
+            text-align: center;
+            margin-bottom: 24px;
+            line-height: 1.6;
+        }
+
+        .confirm-buttons {
+            display: flex;
+            gap: 10px;
+        }
+
+        .confirm-btn {
+            flex: 1;
+            height: 40px;
+            border: none;
+            border-radius: 4px;
+            font-size: 15px;
+            cursor: pointer;
+            transition: all 0.2s ease;
+            font-weight: 500;
+        }
+
+        .cancel-btn {
+            background: #f5f5f5;
+            color: #666;
+            border: 1px solid #e5e5e5;
+        }
+
+        .cancel-btn:hover {
+            background: #eeeeee;
+            border-color: #d5d5d5;
+        }
+
+        .confirm-btn-primary {
+            background: #575d6d;
+            color: white;
+        }
+
+        .confirm-btn-primary:hover {
+            background: #494e5b;
+        }
+
+        .confirm-btn:active {
+            transform: scale(0.98);
+        }
+
+        /* 底部抽屉样式 */
+        .drawer-mask {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.6);
+            z-index: 10003;
+            animation: drawerMaskFadeIn 0.3s ease-out;
+        }
+
+        @keyframes drawerMaskFadeIn {
+            from { opacity: 0; }
+            to { opacity: 1; }
+        }
+
+        .drawer-content {
+            position: absolute;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            max-height: 80vh;
+            background: white;
+            border-radius: 4px 4px 0 0;
+            animation: drawerSlideUp 0.3s ease-out;
+            display: flex;
+            flex-direction: column;
+        }
+
+        @keyframes drawerSlideUp {
+            from {
+                transform: translateY(100%);
+            }
+            to {
+                transform: translateY(0);
+            }
+        }
+
+        .drawer-header {
+            display: flex;
+            align-items: center;
+            justify-content: space-between;
+            padding: 16px 20px;
+            border-bottom: 1px solid #f0f0f0;
+            flex-shrink: 0;
+        }
+
+        .drawer-title {
+            font-size: 18px;
+            font-weight: 600;
+            color: #333;
+        }
+
+        .drawer-close {
+            width: 32px;
+            height: 32px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            border-radius: 4px;
+            transition: background 0.2s;
+        }
+
+        .drawer-close:hover {
+            background: #f5f5f5;
+        }
+
+        .drawer-body {
+            flex: 1;
+            overflow-y: auto;
+            padding: 20px;
+            -webkit-overflow-scrolling: touch;
+        }
+
+        .drawer-body h3 {
+            font-size: 18px;
+            color: #333;
+            margin-bottom: 16px;
+        }
+
+        .drawer-body h4 {
+            font-size: 16px;
+            color: #555;
+            margin-top: 20px;
+            margin-bottom: 12px;
+        }
+
+        .drawer-body p {
+            font-size: 14px;
+            color: #666;
+            line-height: 1.8;
+            margin-bottom: 12px;
+        }
+
+        /* 响应式设计 */
+        /* @media (max-width: 480px) {
+            .popup-content {
+                padding: 15px 10px;
+            }
+
+            .login-input {
+                height: 40px;
+                font-size: 13px;
+            }
+
+            .account-btn {
+                height: 40px;
+                line-height: 40px;
+                font-size: 14px;
+            }
+        } */
+    </style>
+</body>
+</html>

+ 216 - 0
page/mp_addSure.html

@@ -0,0 +1,216 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>确认提交</title>
+    <script src="/js/mp_base/base.js"></script>
+    <style>
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            min-height: 100vh;
+            background: #f5f5f5;
+            box-sizing: border-box;
+        }
+
+        .form-wrap {
+            background: #fff;
+        }
+
+        .table th {
+            width: 140px;
+            max-width: 170px;
+        }
+
+        .desc-editor {
+            width: 100%;
+        }
+    </style>
+</head>
+<body>
+<div id="app" v-cloak>
+    <form id="tjform" class="form-wrap" @submit.prevent>
+        <table class="table">
+            <tr v-if="showPostRow">
+                <th>岗位</th>
+                <td>
+                    <!-- 功能说明:岗位下拉使用组件内置 cb 拉取(对齐PC addSure 的 objp.ss cb=bpmtjgwid) by xu 2026-02-28 -->
+                    <ss-select
+                        v-model="formData.bpmtjgwid"
+                        cb="bpmtjgwid"
+                        :auto-select-first="true"
+                        placeholder="请选择提交岗位"
+                        @loaded="handlePostOptionsLoaded"
+                    >
+                    </ss-select>
+                </td>
+            </tr>
+            <tr>
+                <th>{{ descLabel }}</th>
+                <td>
+                    <!-- 功能说明:addSure 说明区改用 ss-editor(对齐PC editor.ss),提交时仍回写 sqms/sqmswj by xu 2026-02-28 -->
+                    <ss-editor
+                        class="desc-editor"
+                        name="sqmswj"
+                        :model-value="formData.sqmswj"
+                        placeholder="请输入说明"
+                        :height="220"
+                        @change="handleEditorChange"
+                    />
+                </td>
+            </tr>
+        </table>
+    </form>
+
+    <ss-bottom
+        :buttons="bottomButtons"
+        @button-click="handleBottomAction"
+    >
+    </ss-bottom>
+</div>
+
+<script>
+    window.SS.ready(function () {
+        window.SS.dom.initializeFormApp({
+            el: '#app',
+            data() {
+                return {
+                    pageParams: {},
+                    submitting: false,
+                    showPostRow: true,
+                    descLabel: '说明',
+                    formData: {
+                        bpmtjgwid: '',
+                        sqms: '',
+                        sqmswj: '',
+                        sqfjid: '',
+                    },
+                    bottomButtons: [
+                        {
+                            text: '确定',
+                            action: 'confirm',
+                            backgroundColor: '#575d6d',
+                            color: '#fff'
+                        }
+                    ]
+                }
+            },
+            mounted() {
+                this.pageParams = this.getUrlParams()
+                // 功能说明:addSure 说明预填走通用字段优先级(sqms/sqmswj/ms),避免仅适配某个业务 by xu 2026-02-28
+                const presetDesc = this.pageParams.sqms || this.pageParams.sqmswj || this.pageParams.ms || ''
+                if (presetDesc) {
+                    this.formData.sqms = String(presetDesc)
+                    this.formData.sqmswj = String(presetDesc)
+                }
+
+                // 功能说明:对齐PC addSure:ssObjName=ws 时标题为“拟办意见”,否则为“说明” by xu 2026-02-28
+                this.descLabel = String(this.pageParams.ssObjName || '').trim() === 'ws' ? '拟办意见' : '说明'
+
+                // 功能说明:对齐PC addSure:print 非空时显示打印按钮 by xu 2026-02-28
+                if (this.pageParams.print !== undefined && this.pageParams.print !== null && this.pageParams.print !== '') {
+                    this.bottomButtons = [
+                        { text: '打印', action: 'print' },
+                        {
+                            text: '确定',
+                            action: 'confirm',
+                            backgroundColor: '#575d6d',
+                            color: '#fff'
+                        }
+                    ]
+                }
+            },
+            methods: {
+                getUrlParams() {
+                    const params = {}
+                    const urlSearchParams = new URLSearchParams(window.location.search)
+                    for (const [key, value] of urlSearchParams) {
+                        params[key] = decodeURIComponent(value)
+                    }
+                    return params
+                },
+                goBack(refreshParent = false) {
+                    if (window.NavigationManager && typeof window.NavigationManager.goBack === 'function') {
+                        window.NavigationManager.goBack({ refreshParent })
+                    } else {
+                        window.history.back()
+                    }
+                },
+                // 功能说明:对齐PC addSure:岗位下拉无选项时隐藏整行 by xu 2026-02-28
+                handlePostOptionsLoaded(options) {
+                    const arr = Array.isArray(options) ? options : []
+                    this.showPostRow = arr.length > 0
+                },
+                // 功能说明:富文本内容同步回 sqms/sqmswj,保持与后端参数兼容 by xu 2026-02-28
+                handleEditorChange(content) {
+                    const text = String(content || '')
+                    this.formData.sqms = text
+                    this.formData.sqmswj = text
+                },
+                handlePrint() {
+                    try {
+                        window.print()
+                    } catch (_) {}
+                },
+                // 功能说明:移动端 addSure “确定”提交 tjform 对应参数到 sureAdd 接口 by xu 2026-02-28
+                async submitTjForm() {
+                    if (this.submitting) return
+
+                    const ssObjName = String(this.pageParams.ssObjName || '').trim()
+                    const ssObjIdName = String(this.pageParams.ssObjIdName || 'ssObjId').trim()
+                    const ssObjId = String(this.pageParams.ssObjId || '').trim()
+                    if (!ssObjName || !ssObjId) {
+                        if (typeof showToastEffect === 'function') showToastEffect('缺少对象参数,无法确认提交', 2200, 'error')
+                        return
+                    }
+
+                    this.submitting = true
+                    try {
+                        const payload = {
+                            ssObjName,
+                            [ssObjIdName]: ssObjId,
+                            bpmtjgwid: this.formData.bpmtjgwid || '',
+                            sqms: this.formData.sqms || '',
+                            sqmswj: this.formData.sqmswj || this.formData.sqms || '',
+                            sqfjid: this.formData.sqfjid || '',
+                        }
+
+                        const response = await request.post(
+                            '/service?ssServ=sureAdd',
+                            payload,
+                            { loading: true, formData: true }
+                        )
+                        console.log('[mp_addSure] sureAdd响应', response)
+
+                        if (typeof showToastEffect === 'function') {
+                            showToastEffect('确认提交成功', 1500, 'success')
+                        }
+                        setTimeout(() => this.goBack(true), 600)
+                    } catch (error) {
+                        console.error('[mp_addSure] sureAdd提交失败', error)
+                        if (typeof showToastEffect === 'function') {
+                            showToastEffect('确认提交失败,请稍后重试', 2200, 'error')
+                        }
+                    } finally {
+                        this.submitting = false
+                    }
+                },
+                handleBottomAction(payload) {
+                    if (!payload || !payload.action) return
+                    if (payload.action === 'print') {
+                        this.handlePrint()
+                        return
+                    }
+                    if (payload.action === 'confirm') {
+                        this.submitTjForm()
+                    }
+                }
+            }
+        })
+    })
+</script>
+</body>
+</html>

+ 682 - 0
page/mp_ccChk.html

@@ -0,0 +1,682 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>审核</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            height: 100vh;
+            display: flex;
+            flex-direction: column;
+            overflow: hidden; /* 防止页面级别滚动 */
+            box-sizing: border-box; /* 确保padding计算在高度内 */
+        }
+
+        /* iframe样式 */
+        .top-iframe {
+            background: #fff;
+            overflow: hidden;
+            border: none;
+            display: block;
+            box-sizing: border-box;
+        }
+
+        .bottom-iframe {
+            background: #fff;
+            overflow: hidden;
+            border: none;
+            display: block;
+            box-sizing: border-box;
+        }
+
+        .title{
+            flex-shrink: 0;
+            box-sizing: border-box;
+            font-size: 16px;
+            text-align: center;
+            /* margin: 16px auto; */
+            height: 48px;
+            line-height: 48px;
+        }
+
+        /* 录入div样式 */
+        .review-input-popup {
+            position: fixed;
+            bottom: 0;
+            left: 0;
+            right: 0;
+            height: 50px;
+            background: #e6e6e6;
+            display: flex;
+            align-items: center;
+            padding: 0 0 0 10px;
+            z-index: 1000;
+            transform: translateY(100%);
+            transition: transform 0.3s ease;
+            box-sizing: border-box;
+        }
+
+        .review-input-popup.show {
+            transform: translateY(0);
+        }
+
+        .review-input-wrapper {
+            flex: 1;
+            display: flex;
+            align-items: center;
+            gap: 4px;
+        }
+
+        .review-input {
+            flex: 1;
+            height: 32px;
+            border: none;
+            padding: 0 10px;
+            font-size: 16px;
+            background: #fff;
+            outline: none;
+            box-sizing: border-box;
+        }
+
+        .common-phrases-btn {
+            padding: 6px 12px;
+            background: #fff;
+            border: none;
+            font-size: 16px;
+            color: #333;
+            cursor: pointer;
+            white-space: nowrap;
+            margin-right: 4px;
+        }
+
+        .common-phrases-btn:hover {
+            background: #e0e0e0;
+        }
+
+        .submit-btn {
+            box-sizing: border-box;
+            height: 50px;
+            padding: 6px 16px;
+            border: none;
+            font-size: 16px;
+            color: #fff;
+            cursor: pointer;
+            white-space: nowrap;
+            transition: background-color 0.2s;
+        }
+
+        .submit-btn.agree {
+            background: #585e6e;
+        }
+
+        .submit-btn.agree:active {
+            background: #242835;
+        }
+
+        .submit-btn.reject {
+            background: #e58846;
+        }
+
+        .submit-btn.reject:active {
+            background: #eb6100;
+        }
+
+        /* 常用语popup */
+        .common-phrases-popup {
+            position: fixed;
+            bottom: 50px;
+            left: 10px;
+            right: 10px;
+            background: #fff;
+            border-radius: 4px;
+            box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
+            max-height: 200px;
+            overflow-y: auto;
+            z-index: 1001;
+            display: none;
+        }
+
+        .common-phrases-popup.show {
+            display: block;
+        }
+
+        .phrase-item {
+            padding: 12px 16px;
+            border-bottom: 1px solid #f0f0f0;
+            cursor: pointer;
+            font-size: 14px;
+        }
+
+        .phrase-item:hover {
+            background: #f8f8f8;
+        }
+
+        .phrase-item:last-child {
+            border-bottom: none;
+        }
+
+        /* 原ss-bottom隐藏 */
+        .ss-bottom.hidden {
+            display: none;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <div class="title" @click="callApi">
+            {{jdmc}}
+        </div>
+        <!-- 上方iframe - 基本信息区域 -->
+        <iframe
+            ref="topIframe"
+            class="top-iframe"
+            :src="`/page/mp_objInfo.html?sqid=${sqid}&shid=${shid}&bdlbm=${bdlbm}&dataType=${dataType}&encode_shid=${encode_shid}&ssObjName=${ssObjName}&ssObjId=${ssObjId}`"
+            width="100%"
+            frameborder="0">
+        </iframe>
+
+        <!-- 下方iframe - 审核列表区域 -->
+        <iframe
+            ref="bottomIframe"
+            class="bottom-iframe"
+            :src="`/page/mp_shList.html?sqid=${sqid}&shid=${shid}&bdlbm=${bdlbm}&dataType=${dataType}&encode_shid=${encode_shid}&ssObjName=${ssObjName}&ssObjId=${ssObjId}`"
+            width="100%"
+            frameborder="0">
+        </iframe>
+
+        <!-- 底部按钮 -->
+        <ss-bottom
+            :show-shyj="false"
+            :buttons="bottomButtons"
+            @button-click="handleBottomAction"
+            :divider="false"
+            :disabled="submitting"
+            v-if="bottomButtons.length > 0"
+        ></ss-bottom>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        pageParams: {},
+                        loading: false,
+                        jdmc:'',
+                        sqid: '',
+                        shid: '',
+                        bdlbm: '',
+                        dataType:'bdplay',
+                        encode_shid:'',
+                        ssObjName:'',
+                        ssObjId:'',
+                        agrCcData: null, // 存储agrCc返回的数据
+
+
+                        // 底部按钮相关
+                        submitting: false,
+                        bottomButtons: [],
+
+                        // iframe布局相关
+                        layoutCalculated: false,
+                        headerSectionHeight: 0,
+                        availableHeight: 0,
+                        topIframeHeight: 0,
+                        bottomIframeHeight: 0,
+                        bottomIframeMinHeight: 0,
+                        isExpanded: false,
+
+                        // 拖拽相关
+                        isDragging: false,
+                        dragStartY: 0,
+                        dragStartBottomHeight: 0,
+                    }
+                },
+                mounted() {
+                    // 获取URL参数
+                    this.pageParams = this.getUrlParams()
+                    console.log('🔗 mp_ccChk页面接收到参数:', this.pageParams)
+
+                    // 打印所有参数到控制台
+                    Object.keys(this.pageParams).forEach(key => {
+                        console.log(`参数 ${key}:`, this.pageParams[key])
+                    })
+                    
+                    this.callApi()
+
+                    // 添加iframe消息监听器
+                    window.addEventListener('message', this.handleIframeMessage)
+
+                    // 初始化布局计算
+                    this.$nextTick(() => {
+                        setTimeout(() => {
+                            this.initializeLayout()
+                        }, 500) // 等待iframe加载完成
+                    })
+                },
+                beforeDestroy() {
+                    // 清理事件监听器
+                    window.removeEventListener('message', this.handleIframeMessage)
+                },
+                methods: {
+                    // 获取URL参数
+                    getUrlParams() {
+                        const params = {}
+                        const urlSearchParams = new URLSearchParams(window.location.search)
+                        for (const [key, value] of urlSearchParams) {
+                            params[key] = decodeURIComponent(value)
+                        }
+                        return params
+                    },
+
+                    // 调用API
+                    async callApi() {
+                        if (!this.pageParams.ssToken) {
+                            alert('未找到ssToken参数')
+                            return
+                        }
+
+                        this.loading = true
+                        this.apiResult = null
+
+                        try {
+                            console.log('🚀 开始调用API,ssToken:', this.pageParams.ssToken)
+                            const apiResponse = await request.post(
+                                `/service?ssToken=${this.pageParams.ssToken}`,
+                                {},
+                                { loading: false }
+                            )
+                            console.log('✅ 原有API响应:', apiResponse)
+                            this.sqid = apiResponse.data.sqid
+                            this.shid = apiResponse.data.shid
+                            this.ssObjName = apiResponse.data.ssObjName
+                            this.ssObjId = apiResponse.data.ssObjId
+                            this.bdlbm = apiResponse.data.bdlbm
+                            this.jdmc = decodeURIComponent(apiResponse.data.jdmc)
+
+
+                            console.log('📋 调用dataTag服务获取agrCc数据...')
+                            const agrCcResponse = await request.post(
+                                `/service?ssServ=dataTag&ssDest=data&name=agrCc&ssObjName=${this.ssObjName}&ssObjId=${this.ssObjId}&sqid=${this.sqid}&shid=${this.shid}&bdlbm=${this.bdlbm}&dataType=${this.dataType}&encode_shid=${this.encode_shid}`,
+                                
+                                { loading: false,formData:true }
+                            )
+
+                            // 保存agrCc数据并设置底部按钮
+                            if (agrCcResponse && agrCcResponse.data && agrCcResponse.data.agrCc) {
+                                this.agrCcData = agrCcResponse.data.agrCc
+                                console.log('📦 agrCc数据:', this.agrCcData)
+
+                                // 根据agrCc数据设置底部按钮
+                                this.bottomButtons = [{
+                                    text: '确认',
+                                    action: 'submit',
+                                    backgroundColor: '#585e6e',
+                                    color: '#fff',
+                                    clickBgColor: '#242835'
+                                }]
+                            }
+
+
+
+                        } catch (error) {
+                            console.error('❌ API调用失败:', error)
+                        } finally {
+                            this.loading = false
+                        }
+                    },
+
+                    // 处理底部按钮点击
+                    handleBottomAction(data) {
+                        if (data.action === 'submit') {
+                            this.handleConfirm()
+                        }
+                    },
+
+                    // 处理确认提交
+                    async handleConfirm() {
+                        if (!this.agrCcData) {
+                            alert('缺少必要的配置信息')
+                            return
+                        }
+
+                        if (this.submitting) return
+
+                        try {
+                            this.submitting = true
+                            console.log('📝 开始提交...')
+                            console.log('📦 提交参数:', {
+                                service: this.agrCcData.service,
+                                dest: this.agrCcData.dest,
+                                shid: this.shid
+                            })
+
+                            const response = await request.post(
+                                `/service?ssServ=${this.agrCcData.service}&ssDest=${this.agrCcData.dest}`,
+                                { shid: this.shid },
+                                { loading: true, formData: true }
+                            )
+                            console.log('✅ 提交成功:', response)
+
+                            
+                            NavigationManager.goBack({ refreshParent: true })
+                            
+
+                        } catch (error) {
+                            console.error('❌ 提交失败:', error)
+                            alert('提交失败: ' + (error.message || '未知错误'))
+                        } finally {
+                            this.submitting = false
+                        }
+                    },
+
+                    // ===== 布局相关方法 =====
+                    // 初始化布局计算
+                    initializeLayout() {
+                        console.log('🔄 开始初始化布局计算')
+
+                        try {
+                            // 获取bottom iframe
+                            const bottomIframe = document.querySelector('.bottom-iframe')
+                            const bottomWindow = bottomIframe?.contentWindow
+
+                            if (!bottomWindow) {
+                                console.error('无法获取bottom iframe的contentWindow')
+                                return
+                            }
+
+                            // 等待iframe加载完成,然后检查header-section
+                            const waitForIframeLoad = () => {
+                                if (!bottomIframe.contentWindow) {
+                                    console.log('⏳ 等待iframe contentWindow...')
+                                    setTimeout(waitForIframeLoad, 100)
+                                    return
+                                }
+
+                                const bottomWindow = bottomIframe.contentWindow
+                                console.log('✅ 获取到iframe contentWindow')
+
+                                // 等待iframe中的DOM加载完成
+                                const checkHeaderSection = () => {
+                                    try {
+                                        const headerSection = bottomWindow.document.querySelector('.header-section')
+                                        if (headerSection) {
+                                            const headerHeight = headerSection.offsetHeight
+                                            console.log('✅ 计算到header-section高度:', headerHeight)
+
+                                            // 计算布局参数
+                                            const viewportHeight = window.innerHeight
+
+                                            // 动态获取title的实际高度
+                                            const titleElement = document.querySelector('.title')
+                                            const actualTitleHeight = titleElement ? titleElement.offsetHeight : 0
+
+                                            // 等待bottom按钮渲染完成并获取实际高度
+                                            const checkBottomButton = () => {
+                                                try {
+                                                    const bottomElement = document.querySelector('ss-bottom')
+                                                    const actualBottomHeight = bottomElement ? bottomElement.offsetHeight : 50
+
+                                                    this.headerSectionHeight = headerHeight
+                                                    this.bottomIframeMinHeight = headerHeight
+                                                    this.availableHeight = viewportHeight - actualTitleHeight - actualBottomHeight
+
+                                                    // 初始状态:底部iframe刚好显示header-section高度
+                                                    this.bottomIframeHeight = this.bottomIframeMinHeight
+                                                    this.topIframeHeight = this.availableHeight - this.bottomIframeMinHeight
+                                                    this.isExpanded = false // 初始为收起状态
+                                                    this.layoutCalculated = true
+
+                                                    // 应用计算后的高度
+                                                    this.applyIframeHeights()
+
+                                                    console.log('📊 初始布局计算完成(仅显示header-section):', {
+                                                        '视口高度': viewportHeight,
+                                                        '标题实际高度': actualTitleHeight,
+                                                        '底部按钮实际高度': actualBottomHeight,
+                                                        '可用总高度': this.availableHeight,
+                                                        'header-section高度': headerHeight,
+                                                        '上iframe高度': this.topIframeHeight,
+                                                        '下iframe高度': this.bottomIframeHeight,
+                                                        'iframe总高度': this.topIframeHeight + this.bottomIframeHeight,
+                                                        '是否超限': (this.topIframeHeight + this.bottomIframeHeight) > this.availableHeight,
+                                                        '差额': (this.topIframeHeight + this.bottomIframeHeight) - this.availableHeight,
+                                                        isExpanded: this.isExpanded
+                                                    })
+                                                } catch (bottomError) {
+                                                    console.error('❌ 获取底部按钮高度时出错:', bottomError)
+                                                    // 使用默认高度50px重试
+                                                    const defaultBottomHeight = 50
+                                                    this.headerSectionHeight = headerHeight
+                                                    this.bottomIframeMinHeight = headerHeight
+                                                    this.availableHeight = viewportHeight - actualTitleHeight - defaultBottomHeight
+                                                    this.bottomIframeHeight = this.bottomIframeMinHeight
+                                                    this.topIframeHeight = this.availableHeight - this.bottomIframeMinHeight
+                                                    this.isExpanded = false
+                                                    this.layoutCalculated = true
+                                                    this.applyIframeHeights()
+                                                }
+                                            }
+
+                                            // 如果bottom按钮还没加载,稍后重试
+                                            if (!document.querySelector('ss-bottom')) {
+                                                setTimeout(checkBottomButton, 100)
+                                            } else {
+                                                checkBottomButton()
+                                            }
+                                        } else {
+                                            // 如果还没有加载完成,继续等待
+                                            console.log('⏳ 等待header-section加载...')
+                                            setTimeout(checkHeaderSection, 100)
+                                        }
+                                    } catch (error) {
+                                        console.error('❌ 访问iframe DOM时出错:', error)
+                                        console.log('⏳ 重试中...')
+                                        setTimeout(checkHeaderSection, 200)
+                                    }
+                                }
+
+                                checkHeaderSection()
+                            }
+
+                            waitForIframeLoad()
+
+                        } catch (error) {
+                            console.error('❌ 布局初始化失败:', error)
+                        }
+                    },
+
+                    // 应用iframe高度
+                    applyIframeHeights() {
+                        const topIframe = document.querySelector('.top-iframe')
+                        const bottomIframe = document.querySelector('.bottom-iframe')
+
+                        if (topIframe && bottomIframe) {
+                            console.log('📏 设置iframe高度前:', {
+                                topIframe: {
+                                    currentHeight: topIframe.style.height,
+                                    offsetHeight: topIframe.offsetHeight
+                                },
+                                bottomIframe: {
+                                    currentHeight: bottomIframe.style.height,
+                                    offsetHeight: bottomIframe.offsetHeight
+                                },
+                                newTopHeight: this.topIframeHeight,
+                                newBottomHeight: this.bottomIframeHeight
+                            })
+
+                            // 直接应用新高度到iframe
+                            topIframe.style.height = `${this.topIframeHeight}px`
+                            bottomIframe.style.height = `${this.bottomIframeHeight}px`
+
+                            console.log('🎯 应用iframe高度:', {
+                                top: this.topIframeHeight,
+                                bottom: this.bottomIframeHeight
+                            })
+
+                            // 强制重新计算iframe内容
+                            this.$nextTick(() => {
+                                try {
+                                    topIframe.contentWindow.dispatchEvent(new Event('resize'))
+                                } catch (e) {
+                                    // 忽略跨域错误
+                                }
+
+                                try {
+                                    bottomIframe.contentWindow.dispatchEvent(new Event('resize'))
+                                } catch (e) {
+                                    // 忽略跨域错误
+                                }
+
+                                console.log('✅ iframe高度更新完成')
+                            })
+                        }
+                    },
+
+                    // 快速版本的高度应用,用于拖拽时减少延迟
+                    applyIframeHeightsFast() {
+                        const topIframe = document.querySelector('.top-iframe')
+                        const bottomIframe = document.querySelector('.bottom-iframe')
+
+                        if (topIframe && bottomIframe) {
+                            // 直接设置高度,不进行日志和额外的处理
+                            topIframe.style.height = `${this.topIframeHeight}px`
+                            bottomIframe.style.height = `${this.bottomIframeHeight}px`
+                        }
+                    },
+
+                    // 计算iframe高度分配
+                    calculateHeights(bottomHeight) {
+                        // 计算实际可用的bottom iframe高度
+                        // 当bottom iframe展开时,可以覆盖top iframe,所以最大高度就是availableHeight
+                        // 最小高度不能小于header-section高度
+                        const bottomActualHeight = Math.max(
+                            this.bottomIframeMinHeight,
+                            Math.min(bottomHeight, this.availableHeight)
+                        )
+
+                        this.bottomIframeHeight = bottomActualHeight
+                        this.topIframeHeight = this.availableHeight - bottomActualHeight
+
+                        console.log('📐 高度分配计算:', {
+                            requestedBottom: bottomHeight,
+                            availableHeight: this.availableHeight,
+                            minBottomHeight: this.bottomIframeMinHeight,
+                            actualBottom: this.bottomIframeHeight,
+                            actualTop: this.topIframeHeight,
+                            isExpanded: this.isExpanded
+                        })
+
+                        this.applyIframeHeights()
+
+                        return {
+                            top: this.topIframeHeight,
+                            bottom: this.bottomIframeHeight
+                        }
+                    },
+
+                    // 切换展开/收起状态
+                    toggleExpand() {
+                        if (!this.layoutCalculated) {
+                            console.warn('布局尚未计算完成')
+                            return
+                        }
+
+                        this.isExpanded = !this.isExpanded
+
+                        if (this.isExpanded) {
+                            // 展开:bottom iframe占用所有可用高度,覆盖top iframe到title底部
+                            // 也就是bottom iframe高度 = availableHeight (title + button之间的所有空间)
+                            this.calculateHeights(this.availableHeight)
+                            console.log('📈 展开状态 - 覆盖到title底部,高度:', this.availableHeight)
+                        } else {
+                            // 收起:bottom iframe占用最小高度(header-section高度)
+                            this.calculateHeights(this.bottomIframeMinHeight)
+                            console.log('📉 收起状态 - 最小高度:', this.bottomIframeMinHeight)
+                        }
+                    },
+
+                    // 处理来自iframe的消息
+                    handleIframeMessage(event) {
+                        // 安全检查:只接受同源的消息
+                        if (event.origin !== window.location.origin) {
+                            return
+                        }
+
+                        const { type, data } = event.data
+
+                        switch (type) {
+                            case 'header-section-click':
+                                console.log('📱 收到header-section点击事件')
+                                this.toggleExpand()
+                                break
+                            case 'header-section-drag-start':
+                                console.log('🔄 开始拖拽header-section')
+                                this.handleDragStart(data)
+                                break
+                            case 'header-section-drag-move':
+                                console.log('🔄 拖拽中:', data.deltaY)
+                                this.handleDragMove(data.deltaY)
+                                break
+                            case 'header-section-drag-end':
+                                console.log('🔚 结束拖拽')
+                                this.handleDragEnd()
+                                break
+                        }
+                    },
+
+                    // 处理拖拽开始
+                    handleDragStart(data) {
+                        if (!this.layoutCalculated) return
+
+                        this.isDragging = true
+                        this.dragStartY = data.startY
+                        this.dragStartBottomHeight = this.bottomIframeHeight
+                    },
+
+                    // 处理拖拽移动
+                    handleDragMove(deltaY) {
+                        if (!this.isDragging || !this.layoutCalculated) return
+
+                        // 计算新的底部高度
+                        const newBottomHeight = this.dragStartBottomHeight - deltaY
+
+                        // 直接设置高度,避免复杂的计算导致的延迟
+                        const bottomActualHeight = Math.max(
+                            this.bottomIframeMinHeight,
+                            Math.min(newBottomHeight, this.availableHeight)
+                        )
+
+                        this.bottomIframeHeight = bottomActualHeight
+                        this.topIframeHeight = this.availableHeight - bottomActualHeight
+
+                        // 直接应用高度,减少延迟
+                        this.applyIframeHeightsFast()
+                    },
+
+                    // 处理拖拽结束
+                    handleDragEnd() {
+                        this.isDragging = false
+                        this.dragStartY = 0
+                        this.dragStartBottomHeight = 0
+
+                        // 拖拽结束后用完整的方法确保状态正确
+                        this.applyIframeHeights()
+                    },
+                }
+            })
+        })
+    </script>
+</body>

+ 208 - 0
page/mp_clClyd_baseInfo.html

@@ -0,0 +1,208 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>基本情况</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+
+
+
+       
+ 
+        .loading {
+            text-align: center;
+            padding: 50px;
+            color: #999;
+        }
+
+        .error {
+            text-align: center;
+            padding: 50px;
+            color: #ff4d4f;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <div v-if="loading" class="loading">加载中...</div>
+        <div v-else-if="error" class="error">{{ error }}</div>
+        <div v-else class="content-div">
+            <table class="form">
+                <tr>
+                    <th>名称</th>
+                    <td>{{ wpmc || formData.wpid || '-' }}</td>
+                </tr>
+                <tr>
+                    <th>使用人数</th>
+                    <td>{{ formData.syrs || '-' }}</td>
+                </tr>
+                <tr>
+                    <th>目的地</th>
+                    <td>{{ formData.mdd || '-' }}</td>
+                </tr>
+                <tr>
+                    <th>描述</th>
+                    <td>{{ formData.syyq || '-' }}</td>
+                </tr>
+                <tr>
+                    <th>开始时间</th>
+                    <td>{{ formData.kssj || '-' }}</td>
+                </tr>
+                <tr>
+                    <th>结束时间</th>
+                    <td>{{ formData.jssj || '-' }}</td>
+                </tr>
+            </table>
+        </div>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        pageParams: {},
+                        loading: false,
+                        error: '',
+                        formData: {},
+                        wpmc: '' // 物品名称(通过wpid转换得到)
+                    }
+                },
+                mounted() {
+                    // 获取URL参数
+                    this.pageParams = this.getUrlParams()
+                    console.log('📄 mp_clClyd_baseInfo 页面参数:', this.pageParams)
+
+                    // 加载数据
+                    this.loadData()
+                },
+                methods: {
+                    // 获取URL参数
+                    getUrlParams() {
+                        const params = {}
+                        const urlSearchParams = new URLSearchParams(window.location.search)
+                        for (const [key, value] of urlSearchParams) {
+                            params[key] = decodeURIComponent(value)
+                        }
+                        return params
+                    },
+
+                    // 加载数据
+                    async loadData() {
+                        const service = this.pageParams.service
+                        const paramStr = this.pageParams.param
+
+                        if (!service) {
+                            this.error = '缺少 service 参数'
+                            console.error('❌ 缺少 service 参数')
+                            return
+                        }
+
+                        this.loading = true
+                        this.error = ''
+
+                        try {
+                            console.log('📋 调用 service:', service)
+                            console.log('📋 原始 param:', paramStr)
+
+                            // 解析 param 参数
+                            let paramObj = {}
+                            if (paramStr) {
+                                try {
+                                    // 尝试直接解析
+                                    paramObj = JSON.parse(paramStr)
+                                    console.log('✅ 解析后的 param:', paramObj)
+                                } catch (e) {
+                                    console.warn('⚠️ param 格式有误,尝试修复:', paramStr)
+                                    try {
+                                        // 修复常见的 JSON 格式问题:bdlbm:1 -> "bdlbm":1
+                                        const fixedParamStr = paramStr.replace(/([,{]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":')
+                                        paramObj = JSON.parse(fixedParamStr)
+                                        console.log('✅ 修复后解析成功:', paramObj)
+                                    } catch (e2) {
+                                        console.error('❌ param 解析失败:', e2, '原始字符串:', paramStr)
+                                        paramObj = {}
+                                    }
+                                }
+                            }
+
+                            // 调用 service 接口
+                            const response = await request.post(
+                                `/service?ssServ=${service}&ssDest=data`,
+                                paramObj,
+                                { loading: false, formData: true }
+                            )
+
+                            console.log('✅ service 响应:', response)
+
+                            // 处理响应数据
+                            if (response && response.data && response.data.clClyd) {
+                                const data = response.data.clClyd
+                                console.log('📊 clClyd 数据:', data)
+
+                                // 直接使用响应数据
+                                this.formData = data
+
+                                // 格式化时间字段
+                                if (data.kssj && window.H5FieldFormatter) {
+                                    this.formData.kssj = window.H5FieldFormatter.formatDate(data.kssj, 'yyyy-MM-dd HH:mm:ss')
+                                }
+                                if (data.jssj && window.H5FieldFormatter) {
+                                    this.formData.jssj = window.H5FieldFormatter.formatDate(data.jssj, 'yyyy-MM-dd HH:mm:ss')
+                                }
+
+                                // 转换 wpid 为物品名称
+                                if (data.wpid && window.getDictTranslation) {
+                                    this.translateWpid(data.wpid)
+                                }
+
+                                console.log('✅ 表单数据已更新:', this.formData)
+                            } else {
+                                this.error = '未获取到数据'
+                                console.warn('⚠️ 响应数据中没有 clClyd 字段')
+                            }
+
+                        } catch (error) {
+                            console.error('❌ 加载数据失败:', error)
+                            this.error = '加载数据失败:' + (error.message || '未知错误')
+                        } finally {
+                            this.loading = false
+                        }
+                    },
+
+                    // 转换 wpid 为物品名称
+                    async translateWpid(wpid) {
+                        try {
+                            console.log('🔄 转换 wpid:', wpid)
+
+                            const dictCache = new Map()
+                            const cacheKey = `wp_${wpid}`
+
+                            // 调用字典转换
+                            await window.getDictTranslation(
+                                'wp',           // cbName
+                                wpid,           // value
+                                cacheKey,       // cacheKey
+                                dictCache,      // dictCache
+                                () => {         // updateCallback
+                                    // 更新物品名称
+                                    this.wpmc = dictCache.get(cacheKey) || wpid
+                                    console.log('✅ 物品名称已更新:', this.wpmc)
+                                }
+                            )
+                        } catch (error) {
+                            console.error('❌ wpid 转换失败:', error)
+                            this.wpmc = wpid // 转换失败显示原值
+                        }
+                    }
+                }
+            })
+        })
+    </script>
+</body>
+</html>

+ 236 - 0
page/mp_clyy_details.html

@@ -0,0 +1,236 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>预约详情</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+            display: flex;
+            flex-direction: column;
+        }
+
+        /* 当前车辆信息区域 */
+        .current-car-section {
+            padding: 10px;
+            padding-bottom: 0;
+        }
+
+        /* 预约详情 */
+        .reservation-detail {
+            margin: 10px 0;
+        }
+
+        /* 电话链接样式 */
+        .phone-link {
+            color: #007AFF;
+            text-decoration: underline;
+            cursor: pointer;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <!-- 当前车辆信息卡片 -->
+        <div class="current-car-section">
+            <ss-car-card
+                v-if="carData"
+                :car-data="carData"
+                :status="carData.status"
+            />
+        </div>
+
+        <!-- 预约详情 -->
+        <div v-if="reservationDetail" class="reservation-detail">
+            <table>
+                <tr>
+                    <th>开始时间</th>
+                    <td>{{ formatDate(reservationDetail.kssj, 'yyyy-MM-dd HH:mm:ss') }}</td>
+                </tr>
+                <tr>
+                    <th>结束时间</th>
+                    <td>{{ formatDate(reservationDetail.jssj, 'yyyy-MM-dd HH:mm:ss') }}</td>
+                </tr>
+                <tr>
+                    <th>使用人</th>
+                    <td>{{ reservationDetail.ydr }}</td>
+                </tr>
+                <tr>
+                    <th>联系电话</th>
+                    <td>
+                        <span class="phone-link" @click="makePhoneCall(reservationDetail.ydrdh)">
+                            {{ reservationDetail.ydrdh }}
+                        </span>
+                    </td>
+                </tr>
+            </table>
+        </div>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        // 车辆信息
+                        carData: null,
+
+                        // 预约详情
+                        reservationDetail: null,
+
+                        // URL参数
+                        urlParams: {
+                            wpid: '',
+                            yykssj: '',
+                            yyjssj: ''
+                        }
+                    }
+                },
+
+                async mounted() {
+                    // 解析URL参数
+                    this.parseUrlParams()
+
+                    // 加载预约详情
+                    await this.loadReservationDetail()
+                },
+
+                methods: {
+                    // 解析URL参数
+                    parseUrlParams() {
+                        const urlParams = new URLSearchParams(window.location.search)
+
+                        this.urlParams.wpid = urlParams.get('wpid') || ''
+                        this.urlParams.yykssj = urlParams.get('yykssj') || ''
+                        this.urlParams.yyjssj = urlParams.get('yyjssj') || ''
+
+                        // 从 sessionStorage 获取完整的车辆数据(包含 wpcsList 等数组字段)
+                        const carDataStr = sessionStorage.getItem('carDetailData')
+                        if (carDataStr) {
+                            try {
+                                this.carData = JSON.parse(carDataStr)
+                                console.log('从 sessionStorage 获取车辆数据:', this.carData)
+
+                                // 使用后清除,避免占用空间
+                                sessionStorage.removeItem('carDetailData')
+                            } catch (error) {
+                                console.error('解析车辆数据失败:', error)
+                            }
+                        }
+
+                        console.log('解析到的URL参数:', this.urlParams)
+                        console.log('解析到的车辆数据:', this.carData)
+                    },
+
+                    // 加载预约详情
+                    async loadReservationDetail() {
+                        if (!this.urlParams.wpid || !this.urlParams.yykssj || !this.urlParams.yyjssj) {
+                            console.error('缺少必要参数')
+                            window.showToast?.('参数错误', 'error')
+                            return
+                        }
+
+                        try {
+                            // 查询预约详情
+                            const res = await window.request.post(
+                                `/service?ssServ=cl_searchYd`,
+                                {
+                                    wpid: this.urlParams.wpid,
+                                    kssj: this.urlParams.yykssj,
+                                    jssj: this.urlParams.yyjssj
+                                },
+                                { loading: true, formData: true }
+                            )
+
+                            console.log('查询预约响应:', res)
+
+                            if (res.data && res.data.wpydlist && res.data.wpydlist.length > 0) {
+                                this.reservationDetail = res.data.wpydlist[0]
+                            } else {
+                                window.showToast?.('未找到预约信息', 'error')
+                            }
+                        } catch (error) {
+                            console.error('加载预约详情失败:', error)
+                            window.showToast?.('加载失败', 'error')
+                        }
+                    },
+
+                    // 拨打电话
+                    makePhoneCall(phoneNumber) {
+                        window.location.href = `tel:${phoneNumber}`
+                    },
+
+                    // 格式化日期 - 使用 dayjs
+                    formatDate(dateStr, format) {
+                        if (!dateStr) return ''
+
+                        console.log('formatDate 输入:', dateStr, '格式要求:', format)
+
+                        // 检查 dayjs 是否可用
+                        if (typeof dayjs === 'undefined') {
+                            console.error('dayjs 未加载')
+                            return dateStr
+                        }
+
+                        // 清理字符串:移除特殊空格字符(如 \u202F),替换为普通空格
+                        const cleanedDateStr = dateStr
+                            .replace(/[\u202F\u00A0]/g, ' ')  // 替换不间断空格
+                            .replace(/\s+/g, ' ')              // 多个空格合并为一个
+                            .trim()
+
+                        console.log('清理后的字符串:', cleanedDateStr)
+
+                        // 尝试使用原生 Date 解析(兼容性最好)
+                        let date = null
+                        try {
+                            const jsDate = new Date(cleanedDateStr)
+                            if (!isNaN(jsDate.getTime())) {
+                                date = dayjs(jsDate)
+                            }
+                        } catch (e) {
+                            console.warn('Date 解析失败:', e)
+                        }
+
+                        // 如果 Date 解析失败,尝试直接用 dayjs
+                        if (!date || !date.isValid()) {
+                            date = dayjs(cleanedDateStr)
+                        }
+
+                        console.log('dayjs 解析结果:', date, 'isValid:', date.isValid())
+
+                        if (!date.isValid()) {
+                            console.warn('无效的日期格式:', dateStr)
+                            return dateStr // 如果无法解析,返回原始字符串
+                        }
+
+                        // dayjs 的格式化映射
+                        // yyyy -> YYYY, MM -> MM, dd -> DD, HH -> HH, mm -> mm, ss -> ss
+                        const dayjsFormat = format
+                            .replace('yyyy', 'YYYY')
+                            .replace('dd', 'DD')
+
+                        console.log('dayjs 格式:', dayjsFormat)
+
+                        const result = date.format(dayjsFormat)
+                        console.log('格式化结果:', result)
+
+                        return result
+                    }
+                }
+            })
+        })
+    </script>
+</body>
+</html>

+ 340 - 0
page/mp_clyy_form.html

@@ -0,0 +1,340 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>车辆预约</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+            display: flex;
+            flex-direction: column;
+            padding-bottom: 80px;
+        }
+
+        /* 当前车辆信息区域 */
+        .current-car-section {
+            padding: 10px;
+            padding-bottom: 0;
+        }
+
+        /* 预约表单 */
+        .reservation-form {
+            margin: 10px 0;
+        }
+
+        /* 确认弹窗内容 */
+        .confirm-items {
+            padding: 20px;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+        }
+
+        .confirm-item {
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            font-size: 17px;
+            color: #333;
+        }
+
+        .confirm-item .label {
+            text-align: right;
+            flex: 1;
+            margin-right: 10px;
+        }
+
+        .confirm-item .value {
+            flex: 1;
+            font-weight: 500;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <!-- 当前车辆信息卡片 -->
+        <div class="current-car-section">
+            <ss-car-card
+                v-if="carData"
+                :car-data="carData"
+                :status="carData.status"
+            />
+        </div>
+
+        <!-- 预约表单 -->
+        <div class="reservation-form">
+            <table>
+                <tr>
+                    <th>开始时间</th>
+                    <td>{{ formData.yykssj }}</td>
+                </tr>
+                <tr>
+                    <th>结束时间</th>
+                    <td>{{ formData.yyjssj }}</td>
+                </tr>
+                <tr>
+                    <th>人数</th>
+                    <td>
+                        <ss-input v-model="formData.rs" name="rs" placeholder="请输入人数" />
+                    </td>
+                </tr>
+                <tr>
+                    <th>办事地点</th>
+                    <td>
+                        <ss-input v-model="formData.bsdd" name="bsdd" placeholder="请输入办事地点" />
+                    </td>
+                </tr>
+                <tr>
+                    <th>事由</th>
+                    <td>
+                        <ss-input v-model="formData.sy" name="sy" placeholder="请输入事由" />
+                    </td>
+                </tr>
+            </table>
+        </div>
+
+        <!-- 底部按钮 -->
+        <ss-bottom
+            :buttons="bottomButtons"
+            @button-click="handleBottomAction"
+        ></ss-bottom>
+
+        <!-- 确认弹窗 -->
+        <ss-confirm
+            v-model="showConfirm"
+            title="预约确认"
+            @confirm="handleConfirm"
+        >
+            <div class="confirm-items">
+                <div class="confirm-item">
+                    <span class="label">预约开始时间:</span>
+                    <span class="value">{{ formData.yykssj }}</span>
+                </div>
+                <div class="confirm-item">
+                    <span class="label">预约结束时间:</span>
+                    <span class="value">{{ formData.yyjssj }}</span>
+                </div>
+                <div class="confirm-item">
+                    <span class="label">人数:</span>
+                    <span class="value">{{ formData.rs }}</span>
+                </div>
+                <div class="confirm-item">
+                    <span class="label">办事地点:</span>
+                    <span class="value">{{ formData.bsdd }}</span>
+                </div>
+                <div class="confirm-item">
+                    <span class="label">事由:</span>
+                    <span class="value">{{ formData.sy }}</span>
+                </div>
+            </div>
+        </ss-confirm>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        // 车辆信息
+                        carData: null,
+
+                        // 表单数据
+                        formData: {
+                            yykssj: '',  // 预约开始时间
+                            yyjssj: '',  // 预约结束时间
+                            rs: '',      // 人数
+                            bsdd: '',    // 办事地点
+                            sy: ''       // 事由
+                        },
+
+                        // 底部按钮配置
+                        bottomButtons: [
+                            // { text: '取消', action: 'back' },
+                            { text: '预约', action: 'save' }
+                        ],
+
+                        // 确认弹窗
+                        showConfirm: false
+                    }
+                },
+
+                async mounted() {
+                    // 解析URL参数
+                    this.parseUrlParams()
+
+                    // 初始化校验规则
+                    this.$nextTick(() => {
+                        this.initValidationRules()
+                    })
+                },
+
+                methods: {
+                    // 解析URL参数
+                    parseUrlParams() {
+                        const urlParams = new URLSearchParams(window.location.search)
+
+                        // 解析时间参数
+                        this.formData.yykssj = urlParams.get('yykssj') || ''
+                        this.formData.yyjssj = urlParams.get('yyjssj') || ''
+
+                        // 从 sessionStorage 获取完整的车辆数据(包含 wpcsList 等数组字段)
+                        const carDataStr = sessionStorage.getItem('carFormData')
+                        if (carDataStr) {
+                            try {
+                                this.carData = JSON.parse(carDataStr)
+                                console.log('从 sessionStorage 获取车辆数据:', this.carData)
+
+                                // 使用后清除,避免占用空间
+                                sessionStorage.removeItem('carFormData')
+                            } catch (error) {
+                                console.error('解析车辆数据失败:', error)
+                            }
+                        }
+
+                        console.log('解析到的表单数据:', this.formData)
+                        console.log('解析到的车辆数据:', this.carData)
+                    },
+
+                    // 初始化ssVm校验规则
+                    initValidationRules() {
+                        console.log('初始化ssVm校验规则...')
+
+                        // 预约表单校验
+                        ssVm.add('notNull', ['rs'], {
+                            msgPrfx: '人数',
+                            required: true
+                        })
+
+                        ssVm.add('notNull', ['bsdd'], {
+                            msgPrfx: '办事地点',
+                            required: true
+                        })
+
+                        ssVm.add('notNull', ['sy'], {
+                            msgPrfx: '事由',
+                            required: true
+                        })
+
+                        console.log('ssVm校验规则初始化完成')
+                    },
+
+                    // 底部按钮事件处理
+                    handleBottomAction(data) {
+                        console.log('底部按钮点击:', data)
+                        switch (data.action) {
+                            case 'back':
+                                // 返回上一页
+                                window.history.back()
+                                break
+                            case 'save':
+                                // 使用ssVm校验预约表单
+                                if (!this.validateForm()) {
+                                    console.log('校验失败,中止操作')
+                                    return
+                                }
+                                // 显示二次确认弹窗
+                                this.$nextTick(() => {
+                                    this.showConfirm = true
+                                })
+                                break
+                        }
+                    },
+
+                    // 校验表单 - 使用ssVm
+                    validateForm() {
+                        // 使用ssVm的validateAll方法
+                        const result = ssVm.validateAll()
+
+                        if (!result.valid) {
+                            console.log('表单校验失败:', result.errors)
+                            // ssVm会自动显示错误信息
+                            return false
+                        }
+
+                        return true
+                    },
+
+                    // 确认弹窗回调
+                    async handleConfirm() {
+                        await this.submitReservation()
+                    },
+
+                    // 提交预约
+                    async submitReservation() {
+                        try {
+                            // 构建提交参数
+                            const reservationParam = {
+                                cdids: this.carData.id,
+                                kssj: this.formData.yykssj,
+                                jssj: this.formData.yyjssj,
+                                mdd: this.formData.bsdd,
+                                syyq: this.formData.sy,
+                                syrs: this.formData.rs,
+                                sfyj: 0,
+                                yjsjcd: '',
+                                yjms: ''
+                            }
+
+                            console.log('提交预约参数:', reservationParam)
+
+                            // 调用PC端的cl_sureYd接口
+                            const response = await window.request.post(
+                                `/service?ssServ=cl_sureYd`,
+                                reservationParam,
+                                { loading: true, formData: true }
+                            )
+
+                            console.log('预约响应:', response)
+
+                            if (response && response.data.msg == '提交车辆预订成功') {
+                                showToastEffect('预约成功', 2000, 'success')
+
+                                // 返回小程序
+                                setTimeout(() => {
+                                    // 直接调用微信小程序 API 返回
+                                    if (typeof wx !== 'undefined' && wx.miniProgram) {
+                                        console.log('调用 wx.miniProgram.navigateBack 返回小程序')
+                                        wx.miniProgram.navigateBack({
+                                            delta: 1
+                                        })
+                                    } else if (typeof goBack === 'function') {
+                                        NavigationManager.goBack({ refreshParent: true })
+                                    } else {
+                                        console.warn('无法返回小程序,不在小程序环境中')
+                                    }
+                                }, 1500)
+                            } else {
+                                const errorMsg = response.data?.msg || response.message || '预约失败'
+                                if (typeof showToastEffect === 'function') {
+                                    showToastEffect(errorMsg, 2000, 'error')
+                                } else {
+                                    console.error('预约失败:', errorMsg)
+                                }
+                            }
+                        } catch (error) {
+                            console.error('预约失败:', error)
+                            if (typeof showToastEffect === 'function') {
+                                showToastEffect('预约失败,请重试', 2000, 'error')
+                            } else {
+                                console.error('预约失败,请重试')
+                            }
+                        }
+                    }
+                }
+            })
+        })
+    </script>
+</body>
+</html>

+ 467 - 0
page/mp_clyy_inp.html

@@ -0,0 +1,467 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>车辆预约</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+            display: flex;
+            flex-direction: column;
+        }
+
+        th{
+            max-width: 35% !important;
+        }
+
+        /* 车辆列表区域 */
+        .car-list {
+            margin-top: 17px;
+            padding: 0 10px;
+            display: flex;
+            flex-direction: column;
+        }
+
+        /* 加载中提示 */
+        .loading-more {
+            padding: 15px;
+            text-align: center;
+            color: #666;
+            font-size: 14px;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <!-- 选择时间和车辆 -->
+        <table style="padding-top: 8px;">
+            <tr>
+                <th width="200">预约开始时间</th>
+                <td>
+                    <ss-datetime-picker
+                        v-model="formData.yykssj"
+                        name="yykssj"
+                        mode="datetime"
+                        :min-date="yykssjMinDate"
+                        placeholder="YYYY-MM-DD HH:mm"
+                        @change="onYykssjChange"
+                    />
+                </td>
+            </tr>
+            <tr>
+                <th width="200">预约结束时间</th>
+                <td>
+                    <ss-datetime-picker
+                        v-model="formData.yyjssj"
+                        name="yyjssj"
+                        mode="datetime"
+                        :min-date="yyjssjMinDate"
+                        placeholder="YYYY-MM-DD HH:mm"
+                        @change="onYyjssjChange"
+                    />
+                </td>
+            </tr>
+        </table>
+
+        <!-- 车辆选择列表 -->
+        <div class="car-list">
+            <ss-car-card
+                v-for="car in carList"
+                :key="car.id"
+                :car-data="car"
+                :status="car.status"
+                @select="handleCarSelect"
+            />
+
+            <!-- 加载中提示 -->
+            <div v-if="loadingMore" class="loading-more">
+                加载中...
+            </div>
+        </div>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        // 表单数据
+                        formData: {
+                            yykssj: '',  // 预约开始时间
+                            yyjssj: ''   // 预约结束时间
+                        },
+
+                        // 时间选择器最小值
+                        yykssjMinDate: new Date().toISOString().slice(0, 16),
+                        yyjssjMinDate: new Date(new Date().getTime() + 3600 * 1000).toISOString().slice(0, 16),
+
+                        // 车辆列表数据
+                        carList: [],
+
+                        // 加载状态
+                        loading: false,
+                        loadingMore: false,
+                        hasMoreData: false,
+
+                        // 分页信息
+                        pageInfo: {
+                            pageNo: 1,
+                            rowNumPer: 10,
+                            total: 0
+                        }
+                    }
+                },
+
+                async mounted() {
+                    // 初始化页面数据
+                    this.initPageData()
+                },
+
+                methods: {
+
+                    // 初始化页面数据
+                    initPageData() {
+                        // 设置默认查询时间(一天后到两天后)
+                        const tomorrow = new Date()
+                        tomorrow.setDate(tomorrow.getDate() + 1)
+                        tomorrow.setHours(9, 0, 0, 0)
+
+                        const dayAfterTomorrow = new Date()
+                        dayAfterTomorrow.setDate(dayAfterTomorrow.getDate() + 2)
+                        dayAfterTomorrow.setHours(17, 0, 0, 0)
+
+                        // 格式化为 yyyy-MM-dd HH:mm 格式
+                        const formatDateTime = (date) => {
+                            const year = date.getFullYear()
+                            const month = String(date.getMonth() + 1).padStart(2, '0')
+                            const day = String(date.getDate()).padStart(2, '0')
+                            const hours = String(date.getHours()).padStart(2, '0')
+                            const minutes = String(date.getMinutes()).padStart(2, '0')
+                            return `${year}-${month}-${day} ${hours}:${minutes}`
+                        }
+
+                        this.formData.yykssj = formatDateTime(tomorrow)
+                        this.formData.yyjssj = formatDateTime(dayAfterTomorrow)
+
+                        console.log('初始化查询车辆:', {
+                            yykssj: this.formData.yykssj,
+                            yyjssj: this.formData.yyjssj
+                        })
+
+                        // 自动查询车辆
+                        this.queryAvailableCars()
+                    },
+
+                    // 开始时间变化
+                    onYykssjChange(val) {
+                        console.log('📅 开始时间变化:', val)
+
+                        if (val) {
+                            // 更新结束时间的最小值
+                            this.yyjssjMinDate = new Date(new Date(val).getTime() + 3600 * 1000).toISOString().slice(0, 16)
+
+                            // 自动设置结束时间为开始时间 +1 天
+                            const startDate = new Date(val)
+                            const endDate = new Date(startDate.getTime() + 24 * 3600 * 1000) // +1天(24小时)
+
+                            // 格式化为 yyyy-MM-dd HH:mm 格式
+                            const year = endDate.getFullYear()
+                            const month = String(endDate.getMonth() + 1).padStart(2, '0')
+                            const day = String(endDate.getDate()).padStart(2, '0')
+                            const hours = String(endDate.getHours()).padStart(2, '0')
+                            const minutes = String(endDate.getMinutes()).padStart(2, '0')
+
+                            this.formData.yyjssj = `${year}-${month}-${day} ${hours}:${minutes}`
+                            console.log('⏰ 自动设置结束时间为:', this.formData.yyjssj)
+
+                            // 清空车辆列表,等待用户确认时间后再查询
+                            this.carList = []
+                            this.onYyjssjChange(this.formData.yyjssj)
+                        } else {
+                            // 如果开始时间被清空,也清空结束时间和车辆列表
+                            this.formData.yyjssj = ''
+                            this.carList = []
+                            console.log('🧹 已清空结束时间和车辆列表')
+                        }
+
+                        // 手动触发校验
+                        this.$nextTick(() => {
+                            if (window.ssVm) {
+                                window.ssVm.validateField('yyjssj')
+                            }
+                        })
+                    },
+
+                    // 结束时间变化
+                    onYyjssjChange(val) {
+                        console.log('📅 结束时间变化:', val)
+
+                        // 只有同时存在开始时间和结束时间才查询车辆
+                        if (val && this.formData.yykssj) {
+                            console.log('🚗 开始查询车辆...')
+                            this.queryAvailableCars()
+                        } else {
+                            console.log('⏸️ 时间不完整,暂不查询车辆')
+                        }
+                    },
+
+                    // 查询可用车辆
+                    async queryAvailableCars(pageNo = 1) {
+                        const queryStartTime = this.formData.yykssj
+                        const queryEndTime = this.formData.yyjssj
+
+                        if (!queryStartTime || !queryEndTime) {
+                            return
+                        }
+
+                        this.loading = true
+                       
+
+                        try {
+                            // 调用PC端的cl_search接口(分页)
+                            const response = await window.request.post(
+                                `/service?ssServ=cl_searchRcpt`,
+                                {
+                                    beginTime: queryStartTime,
+                                    endTime: queryEndTime,
+                                    pageNo: pageNo,
+                                    rowNumPer: this.pageInfo.rowNumPer
+                                },
+                                {
+                                    loading: true,
+                                    formData: true
+                                }
+                            )
+
+                            console.log('response.data内容:', response)
+
+                            if (response && response.data) {
+                                // 更新分页信息
+                                if (response.wdPage) {
+                                    this.pageInfo = {
+                                        pageNo: response.wdPage.pageNo,
+                                        rowNumPer: response.wdPage.rowNumPer,
+                                        total: response.wdPage.rowNum
+                                    }
+                                }
+
+                                // 处理返回的车辆数据
+                                const processedCars = await Promise.all(response.data.data.map(async (item) => {
+                                    console.log('📦 原始车辆数据:', item)
+
+                                    // 简单的状态判断
+                                    let status = 'available'
+
+                                    if (item.zt == 1) {
+                                        status = 'reserved'
+                                    } else {
+                                        status = 'available'
+                                    }
+
+                                    // 获取车辆类型
+                                    let carType = '车辆'
+                                    if (item.wplbm) {
+                                        try {
+                                            const typeOptions = await window.getDictOptions('wplb')
+                                            const typeOption = typeOptions.find(opt => opt.v == item.wplbm)
+                                            if (typeOption) {
+                                                carType = typeOption.n
+                                            }
+                                        } catch (error) {
+                                            console.warn('获取车辆类型失败:', error)
+                                        }
+                                    }
+
+                                    const processedData = {
+                                        id: item.wpid || item.id,
+                                        plateNumber: item.mc,
+                                        seats: 7,
+                                        name: item.mc,
+                                        color: '白色',
+                                        type: carType,
+                                        image: item.sltwj,
+                                        wph: item.wph || '',
+                                        wpcsList: item.wpcsList || [],
+                                        wplbm: item.wplbm,
+                                        wpid: item.wpid,
+                                        status: status
+                                    }
+
+                                    console.log('✅ 处理后车辆数据:', processedData)
+                                    return processedData
+                                }))
+
+                                // 如果是第一页,直接替换;否则追加
+                                if (pageNo === 1) {
+                                    this.carList = processedCars
+                                    this.hasMoreData = this.pageInfo.total > this.pageInfo.rowNumPer
+                                } else {
+                                    this.carList = [...this.carList, ...processedCars]
+                                    this.hasMoreData = this.pageInfo.pageNo * this.pageInfo.rowNumPer < this.pageInfo.total
+                                }
+                            } else {
+                                if (pageNo === 1) {
+                                    this.carList = []
+                                }
+                            }
+                        } catch (error) {
+                            console.error('查询车辆失败:', error)
+                            if (pageNo === 1) {
+                                this.carList = []
+                            }
+                        } finally {
+                            this.loading = false
+                        }
+                    },
+
+                    // 处理车辆选择
+                    async handleCarSelect(car) {
+                        console.log('选择车辆:', car)
+
+                        // 检查是否已选择时间
+                        if (!this.formData.yykssj || !this.formData.yyjssj) {
+                            window.showToast?.('请先选择预约时间', 'error')
+                            return
+                        }
+
+                        if (car.status === 'reserved') {
+                            // 点击已预约车辆,跳转到详情页
+                            this.navigateToDetail(car)
+                        } else if (car.status === 'available') {
+                            // 点击可预约车辆,跳转到预约表单页
+                            this.navigateToForm(car)
+                        }
+                    },
+
+                    // 跳转到预约详情页
+                    navigateToDetail(car) {
+                        console.log('🔗 跳转到预约详情页', car)
+
+                        // 将完整的车辆数据存储到 sessionStorage(避免URL传递复杂对象)
+                        sessionStorage.setItem('carDetailData', JSON.stringify(car))
+
+                        // 获取当前 URL 的查询参数
+                        const currentParams = new URLSearchParams(window.location.search)
+
+                        // 构建跳转参数
+                        const params = new URLSearchParams({
+                            wpid: car.wpid || '',
+                            yykssj: this.formData.yykssj || '',
+                            yyjssj: this.formData.yyjssj || '',
+                            // 保留必要的认证参数
+                            JSESSIONID: currentParams.get('JSESSIONID') || '',
+                            devId: currentParams.get('devId') || '',
+                            sbmc: currentParams.get('sbmc') || ''
+                        })
+
+                        // 跳转到详情页
+                        const targetUrl = `./mp_clyy_details.html?${params.toString()}`
+                        console.log('🎯 目标 URL:', targetUrl)
+                        window.location.href = targetUrl
+                    },
+
+                    // 跳转到预约表单页
+                    navigateToForm(car) {
+                        console.log('🔗 跳转到预约表单页', car)
+
+                        // 将完整的车辆数据存储到 sessionStorage(避免URL传递复杂对象)
+                        sessionStorage.setItem('carFormData', JSON.stringify(car))
+
+                        // 获取当前 URL 的查询参数
+                        const currentParams = new URLSearchParams(window.location.search)
+
+                        // 构建跳转参数
+                        const params = new URLSearchParams({
+                            wpid: car.wpid || '',
+                            yykssj: this.formData.yykssj || '',
+                            yyjssj: this.formData.yyjssj || '',
+                            // 保留必要的认证参数
+                            JSESSIONID: currentParams.get('JSESSIONID') || '',
+                            devId: currentParams.get('devId') || '',
+                            sbmc: currentParams.get('sbmc') || ''
+                        })
+
+                        // 跳转到表单页
+                        const targetUrl = `./mp_clyy_form.html?${params.toString()}`
+                        console.log('🎯 目标 URL:', targetUrl)
+                        window.location.href = targetUrl
+                    },
+
+                    // 拨打电话
+                    makePhoneCall(phoneNumber) {
+                        window.location.href = `tel:${phoneNumber}`
+                    },
+
+                    // 格式化日期 - 使用 dayjs
+                    formatDate(dateStr, format) {
+                        if (!dateStr) return ''
+
+                        console.log('📅 formatDate 输入:', dateStr, '格式要求:', format)
+
+                        // 检查 dayjs 是否可用
+                        if (typeof dayjs === 'undefined') {
+                            console.error('❌ dayjs 未加载')
+                            return dateStr
+                        }
+
+                        // 清理字符串:移除特殊空格字符(如 \u202F),替换为普通空格
+                        const cleanedDateStr = dateStr
+                            .replace(/[\u202F\u00A0]/g, ' ')  // 替换不间断空格
+                            .replace(/\s+/g, ' ')              // 多个空格合并为一个
+                            .trim()
+
+                        console.log('📅 清理后的字符串:', cleanedDateStr)
+
+                        // 尝试使用原生 Date 解析(兼容性最好)
+                        let date = null
+                        try {
+                            const jsDate = new Date(cleanedDateStr)
+                            if (!isNaN(jsDate.getTime())) {
+                                date = dayjs(jsDate)
+                            }
+                        } catch (e) {
+                            console.warn('⚠️ Date 解析失败:', e)
+                        }
+
+                        // 如果 Date 解析失败,尝试直接用 dayjs
+                        if (!date || !date.isValid()) {
+                            date = dayjs(cleanedDateStr)
+                        }
+
+                        console.log('📅 dayjs 解析结果:', date, 'isValid:', date.isValid())
+
+                        if (!date.isValid()) {
+                            console.warn('⚠️ 无效的日期格式:', dateStr)
+                            return dateStr // 如果无法解析,返回原始字符串
+                        }
+
+                        // dayjs 的格式化映射
+                        // yyyy -> YYYY, MM -> MM, dd -> DD, HH -> HH, mm -> mm, ss -> ss
+                        const dayjsFormat = format
+                            .replace('yyyy', 'YYYY')
+                            .replace('dd', 'DD')
+
+                        console.log('📅 dayjs 格式:', dayjsFormat)
+
+                        const result = date.format(dayjsFormat)
+                        console.log('📅 格式化结果:', result)
+
+                        return result
+                    }
+                }
+            })
+        })
+    </script>
+</body>

+ 112 - 0
page/mp_objInfo.html

@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title></title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+            display: flex;
+            flex-direction: column;
+        }
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <!-- 使用 ss-sub-tab 组件 -->
+        <ss-sub-tab
+            v-if="tabList.length > 0"
+            :tab-list="tabList"
+            :base-params="baseParams"
+            @tab-change="handleTabChange"
+        />
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        pageParams: {},
+                        loading: false,
+                        tabList: [],           // tabTag接口返回的Tab列表
+                        baseParams: {}         // 基础参数(传递给子页面)
+                    }
+                },
+                mounted() {
+                    // 获取URL参数
+                    this.pageParams = this.getUrlParams()
+                    console.log('🔗 mp_objInfo页面接收到参数:', this.pageParams)
+
+                    // 设置基础参数(会传递给所有子页面)
+                    this.baseParams = {
+                        ssObjName: this.pageParams.ssObjName || '',
+                        ssObjId: this.pageParams.ssObjId || '',
+                        sqid: this.pageParams.sqid || '',
+                        shid: this.pageParams.shid || '',
+                        bdlbm: this.pageParams.bdlbm || '',
+                        dataType: this.pageParams.dataType || '',
+                        encode_shid: this.pageParams.encode_shid || ''
+                    }
+
+                    // 只调用tabTag接口获取Tab列表
+                    if (this.pageParams.ssObjName && this.pageParams.ssObjId) {
+                        this.loadTag()
+                    }
+                },
+                methods: {
+                    // 获取URL参数
+                    getUrlParams() {
+                        const params = {}
+                        const urlSearchParams = new URLSearchParams(window.location.search)
+                        for (const [key, value] of urlSearchParams) {
+                            params[key] = decodeURIComponent(value)
+                        }
+                        return params
+                    },
+
+                    // 加载Tab列表
+                    async loadTag() {
+                        this.loading = true
+
+                        try {
+                            console.log('📋 调用tabTag服务获取Tab列表...')
+
+                            const { ssObjName, ssObjId, sqid, shid, bdlbm, dataType, encode_shid } = this.baseParams
+
+                            const tagResponse = await request.post(
+                                `/service?ssServ=tabTag&ssDest=data&name=baseInfo,child&ssObjName=${ssObjName}&ssObjId=${ssObjId}&sqid=${sqid}&shid=${shid}&bdlbm=${bdlbm}&dataType=${dataType}&encode_shid=${encode_shid}`,
+                                { loading: false, formData: true }
+                            )
+
+                            // 保存Tab列表
+                            if (tagResponse && tagResponse.data && tagResponse.data.tabList) {
+                                this.tabList = tagResponse.data.tabList
+                                console.log('✅ 获取到Tab列表:', this.tabList)
+                            } else {
+                                console.warn('⚠️ tabTag响应中没有tabList字段')
+                            }
+
+                        } catch (error) {
+                            console.error('❌ 加载Tab数据失败:', error)
+                        } finally {
+                            this.loading = false
+                        }
+                    },
+
+                    // Tab切换事件处理
+                    handleTabChange({ index, tab }) {
+                        console.log('📑 切换到Tab:', index, tab.desc || tab.title)
+                    }
+                }
+            })
+        })
+    </script>
+</body>

+ 499 - 0
page/mp_objInp.html

@@ -0,0 +1,499 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>公共录入</title>
+    <script src="/js/mp_base/base.js"></script>
+    <style>
+        body {
+            margin: 0;
+            background: #f5f5f5;
+            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+        }
+
+        #app {
+            min-height: 100vh;
+            background: #f5f5f5;
+            overflow: hidden;
+        }
+
+        .content {
+            width: 100%;
+            height: 100vh;
+            min-height: 0;
+        }
+
+        .form-frame {
+            width: 100%;
+            height: 100%;
+            border: 0;
+            display: block;
+            background: #fff;
+        }
+
+        .placeholder {
+            padding: 28px 16px;
+            color: #666;
+            font-size: 14px;
+            text-align: center;
+        }
+    </style>
+</head>
+<body>
+<div id="app">
+    <div class="content" id="contentWrap">
+        <iframe
+            v-if="iframeSrc"
+            id="objInpFrame"
+            class="form-frame"
+            :src="iframeSrc"
+            @load="handleFrameLoad"
+        ></iframe>
+        <div v-else class="placeholder" id="placeholder">{{ placeholderText }}</div>
+    </div>
+
+    <ss-bottom
+        v-if="showBottom"
+        :buttons="bottomButtons"
+        @button-click="handleBottomAction"
+    >
+    </ss-bottom>
+</div>
+
+<script>
+    // 功能说明:公共录入壳页(先调业务接口+dataTag,再按 include_input 装载 mp_ 表单页) by xu 2026-02-28
+    (function () {
+        const boot = function () {
+        const setStatus = (text) => {
+            if (state.vm) {
+                state.vm.placeholderText = text;
+                return;
+            }
+            const placeholder = document.getElementById('placeholder');
+            if (placeholder) placeholder.textContent = text;
+        };
+
+        const state = {
+            pageParams: {},
+            service: '',
+            initPayload: {},
+            dataTagPayload: {},
+            ssObjName: '',
+            ssObjId: '',
+            iframe: null,
+            submitEnabled: false,
+            vm: null,
+        };
+
+        const getUrlParams = () => {
+            const params = {};
+            const search = new URLSearchParams(window.location.search || '');
+            for (const [key, value] of search.entries()) {
+                params[key] = decodeURIComponent(value || '');
+            }
+            return params;
+        };
+
+        const unwrapData = (data) => {
+            if (!data || typeof data !== 'object') return {};
+            if (data.ssData && typeof data.ssData === 'object') return data.ssData;
+            return data;
+        };
+
+        const parseJsonString = (text) => {
+            if (!text || typeof text !== 'string') return {};
+            try {
+                const obj = JSON.parse(text);
+                return obj && typeof obj === 'object' ? obj : {};
+            } catch (_) {
+                return {};
+            }
+        };
+
+        const pruneParams = (obj) => {
+            const out = {};
+            Object.entries(obj || {}).forEach(([key, value]) => {
+                if (value === undefined || value === null || value === '') return;
+                out[key] = value;
+            });
+            return out;
+        };
+
+        // 功能说明:新增场景 id=0 统一按空处理,和你要求保持一致 by xu 2026-02-28
+        const resolveObjIdFromInit = (payload, ssObjName, urlParams) => {
+            const fromUrl = String(urlParams.ssObjId || '').trim();
+            if (fromUrl) return fromUrl;
+            if (!payload || typeof payload !== 'object') return '';
+            const objName = String(ssObjName || '').trim();
+            if (!objName) return '';
+            const objData = payload[objName];
+            if (!objData || typeof objData !== 'object') return '';
+            const idField = String(payload.ssObjIdName || objData.idName || '').trim();
+            if (!idField) return '';
+            const rawId = objData[idField];
+            if (rawId === undefined || rawId === null || rawId === '' || Number(rawId) === 0) return '';
+            return String(rawId);
+        };
+
+        // 功能说明:/page/xyqj_inp.jsp -> /page/mp_xyqj_inp.html by xu 2026-02-28
+        const includeToMpFormPath = (includeInput) => {
+            const raw = String(includeInput || '').trim();
+            if (!raw) return '';
+            const noPrefix = raw.replace(/^\/page\//i, '');
+            const base = noPrefix.replace(/\.ss\.jsp$/i, '').replace(/\.jsp$/i, '');
+            if (!base) return '';
+            return `/page/mp_${base}.html`;
+        };
+
+        const goBack = (refreshParent) => {
+            if (window.NavigationManager && typeof window.NavigationManager.goBack === 'function') {
+                window.NavigationManager.goBack({ refreshParent: !!refreshParent });
+            } else {
+                window.history.back();
+            }
+        };
+
+        const ensureRequest = () => {
+            if (!window.request || typeof window.request.post !== 'function') {
+                throw new Error('request 未就绪');
+            }
+            return window.request;
+        };
+
+        // 功能说明:内容区高度按 100vh 减去 ss-bottom 实际高度计算 by xu 2026-02-28
+        const updateContentHeight = () => {
+            const contentWrap = document.getElementById('contentWrap');
+            if (!contentWrap) return;
+            const bottom = document.querySelector('#app .ss-bottom');
+            const bottomHeight = bottom ? bottom.offsetHeight : 0;
+            contentWrap.style.height = `calc(100vh - ${bottomHeight}px)`;
+        };
+
+        const updateBottomButtons = (submitTitle) => {
+            if (!state.vm) return;
+            state.vm.bottomButtons = [
+                { text: '关闭', action: 'close' },
+                {
+                    text: submitTitle || '保存并提交',
+                    action: 'saveSubmit',
+                    backgroundColor: '#575d6d',
+                    color: '#fff'
+                }
+            ];
+            setTimeout(updateContentHeight, 0);
+        };
+
+        // 功能说明:供 iframe 子页调用,控制父页底部按钮显隐(解决日期弹层被底部按钮遮挡) by xu 2026-02-28
+        const setBottomVisible = (visible) => {
+            if (!state.vm) return;
+            state.vm.showBottom = visible !== false;
+            setTimeout(updateContentHeight, 0);
+        };
+
+        const getFrameFormData = () => {
+            const frame = document.getElementById('objInpFrame');
+            const frameWindow = frame ? frame.contentWindow : null;
+            if (!frameWindow) return { valid: false, message: '表单页面未加载完成', data: {} };
+
+            // 功能说明:优先走子页面暴露方法,尽量减少表单页工作量 by xu 2026-02-28
+            if (typeof frameWindow.__mpObjInpGetFormData === 'function') {
+                const result = frameWindow.__mpObjInpGetFormData();
+                if (result && typeof result === 'object') {
+                    return {
+                        valid: result.valid !== false,
+                        message: result.message || '',
+                        data: result.data && typeof result.data === 'object' ? result.data : {},
+                    };
+                }
+            }
+
+            const doc = frameWindow.document;
+            const data = {};
+            const nodes = doc.querySelectorAll('input[name],select[name],textarea[name]');
+            nodes.forEach((node) => {
+                const name = node.getAttribute('name');
+                if (!name || node.disabled) return;
+                const type = (node.getAttribute('type') || '').toLowerCase();
+                if ((type === 'checkbox' || type === 'radio') && !node.checked) return;
+                data[name] = node.value;
+            });
+            return { valid: true, message: '', data };
+        };
+
+        const getSubmitConfig = () => {
+            const p = state.initPayload || {};
+            // 功能说明:移动端只保留“保存并提交”,按钮可见性按第一接口 saveAndCommitDest 判断 by xu 2026-02-28
+            const enabled = !!String(p.saveAndCommitDest || '').trim();
+            return {
+                enabled,
+                serviceName: String(p.saveAndCommit || '').trim(),
+                dest: String(p.saveAndCommitDest || '').trim(),
+                title: String(p.saveAndCommitButtonValue || '保存并提交'),
+                paramObj: parseJsonString(p.saveAndCommitParam),
+            };
+        };
+
+        const renderFormFrame = () => {
+            const includeInput = (state.dataTagPayload && state.dataTagPayload.include_input) || '';
+            const formPath = includeToMpFormPath(includeInput);
+            console.log('[mp_objInp][renderFormFrame] include_input =', includeInput);
+            console.log('[mp_objInp][renderFormFrame] formPath =', formPath);
+            if (!formPath) {
+                if (state.vm) {
+                    state.vm.iframeSrc = '';
+                    state.vm.placeholderText = '未获取到 include_input,无法加载表单页';
+                }
+                state.submitEnabled = false;
+                console.warn('[mp_objInp][renderFormFrame] formPath为空,终止渲染iframe');
+                return;
+            }
+
+            const childParams = new URLSearchParams();
+            Object.entries(state.pageParams || {}).forEach(([key, value]) => {
+                if (value === undefined || value === null || value === '') return;
+                childParams.set(key, value);
+            });
+            childParams.set('embed', '1');
+            childParams.set('ssObjName', state.ssObjName || '');
+            childParams.set('ssObjId', state.ssObjId || '');
+
+            const iframeSrc = `${formPath}?${childParams.toString()}`;
+            console.log('[mp_objInp][renderFormFrame] iframe src =', iframeSrc);
+            if (state.vm) {
+                state.vm.iframeSrc = iframeSrc;
+            }
+
+            const submitCfg = getSubmitConfig();
+            state.submitEnabled = !!submitCfg.enabled;
+            updateBottomButtons(submitCfg.title);
+            setTimeout(updateContentHeight, 0);
+        };
+
+        const loadInitPayload = async () => {
+            const req = ensureRequest();
+            const postData = {
+                ...state.pageParams,
+                management: state.pageParams.management || '1',
+                isReady: '1',
+            };
+            const res = await req.post(
+                `/service?ssServ=${state.service}&management=1&isReady=1`,
+                postData,
+                { loading: false, formData: true }
+            );
+            state.initPayload = unwrapData(res ? res.data : null);
+            state.ssObjName = String(state.initPayload.ssObjName || state.pageParams.ssObjName || '').trim();
+            state.ssObjId = resolveObjIdFromInit(state.initPayload, state.ssObjName, state.pageParams);
+            console.log('[mp_objInp] init响应', state.initPayload);
+        };
+
+        const loadDataTagPayload = async () => {
+            const req = ensureRequest();
+            const dataTagParams = {
+                ...state.pageParams,
+                ssObjName: state.ssObjName,
+                ssObjId: state.ssObjId,
+            };
+            const res = await req.post(
+                `/service?ssServ=dataTag&ssDest=data&name=inp&ssObjName=${encodeURIComponent(state.ssObjName)}&ssObjId=${encodeURIComponent(state.ssObjId)}`,
+                dataTagParams,
+                { loading: false, formData: true }
+            );
+            state.dataTagPayload = unwrapData(res ? res.data : null);
+            console.log('[mp_objInp] dataTag响应', state.dataTagPayload);
+            console.log('[mp_objInp] dataTag include_input =', (state.dataTagPayload && state.dataTagPayload.include_input) || '');
+        };
+
+        const submitSaveAndCommit = async () => {
+            try {
+                const cfg = getSubmitConfig();
+                if (!state.submitEnabled || !cfg.enabled) {
+                    if (typeof showToastEffect === 'function') showToastEffect('当前页面不支持保存并提交', 1800, 'warning');
+                    return;
+                }
+                if (!cfg.serviceName || !cfg.dest) {
+                    if (typeof showToastEffect === 'function') showToastEffect('提交配置缺失', 1800, 'error');
+                    return;
+                }
+
+                const formResult = getFrameFormData();
+                if (!formResult.valid) {
+                    if (typeof showToastEffect === 'function') showToastEffect(formResult.message || '表单校验未通过', 2000, 'warning');
+                    return;
+                }
+
+                const req = ensureRequest();
+                const payload = pruneParams({
+                    ...(cfg.paramObj || {}),
+                    ...(formResult.data || {}),
+                    ssObjName: state.ssObjName || undefined,
+                    ssObjId: state.ssObjId || undefined,
+                    [((state.initPayload && state.initPayload.ssObjIdName) || 'ssObjId')]: state.ssObjId || undefined,
+                });
+
+                const res = await req.post(
+                    `/service?ssServ=${cfg.serviceName}&ssDest=${encodeURIComponent(cfg.dest)}`,
+                    payload,
+                    { loading: true, formData: true }
+                );
+                console.log('[mp_objInp] saveAndCommit响应', res);
+                const submitResp = unwrapData(res ? res.data : null);
+
+                // 功能说明:对齐PC流程,保存并提交后若目标为 addSure,则进入 mp_addSure 进行二次确认 by xu 2026-02-28
+                if (String(cfg.dest || '').trim().toLowerCase() === 'addsure') {
+                    // 功能说明:addSure 跳转参数改为白名单,避免将复杂对象/超长字段带入 URL 导致后端解析异常 by xu 2026-03-01
+                    const allowedKeys = [
+                        'msg',
+                        'ms',
+                        'print',
+                        'dataType',
+                        'ssServ',
+                        'thisViewObject',
+                        'wdclosewindowparam',
+                    ];
+
+                    const safeRespParams = {};
+                    const respObj = submitResp && typeof submitResp === 'object' ? submitResp : {};
+                    Object.keys(respObj).forEach((key) => {
+                        if (!allowedKeys.includes(key)) return;
+                        const value = respObj[key];
+                        if (value === undefined || value === null) return;
+                        const valueType = typeof value;
+                        if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
+                            safeRespParams[key] = value;
+                        }
+                    });
+
+                    const nextParams = pruneParams({
+                        ssObjName: submitResp.ssObjName || state.ssObjName,
+                        ssObjId: submitResp.ssObjId || state.ssObjId,
+                        ssObjIdName: submitResp.ssObjIdName || (state.initPayload && state.initPayload.ssObjIdName) || 'ssObjId',
+                        fromObjInp: '1',
+                        ...safeRespParams,
+                    });
+                    if (window.NavigationManager && typeof window.NavigationManager.goTo === 'function') {
+                        window.NavigationManager.goTo('mp_addSure', nextParams, { needRefresh: true });
+                    } else {
+                        const qp = new URLSearchParams();
+                        Object.keys(nextParams).forEach((k) => qp.set(k, nextParams[k]));
+                        window.location.href = `/page/mp_addSure.html?${qp.toString()}`;
+                    }
+                    return;
+                }
+
+                if (typeof showToastEffect === 'function') showToastEffect('提交成功', 1400, 'success');
+                setTimeout(() => goBack(true), 600);
+            } catch (err) {
+                console.error('[mp_objInp] 提交失败', err);
+                if (typeof showToastEffect === 'function') showToastEffect('提交失败,请稍后重试', 2200, 'error');
+            }
+        };
+
+        const init = async () => {
+            try {
+                setStatus('正在初始化录入页...');
+                state.pageParams = getUrlParams();
+                state.service = String(state.pageParams.service || '').trim();
+                if (!state.service) throw new Error('缺少 service 参数');
+
+                setStatus('正在加载业务配置...');
+                await loadInitPayload();
+                if (!state.ssObjName) throw new Error('第一接口未返回 ssObjName');
+
+                setStatus('正在加载 dataTag...');
+                await loadDataTagPayload();
+                setStatus('正在加载表单页面...');
+                renderFormFrame();
+                setTimeout(updateContentHeight, 0);
+            } catch (err) {
+                console.error('[mp_objInp] 初始化失败', err);
+                setStatus(`初始化失败:${err && err.message ? err.message : err}`);
+                state.submitEnabled = false;
+            }
+        };
+
+        window.addEventListener('resize', updateContentHeight);
+
+        // 功能说明:使用 ss-bottom 统一底部样式与交互 by xu 2026-02-28
+        window.SS.dom.initializeFormApp({
+            el: '#app',
+            data() {
+                return {
+                    iframeSrc: '',
+                    placeholderText: '正在加载录入页面...',
+                    showBottom: true,
+                    bottomButtons: [
+                        { text: '关闭', action: 'close' },
+                        {
+                            text: '保存并提交',
+                            action: 'saveSubmit',
+                            backgroundColor: '#575d6d',
+                            color: '#fff'
+                        }
+                    ]
+                }
+            },
+            methods: {
+                handleFrameLoad() {
+                    console.log('[mp_objInp][renderFormFrame] iframe onload');
+                    setTimeout(updateContentHeight, 0);
+                },
+                handleBottomAction(payload) {
+                    if (payload && payload.action === 'close') {
+                        goBack(false);
+                        return;
+                    }
+                    if (payload && payload.action === 'saveSubmit') {
+                        submitSaveAndCommit();
+                    }
+                }
+            },
+            mounted() {
+                state.vm = this;
+                window.__mpObjInpSetBottomVisible = setBottomVisible;
+                init();
+            },
+            beforeUnmount() {
+                window.removeEventListener('resize', updateContentHeight);
+                try {
+                    if (window.__mpObjInpSetBottomVisible === setBottomVisible) {
+                        delete window.__mpObjInpSetBottomVisible;
+                    }
+                } catch (_) {}
+            }
+        });
+        };
+
+        // 功能说明:SS.ready 某些场景可能不触发,增加超时兜底启动 by xu 2026-02-28
+        let booted = false;
+        let tries = 0;
+        const maxTries = 20;
+        const safeBoot = function () {
+            if (booted) return;
+            const ready = !!(window.SS && window.SS.dom && typeof window.SS.dom.initializeFormApp === 'function');
+            if (!ready) {
+                tries += 1;
+                if (tries >= maxTries) {
+                    const ph = document.getElementById('placeholder');
+                    if (ph) ph.textContent = '初始化失败:ss 组件未就绪';
+                    return;
+                }
+                setTimeout(safeBoot, 200);
+                return;
+            }
+            booted = true;
+            boot();
+        };
+
+        if (window.SS && typeof window.SS.ready === 'function') {
+            window.SS.ready(safeBoot);
+            setTimeout(safeBoot, 1600);
+        } else {
+            setTimeout(safeBoot, 0);
+        }
+    })();
+</script>
+
+</body>
+</html>

+ 1261 - 0
page/mp_objList.html

@@ -0,0 +1,1261 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>列表</title>
+    <!-- 引入基础依赖(统一由 base.js 动态注入其他依赖) -->
+    <script src="/js/mp_base/base.js"></script>
+    <style>
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+        }
+
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        /* 搜索筛选区域 */
+        .search-filter-container {
+            background: #f5f5f5;
+            padding: 15px;
+            position: sticky;
+            top: 0;
+            z-index: 100;
+            display: flex;
+            justify-content: flex-end;
+            flex-wrap: wrap;
+            gap: 10px;
+        }
+        .search-filter-container .ss-select-container{
+            border: 1px solid #ccc;
+            border-radius: 4px;
+            padding: 0 10px;
+            height: 34px;
+            box-sizing: border-box;
+        }
+        
+        /* 加载状态 */
+        .loading-container {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            height: 200px;
+            color: #666;
+        }
+
+        .loading-spinner {
+            width: 40px;
+            height: 40px;
+            border: 4px solid #f3f3f3;
+            border-top: 4px solid #40ac6d;
+            border-radius: 50%;
+            animation: spin 1s linear infinite;
+            margin-bottom: 15px;
+        }
+
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+
+        .loading-text {
+            font-size: 14px;
+        }
+
+        /* 列表容器 */
+        .list-container {
+            padding:0 15px;
+        }
+
+        /* 空状态 */
+        .empty-state {
+            text-align: center;
+            padding: 60px 20px;
+            color: #999;
+        }
+
+        .empty-icon {
+            font-size: 48px;
+            margin-bottom: 15px;
+        }
+
+        .empty-text {
+            font-size: 16px;
+        }
+
+        /* 卡片内容样式 - 按照小程序list.vue转换 */
+        .card-content .card-header {
+            margin-bottom: 10px; /* 20rpx -> 10px */
+        }
+
+        .card-content .card-title {
+            font-size: 16px; /* 32rpx -> 16px */
+            font-weight: bold;
+            color: #333;
+        }
+
+        .card-content .card-description {
+            font-size: 14px; /* 28rpx -> 14px */
+            color: #666;
+            margin-bottom: 8px; /* 15rpx -> 8px */
+        }
+
+        .card-content .attribute-group {
+            display: flex;
+            flex-wrap: wrap;
+            column-gap: 10px; /* 20rpx -> 10px */
+        }
+
+        .card-content .attribute-item {
+            display: flex;
+            margin-bottom: 5px; /* 10rpx -> 5px */
+        }
+
+        .card-content .attr-label {
+            font-size: 13px; /* 26rpx -> 13px */
+            color: #999;
+        }
+
+        .card-content .attr-value {
+            font-size: 13px; /* 26rpx -> 13px */
+            color: #333;
+            flex: 1;
+        }
+
+        /* 状态文本样式 */
+        .status-text {
+            font-weight: bold;
+        }
+
+
+
+        /* 加载更多提示样式 */
+        .load-more-container {
+            text-align: center;
+            padding: 20px;
+            color: #999;
+            font-size: 14px;
+        }
+
+        .load-more-loading {
+            color: #007aff;
+        }
+
+        .load-more-end {
+            color: #999;
+        }
+
+        .load-more-tip {
+            color: #ccc;
+        }
+
+        /* 回到顶部按钮 */
+        .back-to-top-btn {
+            width: 50px;
+            height: 50px;
+            border-radius: 50%;
+            background: rgba(87, 93, 109, 0.5);
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            position: fixed;
+            bottom: 200px;
+            right: 15px;
+            cursor: pointer;
+            transition: all 0.3s ease;
+            z-index: 999;
+        }
+
+        .back-to-top-btn:active {
+            transform: scale(0.9);
+        }
+
+        .back-to-top-inner {
+            width: 42px;
+            height: 42px;
+            border-radius: 50%;
+            background: rgba(87, 93, 109, 0.5);
+            display: flex;
+            justify-content: center;
+            align-items: center;
+        }
+
+        /* 搜索弹窗 */
+        .search-modal-mask {
+            position: fixed;
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+            background: rgba(0, 0, 0, 0.5);
+            z-index: 1000;
+            display: flex;
+            align-items: flex-end;
+        }
+
+        .search-modal-content {
+            width: 100%;
+            background: white;
+            animation: slideUp 0.3s ease-out;
+            /* 让键盘弹起时自动上移 */
+            position: relative;
+        }
+
+        @keyframes slideUp {
+            from {
+                transform: translateY(100%);
+            }
+            to {
+                transform: translateY(0);
+            }
+        }
+
+        .search-input-container {
+            display: flex;
+            align-items: center;
+            height: 50px;
+            background: white;
+            border-top: 1px solid #e5e5e5;
+            /* 适配iPhone底部安全区域 */
+            /* padding-bottom: env(safe-area-inset-bottom); */
+        }
+
+        .search-input {
+            flex: 1;
+            height: 100%;
+            border: none;
+            padding: 0 15px;
+            font-size: 16px;
+            outline: none;
+            background: transparent;
+        }
+
+        .search-divider {
+            width: 1px;
+            height: 30px;
+            background: #d5d8dc;
+        }
+
+        .search-icon-btn {
+            width: 60px;
+            height: 100%;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            cursor: pointer;
+            background: transparent;
+            border: none;
+            padding: 0;
+        }
+
+        .search-icon-btn:active {
+            background: #f5f5f5;
+        }
+
+    </style>
+</head>
+<body>
+    <div id="app" v-cloak>
+        <!-- 搜索和筛选区域 -->
+        <div class="search-filter-container">
+            <!-- 动态下拉选择器 -->
+            <template v-for="(options, fieldName) in filterSelectOptions" :key="fieldName">
+                <ss-select
+                    v-model="selectedFilters[fieldName]"
+                    :placeholder="`选择${getFieldDesc(fieldName)}`"
+                    :options="options"
+                    @change="handleFilterChange"
+                >
+                </ss-select>
+            </template>
+            <!-- 完全由buttonList决定的动态按钮组 -->
+            <ss-search-button
+                v-for="(button, index) in buttonList"
+                :key="index"
+                :text="getButtonText(button)"
+                
+                @click="handleButtonClick(button)"
+            >
+            </ss-search-button>
+        </div>
+
+        <!-- 加载状态 -->
+        <div v-if="loading" class="loading-container">
+            <div class="loading-spinner"></div>
+            <div class="loading-text">加载中...</div>
+        </div>
+
+        <!-- 列表区域 -->
+        <div v-else class="list-container">
+            <!-- 空状态 -->
+            <div v-if="list.length === 0" class="empty-state">
+                <div class="empty-icon">📋</div>
+                <div class="empty-text">暂无数据</div>
+            </div>
+
+            <!-- 数据列表 -->
+            <div v-else>
+                <ss-card
+                    v-for="(item, index) in list"
+                    :key="index"
+                    :item="item"
+                    @click="handleCardClick(item)"
+                    @button-click="handleCardAction"
+                >
+                    <!-- 卡片内容 - 按照小程序list.vue的结构,使用API数据 -->
+                    <div class="card-content">
+                        <!-- 主标题 (first) -->
+                        <div class="card-header" v-if="item.firstDisplay">
+                            <div class="card-title">{{ item.firstDisplay }}</div>
+                        </div>
+
+                        <!-- 描述 (second) -->
+                        <div class="card-description" v-if="item.secondDisplay">
+                            {{ item.secondDisplay }}
+                        </div>
+
+                        <!-- 属性列表 (third) -->
+                        <div class="card-attributes" v-if="item.thirdDisplay && item.thirdDisplay.length > 0">
+                            <div
+                                v-for="(group, groupIndex) in item.thirdDisplay"
+                                :key="groupIndex"
+                                class="attribute-group"
+                            >
+                                <div
+                                    v-for="(attr, attrIndex) in group"
+                                    :key="attrIndex"
+                                    class="attribute-item"
+                                >
+                                    <span class="attr-label">{{ attr.field.desc }}:</span>
+                                    <span class="attr-value">{{ attr.displayValue }}</span>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </ss-card>
+            </div>
+        </div>
+
+        <!-- 加载更多提示 -->
+        <div class="load-more-container" v-if="list.length > 0">
+            <div v-if="isLoadingMore" class="load-more-loading">
+                <span>正在加载更多...</span>
+            </div>
+            <div v-else-if="!hasMore" class="load-more-end">
+                <span>没有更多数据了</span>
+            </div>
+            <div v-else class="load-more-tip">
+                <span>滚动到底部加载更多</span>
+            </div>
+        </div>
+
+        <!-- 回到顶部按钮 -->
+        <div
+            v-if="showBackToTop"
+            class="back-to-top-btn"
+            @touchstart="handleLongPressStart"
+            @touchend="handleLongPressEnd"
+            @touchcancel="handleLongPressCancel"
+            @click.prevent="handleBackToTopClick"
+        >
+            <div class="back-to-top-inner">
+                <Icon name="icon-huidaodingbu" size="40" color="#fff"></Icon>
+            </div>
+        </div>
+
+        <!-- 搜索弹窗 -->
+        <div v-if="showSearchModal" class="search-modal-mask" @click="closeSearchModal">
+            <div class="search-modal-content" @click.stop>
+                <div class="search-input-container">
+                    <input
+                        ref="searchInput"
+                        v-model="searchKeyword"
+                        type="search"
+                        inputmode="search"
+                        class="search-input"
+                        placeholder="请输入关键词"
+                        @keyup.enter="performSearch"
+                        autocomplete="off"
+                    />
+                    <div class="search-divider"></div>
+                    <button class="search-icon-btn" @click="performSearch">
+                        <Icon name="icon-chazhao" size="24" color="#575d6d"></Icon>
+                    </button>
+                </div>
+            </div>
+        </div>
+
+    </div>
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        // 加载状态
+                        loading: true,
+
+                        // 列表数据
+                        list: [],
+                        originalList: [], // 原始数据,用于筛选
+
+                        // 分页相关
+                        currentPage: 1,
+                        pageSize: 10,
+                        hasMore: true,
+                        isLoadingMore: false,
+
+                        // 页面参数
+                        pageParams: {},
+                        service: '', // init服务名称
+                        // 功能说明:对接PC两段式接口(init 返回 home/list 服务名) by xu 2026-02-28
+                        ssSearchPobjHomeServName: '',
+                        ssSearchPobjListServName: '',
+
+                        // API返回的动态配置
+                        buttonList: [],
+                        fieldsList: [],
+                        ssPaging: null,
+
+                        // 动态搜索筛选选项
+                        filterOptions: [],
+                        sortOptions: [],
+
+                        // 下拉选择器数据
+                        selectedFilters: {}, // 动态筛选条件
+                        filterSelectOptions: {}, // 各个筛选字段的选项
+
+                        // 字典缓存
+                        dictCache: new Map(),
+
+                        // 显示字段配置 - 根据service动态设置
+                        displayFields: [],
+
+                        // 卡片操作按钮
+                        cardActions: [
+                            { text: '查看', name: 'view' },
+                            { text: '编辑', name: 'edit' }
+                        ],
+
+                        // 回到顶部按钮
+                        showBackToTop: false,
+
+                         // 是否存在关键词搜索
+                        hasKeyWord:false,
+
+                        // 搜索相关
+                        showSearchModal: false,
+                        searchKeyword: '',
+
+                        // 长按相关
+                        longPressTimer: null,
+                        isLongPress: false,
+
+                    }
+                },
+
+                mounted() {
+                    // 页面加载时初始化
+                    this.initPage();
+
+                    // 监听页面刷新通知
+                    this.setupRefreshListener();
+
+                    // 监听滚动事件,实现滚动加载
+                    this.setupScrollListener();
+
+                    // 开发模式:加载Mock数据(测试用)
+                    // this.loadMockData();
+                },
+
+                beforeUnmount() {
+                    // 清理刷新监听器
+                    if (this.refreshCleanup) {
+                        this.refreshCleanup();
+                    }
+
+                    // 清理滚动监听器
+                    if (this.scrollCleanup) {
+                        this.scrollCleanup();
+                    }
+                },
+
+                methods: {
+                    // 加载Mock数据(用于开发测试)
+                    loadMockData() {
+                        console.log('🎭 加载Mock数据...');
+
+                        const mockData = [
+                            {
+                                firstDisplay: '张三 - 2024级计算机科学与技术1班',
+                                secondDisplay: '学号:2024001 | 手机:138****1234',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '男' },
+                                        { field: { desc: '年龄' }, displayValue: '20' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '王老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '李四 - 2024级软件工程2班',
+                                secondDisplay: '学号:2024002 | 手机:139****5678',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '女' },
+                                        { field: { desc: '年龄' }, displayValue: '19' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '李老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '王五 - 2023级人工智能1班',
+                                secondDisplay: '学号:2023003 | 手机:136****9012',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '男' },
+                                        { field: { desc: '年龄' }, displayValue: '21' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '赵老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '赵六 - 2024级数据科学与大数据技术1班',
+                                secondDisplay: '学号:2024004 | 手机:137****3456',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '女' },
+                                        { field: { desc: '年龄' }, displayValue: '20' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '刘老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '钱七 - 2023级网络工程1班',
+                                secondDisplay: '学号:2023005 | 手机:135****7890',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '男' },
+                                        { field: { desc: '年龄' }, displayValue: '21' },
+                                        { field: { desc: '状态' }, displayValue: '休学' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '周老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '孙八 - 2024级信息安全1班',
+                                secondDisplay: '学号:2024006 | 手机:133****2468',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '女' },
+                                        { field: { desc: '年龄' }, displayValue: '19' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '吴老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '周九 - 2023级物联网工程1班',
+                                secondDisplay: '学号:2023007 | 手机:188****1357',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '男' },
+                                        { field: { desc: '年龄' }, displayValue: '22' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '郑老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '吴十 - 2024级云计算1班',
+                                secondDisplay: '学号:2024008 | 手机:189****2468',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '女' },
+                                        { field: { desc: '年龄' }, displayValue: '20' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '冯老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '郑十一 - 2023级区块链工程1班',
+                                secondDisplay: '学号:2023009 | 手机:180****3691',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '男' },
+                                        { field: { desc: '年龄' }, displayValue: '21' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '陈老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '王十二 - 2024级电子商务1班',
+                                secondDisplay: '学号:2024010 | 手机:181****4802',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '女' },
+                                        { field: { desc: '年龄' }, displayValue: '19' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '褚老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '陈十三 - 2023级金融科技1班',
+                                secondDisplay: '学号:2023011 | 手机:182****5913',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '男' },
+                                        { field: { desc: '年龄' }, displayValue: '22' },
+                                        { field: { desc: '状态' }, displayValue: '毕业' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2023-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '卫老师' }
+                                    ]
+                                ]
+                            },
+                            {
+                                firstDisplay: '刘十四 - 2024级数字媒体技术1班',
+                                secondDisplay: '学号:2024012 | 手机:183****6024',
+                                thirdDisplay: [
+                                    [
+                                        { field: { desc: '性别' }, displayValue: '女' },
+                                        { field: { desc: '年龄' }, displayValue: '20' },
+                                        { field: { desc: '状态' }, displayValue: '在读' }
+                                    ],
+                                    [
+                                        { field: { desc: '入学时间' }, displayValue: '2024-09-01' },
+                                        { field: { desc: '辅导员' }, displayValue: '蒋老师' }
+                                    ]
+                                ]
+                            }
+                        ];
+
+                        // 设置数据
+                        this.list = mockData;
+                        this.originalList = [...mockData];
+                        this.loading = false;
+                        this.hasMore = false;
+
+                        console.log('✅ Mock数据加载完成,共', mockData.length, '条');
+                    },
+
+                    // 初始化页面
+                    async initPage() {
+                        try {
+                            console.log('🔄 初始化列表页面...');
+
+                            // 获取URL参数
+                            this.pageParams = this.getUrlParams();
+                            this.service = this.pageParams.service || 'default';
+
+                            console.log('📋 页面参数:', this.pageParams);
+
+                            // 功能说明:先调 init,再按 init 返回的 home/list 服务继续拉取数据 by xu 2026-02-28
+                            await this.loadInitAndHomeData();
+
+                        } catch (error) {
+                            console.log('❌ 页面初始化失败:', error);
+                            // this.showToast('页面初始化失败', 'error');
+                        } finally {
+                            this.loading = false;
+                        }
+                    },
+
+                    // 功能说明:调用 init 接口并解析 ssSearchPobjHomeServName/ssSearchPobjListServName by xu 2026-02-28
+                    async loadInitAndHomeData() {
+                        const initService = (this.service || '').trim();
+                        if (!initService) {
+                            await this.loadData(1, false);
+                            return;
+                        }
+
+                        const initParams = {
+                            pageNo: 1,
+                            rowNumPer: this.pageSize,
+                            management: '1',
+                            isReady: '1'
+                        };
+
+                        const initResult = await request.post(
+                            `/service?ssServ=${initService}&management=1&isReady=1`,
+                            initParams,
+                            {
+                                loading: false,
+                                formData: true
+                            }
+                        );
+
+                        const initPayload = this.unwrapResponseData(initResult?.data);
+                        this.ssSearchPobjHomeServName = String(initPayload?.ssSearchPobjHomeServName || '').trim();
+                        this.ssSearchPobjListServName = String(initPayload?.ssSearchPobjListServName || '').trim();
+
+                        console.log('✅ init返回服务名:', {
+                            initService,
+                            ssSearchPobjHomeServName: this.ssSearchPobjHomeServName,
+                            ssSearchPobjListServName: this.ssSearchPobjListServName
+                        });
+
+                        const homeService = this.ssSearchPobjHomeServName || initService;
+                        await this.loadDataByService(homeService, 1, false);
+                    },
+
+                    // 功能说明:统一处理 /service 返回结构(兼容 {ssData} 与平铺结构) by xu 2026-02-28
+                    unwrapResponseData(data) {
+                        if (!data || typeof data !== 'object') {
+                            return {};
+                        }
+                        if (data.ssData && typeof data.ssData === 'object') {
+                            return data.ssData;
+                        }
+                        return data;
+                    },
+
+                    // 功能说明:封装按指定 ssServ 拉取列表数据(home/list 共用) by xu 2026-02-28
+                    async loadDataByService(ssServ, pageNo = 1, isLoadMore = false) {
+                        const serviceName = String(ssServ || '').trim();
+                        if (!serviceName) {
+                            console.warn('⚠️ 缺少服务名,跳过请求');
+                            return;
+                        }
+
+                        // 防止重复加载:只有在非首次加载且正在加载时才阻止
+                        if (this.loading && !isLoadMore && this.list.length > 0) return;
+                        if (this.isLoadingMore && isLoadMore) return;
+
+                        try {
+                            console.log(`🔄 加载列表数据... 服务: ${serviceName}, 页码: ${pageNo}, 加载更多: ${isLoadMore}`);
+
+                            if (isLoadMore) {
+                                this.isLoadingMore = true;
+                            } else {
+                                this.loading = true;
+                            }
+
+                            const requestParams = {
+                                pageNo: pageNo,
+                                rowNumPer: this.pageSize,
+                                management: '1',
+                                isReady: '1',
+                                // 功能说明:请求参数只保留有效筛选项(避免空值/旧值污染查询) by xu 2026-02-28
+                                ...this.getActiveFilterParams(this.selectedFilters)
+                            };
+
+                            const result = await request.post(
+                                `/service?ssServ=${serviceName}&management=1&isReady=1`,
+                                requestParams,
+                                {
+                                    loading: false,
+                                    formData: true
+                                }
+                            );
+
+                            console.log('✅ API响应数据:', result);
+
+                            if (result && result.data) {
+                                await this.processApiData(result.data, isLoadMore);
+                            } else {
+                                console.warn('⚠️ API返回数据格式异常:', result);
+                            }
+
+                        } catch (error) {
+                            console.error('❌ 数据加载失败:', error);
+                        } finally {
+                            if (isLoadMore) {
+                                this.isLoadingMore = false;
+                            } else {
+                                this.loading = false;
+                            }
+                        }
+                    },
+
+                    // 加载数据
+                    async loadData(pageNo = 1, isLoadMore = false) {
+                        // 功能说明:翻页/筛选/搜索统一走 list 服务;未返回时回退 home/init by xu 2026-02-28
+                        const listService = this.ssSearchPobjListServName || this.ssSearchPobjHomeServName || this.service;
+                        await this.loadDataByService(listService, pageNo, isLoadMore);
+                    },
+
+                    // 处理API返回的数据
+                    async processApiData(data, isLoadMore = false) {
+                        try {
+                            const payload = this.unwrapResponseData(data);
+                            const objectList = Array.isArray(payload.objectList)
+                                ? payload.objectList
+                                : Array.isArray(payload.objList)
+                                    ? payload.objList
+                                    : [];
+                            const draftList = Array.isArray(payload.draftList) ? payload.draftList : [];
+                            const combinedObjectList = draftList.concat(objectList);
+
+                            console.log('🔄 处理API数据...', {
+                                isLoadMore,
+                                objectListLength: combinedObjectList.length,
+                                fromObjList: Array.isArray(payload.objList)
+                            });
+
+                            // 保存API返回的配置信息
+                            if (!isLoadMore) {
+                                // 功能说明:列表接口通常不返回按钮/搜索字段,缺省时保留首屏(home)已加载配置,避免筛选后按钮消失 by xu 2026-02-28
+                                if (Array.isArray(payload.buttonList) || Array.isArray(payload.rootFuncList)) {
+                                    this.buttonList = payload.buttonList || payload.rootFuncList || [];
+                                }
+                                if (Array.isArray(payload.fieldsList) || Array.isArray(payload.searchFieldList)) {
+                                    this.fieldsList = payload.fieldsList || payload.searchFieldList || [];
+                                }
+                            }
+                            // ssPaging信息每次都要更新,因为包含当前页信息
+                            this.ssPaging = payload.ssPaging || this.ssPaging || null;
+                            // 功能说明:list接口未返回 hasKeyword 时沿用首屏值,避免长按搜索入口被错误隐藏 by xu 2026-02-28
+                            if (Object.prototype.hasOwnProperty.call(payload, 'hasKeyWord') || Object.prototype.hasOwnProperty.call(payload, 'hasKeyword')) {
+                                this.hasKeyWord = payload.hasKeyWord || payload.hasKeyword || false;
+                            }
+                            // 处理objectList数据
+                            if (combinedObjectList.length > 0) {
+                                // 使用field-formatter.js格式化列表数据
+                                const formattedList = await window.formatObjectList(combinedObjectList, this.dictCache);
+
+                                if (isLoadMore) {
+                                    // 加载更多:追加到现有列表
+                                    this.list = [...this.list, ...formattedList];
+                                    this.originalList = [...this.originalList, ...formattedList];
+                                } else {
+                                    // 首次加载或刷新:替换列表
+                                    this.originalList = formattedList;
+                                    this.list = [...this.originalList];
+                                }
+
+                                console.log('✅ 列表数据处理完成:', this.list.length, '条');
+
+                                // 更新分页状态
+                                this.currentPage = isLoadMore ? this.currentPage + 1 : 1;
+
+                                // 根据ssPaging信息判断是否还有更多数据
+                                if (this.ssPaging && this.ssPaging.rowNum !== undefined) {
+                                    const totalRecords = this.ssPaging.rowNum;
+                                    const currentRecords = this.list.length;
+                                    this.hasMore = currentRecords < totalRecords;
+                                    console.log('📊 分页信息:', {
+                                        totalRecords,
+                                        currentRecords,
+                                        hasMore: this.hasMore,
+                                        currentPage: this.currentPage
+                                    });
+                                } else {
+                                    // 降级处理:根据当前页数据量判断
+                                    this.hasMore = formattedList.length >= this.pageSize;
+                                    console.log('⚠️ 使用降级分页判断:', {
+                                        returnedCount: formattedList.length,
+                                        pageSize: this.pageSize,
+                                        hasMore: this.hasMore
+                                    });
+                                }
+                            } else {
+                                // 没有更多数据
+                                this.hasMore = false;
+                                console.log('❌ 没有返回数据,设置hasMore为false');
+                            }
+
+                            // 根据fieldsList生成筛选选项(只在首次加载时生成)
+                            if (!isLoadMore) {
+                                await this.generateFilterOptions();
+                            }
+
+                        } catch (error) {
+                            console.error('❌ 数据处理失败:', error);
+                            throw error;
+                        }
+                    },
+
+
+
+
+
+                    // 获取URL参数
+                    getUrlParams() {
+                        const params = {};
+                        const urlSearchParams = new URLSearchParams(window.location.search);
+                        for (const [key, value] of urlSearchParams) {
+                            params[key] = decodeURIComponent(value);
+                        }
+                        return params;
+                    },
+
+
+
+                    // 处理buttonList按钮点击
+                    handleButtonClick(button) {
+                        console.log('🔘 按钮点击:', button);
+
+                        // 直接跳转到目标页面
+                        const destPage = button.function?.dest || button.dest;
+                        NavigationManager.goToFromButton(button);
+                    },
+
+
+
+                    // 跳转到目标页面
+                    navigateToPage(button, destPage) {
+                        const urlParams = new URLSearchParams(window.location.search);
+
+                        // 添加按钮相关参数
+                        if (button.function) {
+                            urlParams.set('dest', destPage);
+                            urlParams.set('title', encodeURIComponent(button.function.desc || button.buttonName));
+                            urlParams.set('service', button.function.servName || button.service || '');
+                        } else {
+                            urlParams.set('dest', destPage);
+                            urlParams.set('title', encodeURIComponent(button.buttonName));
+                            urlParams.set('service', button.service || '');
+                        }
+                        const newUrl = `${destPage}.html?${urlParams.toString()}`;
+
+                        console.log('� 跳转到:', newUrl);
+                        window.location.href = newUrl;
+                    },
+
+
+
+                    // 根据fieldsList生成筛选选项
+                    async generateFilterOptions() {
+                        if (!this.fieldsList || this.fieldsList.length === 0) {
+                            return;
+                        }
+
+                        for (const field of this.fieldsList) {
+                            // 如果字段有cbName,生成下拉选项
+                            if (field.cbName) {
+                                try {
+                                    const options = await window.getDictOptions(field.cbName, this.dictCache);
+                                    this.filterSelectOptions[field.name] = [
+                                        { n: `全部${field.desc}`, v: '' },
+                                        ...options
+                                    ];
+                                } catch (error) {
+                                    console.error('获取筛选选项失败:', field.cbName, error);
+                                }
+                            }
+                        }
+
+                        console.log('🔽 生成筛选选项:', this.filterSelectOptions);
+                    }, 
+
+                    // 卡片点击 - SsCard组件会自动传递item数据
+                    handleCardClick(item) {
+                        console.log('📄 卡片点击事件触发',item);
+                        
+                    },
+
+                    // 卡片操作 - SsCard组件的按钮点击事件
+                    handleCardAction({ button, item, index }) {
+                        console.log('⚡ 卡片操作:', button, item);
+
+                        if (button && button.onclick) {
+                            // 执行按钮的onclick回调
+                            button.onclick();
+                        }
+
+                        this.showToast(`执行操作: ${button.title}`, 'info');
+                    },
+
+                    // 加载更多数据
+                    async loadMore() {
+                        if (!this.hasMore || this.isLoadingMore) {
+                            console.log('🚫 无法加载更多:', { hasMore: this.hasMore, isLoadingMore: this.isLoadingMore });
+                            return;
+                        }
+
+                        console.log('📄 加载更多数据...');
+                        await this.loadData(this.currentPage + 1, true);
+                    },
+
+                    // 刷新数据
+                    async refreshData() {
+                        console.log('🔄 刷新数据...');
+                        // 重置分页状态
+                        this.currentPage = 1;
+                        this.hasMore = true;
+                        this.list = [];
+                        this.originalList = [];
+
+                        try {
+                            await this.loadData(1, false);
+                            this.showToast('刷新成功', 'success');
+                        } catch (error) {
+                            this.showToast('刷新失败', 'error');
+                        }
+                    },
+
+                    // 获取字段描述
+                    getFieldDesc(fieldName) {
+                        const field = this.fieldsList.find(f => f.name === fieldName);
+                        return field ? field.desc : fieldName;
+                    },
+
+                    // 功能说明:统一按钮文案映射(兼容 rootFuncList/buttonList 不同字段) by xu 2026-02-28
+                    getButtonText(button) {
+                        if (!button || typeof button !== 'object') return '';
+                        return button.desc || button.title || button.buttonName || button.name || '';
+                    },
+
+                    // 功能说明:提取有效筛选参数(过滤空串/null/undefined) by xu 2026-02-28
+                    getActiveFilterParams(source) {
+                        const params = {};
+                        const obj = (source && typeof source === 'object') ? source : {};
+                        Object.entries(obj).forEach(([key, value]) => {
+                            if (value === '' || value === null || value === undefined) return;
+                            params[key] = value;
+                        });
+                        return params;
+                    },
+
+                    // 筛选选择器变化(立即搜索)
+                    handleFilterChange(value) {
+                        console.log('🔽 筛选条件变化,立即搜索:', value);
+                        // 立即执行搜索
+                        this.applyDynamicFilters();
+                    },
+
+                    // 应用动态筛选(重新调用API)
+                    async applyDynamicFilters() {
+                        try {
+                            console.log('🔍 应用筛选条件:', this.selectedFilters);
+
+                            // 功能说明:筛选参数先归一化,清理已取消的筛选条件 by xu 2026-02-28
+                            const filterParams = this.getActiveFilterParams(this.selectedFilters);
+
+                            // 功能说明:筛选后重置分页并走 list 服务 by xu 2026-02-28
+                            this.currentPage = 1;
+                            this.hasMore = true;
+                            this.list = [];
+                            this.originalList = [];
+                            this.selectedFilters = { ...filterParams };
+                            await this.loadData(1, false);
+
+                        } catch (error) {
+                            console.error('❌ 筛选失败:', error);
+                            this.showToast('筛选失败', 'error');
+                        }
+                    },
+
+                    // 生成项目按钮
+                    generateItemButtons(service) {
+                        if (!service || !service.play) return [];
+
+                        return [{
+                            title: service.play.name || '查看',
+                            icon: 'icon-chakan',
+                            onclick: () => {
+                                console.log('点击查看按钮:', service.play);
+                                // 这里可以跳转到详情页面
+                                this.showToast(`查看: ${service.play.title}`, 'info');
+                            }
+                        }];
+                    },
+
+
+
+                    // 设置页面刷新监听
+                    setupRefreshListener() {
+                        // 监听子页面返回时的刷新通知
+                        this.refreshCleanup = NavigationManager.onRefreshNotify((refreshData) => {
+                            console.log('📢 收到刷新通知,重新加载数据');
+                            this.refreshData();
+                        });
+                    },
+
+                    // 设置滚动监听
+                    setupScrollListener() {
+                        let isThrottled = false;
+
+                        const handleScroll = () => {
+                            if (isThrottled) return;
+
+                            isThrottled = true;
+                            setTimeout(() => {
+                                isThrottled = false;
+                            }, 200); // 节流200ms
+
+                            // 检查是否滚动到底部
+                            const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
+                            const windowHeight = window.innerHeight;
+                            const documentHeight = document.documentElement.scrollHeight;
+
+                            // 控制回到顶部按钮显示:滚动超过300px时显示
+                            this.showBackToTop = scrollTop > 300;
+
+                            // 距离底部50px时触发加载更多
+                            if (scrollTop + windowHeight >= documentHeight - 50) {
+                                console.log('📄 滚动到底部,尝试加载更多...');
+                                this.loadMore();
+                            }
+                        };
+
+                        // 添加滚动监听
+                        window.addEventListener('scroll', handleScroll);
+
+                        // 保存清理函数
+                        this.scrollCleanup = () => {
+                            window.removeEventListener('scroll', handleScroll);
+                        };
+                    },
+
+
+
+                    // 返回顶部
+                    scrollToTop() {
+                        window.scrollTo({
+                            top: 0,
+                            behavior: 'smooth'
+                        });
+                    },
+
+                    // 处理回到顶部按钮点击
+                    handleBackToTopClick() {
+                        // 如果不是长按触发的搜索,则执行返回顶部
+                        if (!this.isLongPress) {
+                            this.scrollToTop();
+                        }
+                        // 重置长按标志
+                        this.isLongPress = false;
+                    },
+
+                    // 长按开始
+                    handleLongPressStart(event) {
+                        if(this.hasKeyWord){ // 有输入关键字才显示,否则不处理长按
+                            this.isLongPress = false;
+
+                            // 设置长按定时器(500ms)
+                            this.longPressTimer = setTimeout(() => {
+                                this.isLongPress = true;
+                                this.openSearchModal();
+                                // 震动反馈(如果支持)
+                                if (navigator.vibrate) {
+                                    navigator.vibrate(50);
+                                }
+                            }, 500);
+                        }
+                    },
+
+                    // 长按结束
+                    handleLongPressEnd(event) {
+                        // 清除长按定时器
+                        if (this.longPressTimer) {
+                            clearTimeout(this.longPressTimer);
+                            this.longPressTimer = null;
+                        }
+                    },
+
+                    // 长按取消(手指移出按钮区域)
+                    handleLongPressCancel(event) {
+                        // 清除长按定时器
+                        if (this.longPressTimer) {
+                            clearTimeout(this.longPressTimer);
+                            this.longPressTimer = null;
+                        }
+                        this.isLongPress = false;
+                    },
+
+                    // 打开搜索弹窗
+                    openSearchModal() {
+                        this.showSearchModal = true;
+                        this.searchKeyword = '';
+
+                        // 延迟聚焦输入框,确保DOM已渲染和动画完成
+                        this.$nextTick(() => {
+                            // 使用 setTimeout 确保在移动端也能正常触发键盘
+                            setTimeout(() => {
+                                if (this.$refs.searchInput) {
+                                    // 先点击再聚焦,确保移动端键盘弹出
+                                    this.$refs.searchInput.click();
+                                    this.$refs.searchInput.focus();
+                                    console.log('✅ 输入框已聚焦');
+                                }
+                            }, 100); // 等待动画完成后再聚焦
+                        });
+                    },
+
+                    // 关闭搜索弹窗
+                    closeSearchModal() {
+                        this.showSearchModal = false;
+                        this.searchKeyword = '';
+                    },
+
+                    // 执行搜索
+                    async performSearch() {
+                        const keyword = this.searchKeyword.trim();
+
+                        // 无关键词时,直接关闭弹窗,什么都不做
+                        if (!keyword) {
+                            // this.closeSearchModal();
+                            return;
+                        }
+
+                        console.log('🔍 执行关键词搜索:', keyword);
+
+                        // 关闭搜索弹窗
+                        this.closeSearchModal();
+
+                        // 设置 keyword 到筛选条件中
+                        this.selectedFilters.ssKeyword = keyword;
+
+                        // 调用 API 搜索
+                        await this.applyDynamicFilters();
+                    },
+
+                    // 显示提示
+                    showToast(message, type = 'info') {
+                        console.log(`${type.toUpperCase()}: ${message}`);
+                        // 使用浏览器原生alert,后续可以替换为更好的提示组件
+                        alert(message);
+                    }
+                }
+            })
+        })
+
+    </script>
+</body>
+</html>

+ 440 - 0
page/mp_rcXcdjl_excelZxxzAdd.html

@@ -0,0 +1,440 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1.0, user-scalable=no"
+    />
+    <title>校长巡查</title>
+    <script src="/js/mp_base/base.js"></script>
+  </head>
+  <body>
+    <div id="app" v-cloak>
+      <!-- 加载状态 -->
+      <div v-if="loading" class="loading-container">
+        <div class="loading-spinner"></div>
+        <div class="loading-text">加载中...</div>
+      </div>
+
+      <!-- 校长巡查表单 -->
+      <div v-else class="form-container" style="padding-top: 12px">
+        <!-- 循环遍历巡查点数据 -->
+        <div
+          v-for="(item, index) in xunchaList"
+          :key="item.xcdid"
+          class="xuncha-section"
+        >
+          <div class="section-title">{{ item.xcdmc }}</div>
+          <table class="xuncha-table">
+            <tr>
+              <th>情况描述</th>
+              <td style="position: relative">
+                <ss-input
+                  v-model="formData[`xcd_${index}`].qkms"
+                  :name="`xcd_${index}_qkms`"
+                  placeholder="请输入情况描述"
+                  :required="true"
+                />
+              </td>
+            </tr>
+            <tr>
+              <th>处理描述</th>
+              <td style="position: relative">
+                <ss-input
+                  v-model="formData[`xcd_${index}`].clms"
+                  :name="`xcd_${index}_clms`"
+                  placeholder="请输入处理描述"
+                  :required="true"
+                />
+              </td>
+            </tr>
+            <tr>
+              <th>责任人</th>
+              <td style="position: relative">
+                <ss-input
+                  v-model="formData[`xcd_${index}`].zrr"
+                  :name="`xcd_${index}_zrr`"
+                  placeholder="请输入责任人"
+                  :required="true"
+                />
+              </td>
+            </tr>
+          </table>
+        </div>
+      </div>
+
+      <!-- 使用SsBottom组件 -->
+      <ss-bottom
+        :show-shyj="false"
+        :buttons="bottomButtons"
+        @button-click="handleBottomAction"
+        :disabled="submitting"
+      />
+    </div>
+
+    <script>
+      SS.ready(function () {
+        window.SS.dom.initializeFormApp({
+          el: "#app",
+          data() {
+            return {
+              // 加载状态
+              loading: true,
+              submitting: false,
+
+              // 巡查数据
+              xunchaList: [],
+              formData: {},
+
+              // 页面参数
+              rcid: 521185, // 写死,后续会从小程序传过来
+
+              // 底部按钮
+              bottomButtons: [
+                { text: "取消", action: "cancel" },
+                { text: "保存并提交", action: "submit" },
+              ],
+            };
+          },
+
+          mounted() {
+            // 页面加载时初始化数据
+            this.initPage();
+          },
+
+          methods: {
+            // 初始化页面数据
+            async initPage() {
+              try {
+                // console.log('🔄 开始加载巡查数据...');
+
+                // 获取URL参数
+                const urlParams = this.getUrlParams();
+
+                // 调用初始化接口
+                const response = await request.post(
+                  `/service?ssToken=${urlParams.ssToken}`,
+                  {},
+                  { loading: false }
+                );
+                console.log("✅ 巡查数据加载成功:", response);
+                if (response && response.data.ssData) {
+                  // console.log('✅ 巡查数据加载成功:', response.data.ssData);
+                  // 使用新格式的数据结构
+                  this.initFormData(response.data.ssData.xcdjlList);
+                  // 如果需要,可以使用返回的rcid
+                  if (response.data.ssData.rcid) {
+                    this.rcid = response.data.ssData.rcid;
+                  }
+                }
+              } catch (error) {
+                console.log("❌ 加载巡查数据失败:", error);
+              } finally {
+                this.loading = false;
+              }
+            },
+
+            // 初始化表单数据
+            initFormData(xunchaData) {
+              console.log("🔧 初始化表单数据:", xunchaData);
+
+              // 保存巡查点列表
+              this.xunchaList = xunchaData || [];
+
+              // 初始化表单数据
+              const newFormData = {};
+              this.xunchaList.forEach((item, index) => {
+                const areaKey = `xcd_${index}`;
+                newFormData[areaKey] = {
+                  qkms: item.qkms || "",
+                  clms: item.clms || "",
+                  zrr: item.zrr || "",
+                };
+              });
+
+              this.formData = newFormData;
+              console.log("✅ 表单数据初始化完成:", this.formData);
+
+              // 初始化校验规则
+              this.$nextTick(() => {
+                this.initValidationRules();
+              });
+            },
+
+            // 获取URL参数
+            getUrlParams() {
+              const params = {};
+              const urlSearchParams = new URLSearchParams(
+                window.location.search
+              );
+              for (const [key, value] of urlSearchParams) {
+                params[key] = decodeURIComponent(value);
+              }
+              return params;
+            },
+
+            // 显示提示
+            showToast(message, type = "info") {
+              // 这里可以使用SS框架的提示组件
+              console.log(`${type.toUpperCase()}: ${message}`);
+              // 临时使用alert
+              window.showToast(message);
+            },
+
+            // 初始化ssVm校验规则
+            initValidationRules() {
+              console.log("🔧 初始化ssVm校验规则...");
+
+              // 为每个巡查点添加校验规则
+              this.xunchaList.forEach((item, index) => {
+                const prefix = `xcd_${index}`;
+                const areaName = item.xcdmc;
+
+                // 情况描述校验 - 使用required选项
+                ssVm.add("notNull", [`${prefix}_qkms`], {
+                  msgPrfx: `${areaName}的情况描述`,
+                  required: true,
+                });
+
+                // 处理描述校验
+                ssVm.add("notNull", [`${prefix}_clms`], {
+                  msgPrfx: `${areaName}的处理描述`,
+                  required: true,
+                });
+
+                // 责任人校验
+                ssVm.add("notNull", [`${prefix}_zrr`], {
+                  msgPrfx: `${areaName}的责任人`,
+                  required: true,
+                });
+              });
+
+              console.log("✅ ssVm校验规则初始化完成");
+            },
+
+            // 处理底部按钮点击
+            handleBottomAction(data) {
+              console.log("底部按钮操作:", data);
+
+              switch (data.action) {
+                case "cancel":
+                  this.handleCancel();
+                  break;
+                case "submit":
+                  this.handleSubmit();
+                  break;
+              }
+            },
+
+            // 处理取消操作 - 使用bridge.js的goBack方法
+            handleCancel() {
+              // 使用bridge.js提供的goBack方法
+              NavigationManager.goBack({ refreshParent: true });
+            },
+            // 获取URL参数
+            getUrlParams() {
+              const params = {};
+              const urlSearchParams = new URLSearchParams(
+                window.location.search
+              );
+              for (const [key, value] of urlSearchParams) {
+                params[key] = decodeURIComponent(value);
+              }
+              return params;
+            },
+
+            // 处理提交操作
+            async handleSubmit() {
+              if (this.submitting) return;
+
+              try {
+                this.submitting = true;
+                console.log("📝 开始提交表单...");
+
+                // 校验表单
+                if (!this.validateForm()) {
+                  return;
+                }
+
+                // 构建提交数据(模拟JSP表单格式)
+                const submitData = this.buildSubmitData();
+                console.log("📤 提交数据:", submitData);
+
+                // 调用提交接口
+                const response = await request.post(
+                  "/service?ssServ=rcXcdjl_excelSaveAddByZxxz&ssDest=info",
+                  submitData,
+                  {
+                    loading: false,
+                    formData: true, // 使用表单格式提交,数组会被展开为多个同名参数
+                  }
+                );
+
+                console.log("✅ 提交成功:", response);
+                this.showToast("提交成功!", "success");
+
+                // 延迟返回
+                setTimeout(() => {
+                  this.returnToHome();
+                }, 1500);
+              } catch (error) {
+                console.error("❌ 提交失败:", error);
+                this.showToast("提交失败", "error");
+              } finally {
+                this.submitting = false;
+              }
+            },
+
+            // 校验表单 - 使用项目的ValidationManager
+            validateForm() {
+              console.log("🔍 使用ssVm校验表单数据...");
+
+              // 使用ValidationManager的validateAll方法
+              const result = ssVm.validateAll();
+
+              if (!result.valid) {
+                console.log("❌ 表单校验失败:", result.errors);
+                // ValidationManager会自动显示错误信息
+                return false;
+              }
+
+              console.log("✅ 表单校验通过");
+              return true;
+            },
+
+            // 构建提交数据(JSP格式)
+            buildSubmitData() {
+              const submitData = {
+                // 隐藏字段
+                rcid: this.rcid,
+                requestParentViewObject: "rc",
+                // 巡查点数据 - 使用JSP中的字段名格式
+                xcdid: [],
+                xcdjlid: [],
+                qkms: [],
+                clms: [],
+                zrr: [],
+              };
+
+              // 填充数组数据(按JSP中的格式)
+              this.xunchaList.forEach((item, index) => {
+                submitData.xcdid.push(item.xcdid);
+                submitData.xcdjlid.push(item.xcdjlid || "null");
+                submitData.qkms.push(this.formData[`xcd_${index}`].qkms);
+                submitData.clms.push(this.formData[`xcd_${index}`].clms);
+                submitData.zrr.push(this.formData[`xcd_${index}`].zrr);
+              });
+
+              return submitData;
+            },
+            // 2025-12-08 修改 by xu:提交成功后回到小程序首页
+            returnToHome() {
+                console.log("🏠 退出小程序")
+				wx.miniProgram.reLaunch({
+					url: "/pages/main/index"
+				})
+            },
+
+            // 显示提示
+            showToast(message, type = "info") {
+              console.log(`${type.toUpperCase()}: ${message}`);
+              // 使用浏览器原生alert,后续可以替换为更好的提示组件
+              alert(message);
+            },
+          },
+        });
+      });
+    </script>
+
+    <style>
+      /* 防止Vue模板闪烁 */
+      [v-cloak] {
+        display: none !important;
+      }
+
+      /* 加载状态样式 */
+      .loading-container {
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        justify-content: center;
+        height: 200px;
+        color: #666;
+      }
+
+      .loading-spinner {
+        width: 40px;
+        height: 40px;
+        border: 4px solid #f3f3f3;
+        border-top: 4px solid #40ac6d;
+        border-radius: 50%;
+        animation: spin 1s linear infinite;
+        margin-bottom: 15px;
+      }
+
+      @keyframes spin {
+        0% {
+          transform: rotate(0deg);
+        }
+        100% {
+          transform: rotate(360deg);
+        }
+      }
+
+      .loading-text {
+        font-size: 14px;
+      }
+
+      /* 巡查区域样式 */
+      .xuncha-section {
+        margin-bottom: 20px;
+        /* border: 1px solid #e0e0e0; */
+        /* border-radius: 8px; */
+        overflow: hidden;
+      }
+
+      .section-title {
+        /* background: #f5f5f5; */
+        padding: 12px 0;
+        font-weight: bold;
+        font-size: 16px;
+        color: #333;
+        /* border-bottom: 1px solid #e0e0e0; */
+      }
+
+      .xuncha-table {
+        width: 100%;
+        border-collapse: collapse;
+      }
+
+      .xuncha-table th {
+        background: #fafafa;
+        padding: 12px 15px;
+        text-align: left;
+        font-weight: normal;
+        color: #666;
+        border-bottom: 1px solid #e0e0e0;
+        width: 100px;
+      }
+
+      .xuncha-table td {
+        padding: 12px 15px;
+        border-bottom: 1px solid #e0e0e0;
+      }
+
+      /* .xuncha-table tr:last-child th,
+			.xuncha-table tr:last-child td {
+				border-bottom: none;
+			} */
+
+      /* 表单容器样式 */
+      .form-container {
+        padding: 15px;
+        padding-bottom: 45px;
+        background: #fff;
+        min-height: calc(100vh - 100px);
+      }
+    </style>
+  </body>
+</html>

+ 485 - 0
page/mp_shList.html

@@ -0,0 +1,485 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>审核</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        /* 防止Vue模板闪烁 */
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #edf1f5;
+            min-height: 100vh;
+            display: flex;
+            flex-direction: column;
+        }
+
+        .header-section {
+            position: relative;
+            padding: 10px 0;
+            cursor: pointer;
+            user-select: none;
+            touch-action: none;
+            transition: background-color 0.3s ease;
+        }
+
+        .header-section::after {
+            width: 95%;
+            height: 1px;
+            content: " ";
+            position: absolute;
+            bottom: 0px;
+            left: 50%;
+            transform: translateX(-50%);
+            background: #e2e4ec;
+        }
+
+        .header-section:hover {
+            background: rgba(64, 172, 109, 0.05);
+        }
+
+        .header-section.dragging {
+            background: rgba(64, 172, 109, 0.1);
+        }
+    </style>
+</head>
+
+<body>
+    <div id="app" v-cloak>
+        <div class="sh-list-container">
+            <!-- 头部节点 -->
+            <div class="header-section" @click="handleHeaderClick" @mousedown="handleDragStart"
+                @touchstart="handleDragStart">
+                <ss-verify-node v-if="rightHeader" class="header-node" :item="rightHeader" />
+            </div>
+
+            <!-- 审批链条 -->
+            <div class="verify-section">
+                <ss-verify v-if="rightGroupList && rightGroupList.length > 0" :verify-list="rightGroupList"
+                    class="fit-height-content" @toggle-group="handleToggleGroup" />
+            </div>
+
+            <!-- 空状态 -->
+            <div v-else class="empty-state">
+                <Icon name="icon-kongzhuangtai" size="64" color="#ccc" />
+                <p>暂无审核节点数据</p>
+            </div>
+        </div>
+    </div>
+
+    <script>
+        // 等待SS框架加载完成
+        window.SS.ready(function () {
+            // 使用SS框架的方式创建Vue实例
+            window.SS.dom.initializeFormApp({
+                el: '#app',
+                data() {
+                    return {
+                        
+                        ssObjName:'',
+                        ssObjId:'',
+                        sqid: '',
+                        shid: '',
+                        bdlbm: '',
+                        dataType:'',
+                        encode_shid:'',
+                        pageParams: {},
+                        // 头部节点数据
+                        rightHeader: null,
+                        // 审批链条数据
+                        rightGroupList: [],
+                        loading: false,
+
+                        // 拖拽相关
+                        isDragging: false,
+                        dragStartY: 0,
+                        dragStartTime: 0,
+                        dragTimer: null,
+                        hasMoved: false,
+                        dragStarted: false,
+                    }
+                },
+                mounted() {
+                    // 获取URL参数
+                    this.pageParams = this.getUrlParams()
+                    console.log('🔗 mp_shList页面接收到参数:', this.pageParams)
+
+                    // 如果有sqid参数,调用selSh接口
+                    if (this.pageParams.sqid) {
+                        this.ssObjName = this.pageParams.ssObjName
+                        this.ssObjId = this.pageParams.ssObjId
+                        this.sqid = this.pageParams.sqid
+                        this.shid = this.pageParams.shid
+                        this.bdlbm = this.pageParams.bdlbm
+                        this.dataType = this.pageParams.dataType
+                        this.encode_shid = this.pageParams.encode_shid
+                        this.loadSelSh()
+                    }
+                },
+                methods: {
+                    // 获取URL参数
+                    getUrlParams() {
+                        const params = {}
+                        const urlSearchParams = new URLSearchParams(window.location.search)
+                        for (const [key, value] of urlSearchParams) {
+                            params[key] = decodeURIComponent(value)
+                        }
+                        return params
+                    },
+
+                    // 加载selSh数据
+                    async loadSelSh() {
+                        if (!this.sqid) {
+                            console.warn('⚠️ sqid为空,无法调用selSh接口')
+                            return
+                        }
+
+                        this.loading = true
+
+                        try {
+                            console.log('📋 调用selSh服务获取数据...', this.sqid)
+                            const selShResponse = await request.post(
+                                `/service?ssServ=selSh&sqid=${this.sqid}`,
+                                
+                                { loading: false, formData: true }
+                            )
+                            console.log('✅ selSh响应:', selShResponse)
+
+                            // 处理selSh数据
+                            if (selShResponse && selShResponse.data) {
+                                this.processShListData(selShResponse.data)
+                            }
+
+                        } catch (error) {
+                            console.error('❌ 加载selSh数据失败:', error)
+                        } finally {
+                            this.loading = false
+                        }
+                    },
+
+                    // 处理审核列表数据
+                    processShListData(data) {
+                        console.log('🔄 开始处理审核列表数据:', data)
+
+                        // 构建完整的审核记录列表
+                        const allRecords = []
+
+                        // 1. 添加申请人记录 (来自sq字段)
+                        if (data.sq) {
+                            allRecords.push({
+                                ...data.sq,
+                                isSqry: true // 标记为申请人
+                            })
+                        }
+
+                        // 2. 添加审核人员记录 (来自shList)
+                        if (data.shList && Array.isArray(data.shList)) {
+                            data.shList.forEach(group => {
+                                if (group.ryList && Array.isArray(group.ryList)) {
+                                    group.ryList.forEach(ry => {
+                                        allRecords.push({
+                                            ...ry,
+                                            isSqry: false // 标记为审核人
+                                        })
+                                    })
+                                }
+                            })
+                        }
+
+                        console.log('📋 所有审核记录:', allRecords)
+
+                        // 3. 设置申请人头部信息 (第一条记录)
+                        const sqry = allRecords.find(item => item.isSqry) || {}
+                        this.rightHeader = {
+                            thumb: this.getThumbUrl(sqry.zjzwj),
+                            name: sqry.xm || '',
+                            role: sqry.bmmc || '',
+                            description: sqry.sm || '发起审核',
+                            time: this.formatTime(sqry.shsj),
+                            link: false,
+                        }
+                        console.log('👤 申请人信息:', this.rightHeader)
+
+                        // 4. 按部门分组构建审核链条
+                        const shRecords = allRecords.filter(item => !item.isSqry)
+                        this.rightGroupList = this.groupByDepartment(shRecords)
+                        console.log('📊 审核链条:', this.rightGroupList)
+                    },
+
+                    // 按部门分组
+                    groupByDepartment(records) {
+                        const groups = {}
+
+                        records.forEach(item => {
+                            const groupKey = item.bmmc || '未知部门'
+
+                            if (!groups[groupKey]) {
+                                groups[groupKey] = {
+                                    groupName: groupKey,
+                                    open: true,
+                                    children: []
+                                }
+                            }
+
+                            groups[groupKey].children.push({
+                                thumb: this.getThumbUrl(item.zjzwj),
+                                name: item.xm || '',
+                                role: item.bmmc || '',
+                                description: item.sm || '同意',
+                                time: this.formatTime(item.shsj),
+                                link: false
+                            })
+                        })
+
+                        return Object.values(groups)
+                    },
+
+                    // 获取头像URL
+                    getThumbUrl(path) {
+                        // 如果没有路径,返回默认头像
+                        if (!path) {
+                            return 'skin/easy/image/default-personalPhoto.png'
+                        }
+
+                        // 使用全局的 getImageUrl 方法处理图片路径
+                        if (typeof window.getImageUrl === 'function') {
+                            return window.getImageUrl(path)
+                        }
+
+                        // 如果 getImageUrl 不存在,返回原路径
+                        console.warn('⚠️ getImageUrl 方法不存在')
+                        return path
+                    },
+
+                    // 格式化时间
+                    formatTime(timeStr) {
+                        // 检查时间字符串是否有效
+                        if (!timeStr || timeStr === '' || timeStr === null || timeStr === undefined) {
+                            console.log('⚠️ formatTime: 时间为空')
+                            return ''
+                        }
+
+                        console.log('🕐 formatTime 输入:', timeStr)
+
+                        // 检查 dayjs 是否可用
+                        if (typeof dayjs === 'undefined') {
+                            console.error('❌ dayjs 未加载')
+                            return ''
+                        }
+
+                        try {
+                            // 清理字符串:移除特殊空格字符
+                            const cleanedDateStr = String(timeStr)
+                                .replace(/[\u202F\u00A0]/g, ' ')
+                                .replace(/\s+/g, ' ')
+                                .trim()
+
+                            console.log('清理后的字符串:', cleanedDateStr)
+
+                            // 使用原生 Date 解析
+                            const jsDate = new Date(cleanedDateStr)
+                            if (isNaN(jsDate.getTime())) {
+                                console.warn('⚠️ Date 解析失败')
+                                return ''
+                            }
+
+                            // 转换为 dayjs
+                            const date = dayjs(jsDate)
+                            if (!date.isValid()) {
+                                console.warn('⚠️ dayjs 无效')
+                                return ''
+                            }
+
+                            console.log('✅ dayjs 解析成功')
+
+                            // 格式化: HH:mm   MM/DD
+                            const result = date.format('HH:mm   MM/DD')
+                            console.log('🕐 formatTime 输出:', result)
+
+                            return result
+                        } catch (error) {
+                            console.error('❌ 时间格式化失败:', error, timeStr)
+                            return ''
+                        }
+                    },
+
+                    // 处理分组展开/收起
+                    handleToggleGroup(data) {
+                        console.log('🔄 分组状态变化:', data)
+                    },
+
+                    // ===== 与父页面通信方法 =====
+                    // 发送消息到父页面
+                    sendMessageToParent(type, data = {}) {
+                        try {
+                            // 通过postMessage向父页面发送消息
+                            if (window.parent && window.parent !== window) {
+                                window.parent.postMessage({
+                                    type,
+                                    data
+                                }, '*') // 在生产环境中应该使用具体的origin
+                            }
+                        } catch (error) {
+                            console.error('发送消息到父页面失败:', error)
+                        }
+                    },
+
+                    // 处理header-section点击
+                    handleHeaderClick(event) {
+                        // 如果正在拖拽中,不处理点击
+                        if (this.isDragging) {
+                            return
+                        }
+                        console.log('🖱️ header-section被点击')
+                        this.sendMessageToParent('header-section-click')
+                    },
+
+                    // 处理拖拽开始(长按检测)
+                    handleDragStart(event) {
+                        // 阻止默认行为,防止页面滚动
+                        event.preventDefault()
+
+                        // 只处理鼠标左键或触摸事件
+                        if (event.type === 'mousedown' && event.button !== 0) {
+                            return
+                        }
+
+                        console.log('🔄 开始长按检测')
+
+                        // 记录开始时间和位置
+                        this.dragStartTime = Date.now()
+                        this.dragStartY = event.type === 'mousedown' ? event.clientY : event.touches[0].clientY
+                        this.hasMoved = false
+                        this.dragStarted = false
+
+                        // 添加长按检测
+                        this.dragTimer = setTimeout(() => {
+                            if (!this.hasMoved && !this.dragStarted) {
+                                this.dragStarted = true
+                                console.log('🔄 开始拖拽header-section')
+                                this.isDragging = true
+
+                                // 添加拖拽样式
+                                document.querySelector('.header-section').classList.add('dragging')
+
+                                // 通知父页面开始拖拽
+                                this.sendMessageToParent('header-section-drag-start', {
+                                    startY: this.dragStartY
+                                })
+
+                                // 添加全局事件监听器
+                                if (event.type === 'mousedown') {
+                                    document.addEventListener('mousemove', this.handleDragMove)
+                                    document.addEventListener('mouseup', this.handleDragEnd)
+                                } else {
+                                    document.addEventListener('touchmove', this.handleDragMove, { passive: false })
+                                    document.addEventListener('touchend', this.handleDragEnd)
+                                }
+                            }
+                        }, 500) // 500ms后开始拖拽
+
+                        // 添加临时事件监听器来检测移动
+                        const tempMouseMove = (e) => {
+                            const currentY = e.type === 'mousemove' ? e.clientY : e.touches[0].clientY
+                            const moveDistance = Math.abs(currentY - this.dragStartY)
+
+                            if (moveDistance > 10) { // 移动超过10px就认为不是长按
+                                this.hasMoved = true
+                                clearTimeout(this.dragTimer)
+
+                                // 移除临时监听器
+                                this.removeTempListeners(event.type, tempMouseMove, tempMouseUp)
+                            }
+                        }
+
+                        const tempMouseUp = (e) => {
+                            clearTimeout(this.dragTimer)
+
+                            // 如果没有开始拖拽且有移动,则触发点击事件
+                            if (!this.dragStarted && !this.hasMoved) {
+                                console.log('🖱️ 触发点击事件(mouseup)')
+                                // 延迟触发点击,避免与拖拽冲突
+                                setTimeout(() => {
+                                    this.handleHeaderClick(e)
+                                }, 50)
+                            }
+
+                            // 移除临时监听器
+                            this.removeTempListeners(event.type, tempMouseMove, tempMouseUp)
+                        }
+
+                        // 添加临时事件监听器
+                        if (event.type === 'mousedown') {
+                            document.addEventListener('mousemove', tempMouseMove)
+                            document.addEventListener('mouseup', tempMouseUp)
+                        } else {
+                            document.addEventListener('touchmove', tempMouseMove, { passive: false })
+                            document.addEventListener('touchend', tempMouseUp)
+                        }
+                    },
+
+                    // 移除临时监听器的辅助方法
+                    removeTempListeners(eventType, mouseMoveHandler, mouseUpHandler) {
+                        if (eventType === 'mousedown') {
+                            document.removeEventListener('mousemove', mouseMoveHandler)
+                            document.removeEventListener('mouseup', mouseUpHandler)
+                        } else {
+                            document.removeEventListener('touchmove', mouseMoveHandler)
+                            document.removeEventListener('touchend', mouseUpHandler)
+                        }
+                    },
+
+                    // 处理拖拽移动
+                    handleDragMove(event) {
+                        if (!this.isDragging) return
+
+                        event.preventDefault()
+
+                        const currentY = event.type === 'mousemove'
+                            ? event.clientY
+                            : event.touches[0].clientY
+
+                        const deltaY = currentY - this.dragStartY
+
+                        // 通知父页面拖拽移动
+                        this.sendMessageToParent('header-section-drag-move', {
+                            deltaY
+                        })
+                    },
+
+                    // 处理拖拽结束
+                    handleDragEnd() {
+                        if (!this.isDragging) return
+
+                        console.log('🔚 结束拖拽')
+
+                        // 移除拖拽样式
+                        const headerSection = document.querySelector('.header-section')
+                        if (headerSection) {
+                            headerSection.classList.remove('dragging')
+                        }
+
+                        // 重置状态
+                        this.isDragging = false
+                        this.dragStartY = 0
+
+                        // 移除全局事件监听器
+                        document.removeEventListener('mousemove', this.handleDragMove)
+                        document.removeEventListener('mouseup', this.handleDragEnd)
+                        document.removeEventListener('touchmove', this.handleDragMove)
+                        document.removeEventListener('touchend', this.handleDragEnd)
+
+                        // 通知父页面拖拽结束
+                        this.sendMessageToParent('header-section-drag-end')
+                    },
+                }
+            })
+        })
+    </script>
+</body>

+ 227 - 0
page/mp_xyqj_baseInfo.html

@@ -0,0 +1,227 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>基本信息</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+        }
+
+        .loading {
+            text-align: center;
+            padding: 50px;
+            color: #999;
+        }
+
+        .error {
+            text-align: center;
+            padding: 50px;
+            color: #ff4d4f;
+        }
+
+        .section-title {
+            padding: 10px 12px;
+            color: #666;
+            font-size: 16px;
+            font-weight: 600;
+        }
+
+        .reason-text {
+            white-space: pre-wrap;
+            line-height: 1.6;
+        }
+
+        .attachment-text {
+            color: #999;
+            font-size: 14px;
+        }
+    </style>
+</head>
+<body>
+<div id="app" v-cloak>
+    <div v-if="loading" class="loading">加载中...</div>
+    <div v-else-if="error" class="error">{{ error }}</div>
+    <!-- 新增学员请假基本信息回显页 by xu 2026-02-28 -->
+    <div v-else class="content-div">
+        <table class="form">
+            <tr>
+                <th>请假类别</th>
+                <td>{{ displayData.rclb || '-' }}</td>
+            </tr>
+            <tr>
+                <th>学员</th>
+                <td>{{ displayData.fzrymc || displayData.xm || '-' }}</td>
+            </tr>
+            <tr>
+                <th>请假开始时间</th>
+                <td>{{ displayData.kssj || '-' }}</td>
+            </tr>
+            <tr>
+                <th>请假结束时间</th>
+                <td>{{ displayData.jssj || '-' }}</td>
+            </tr>
+            <tr>
+                <th>事由</th>
+                <td class="reason-text">{{ displayData.ms || '-' }}</td>
+            </tr>
+            <tr>
+                <th>附件</th>
+                <td>
+                    <span class="attachment-text">{{ attachmentText }}</span>
+                </td>
+            </tr>
+        </table>
+    </div>
+</div>
+
+<script>
+    window.SS.ready(function () {
+        window.SS.dom.initializeFormApp({
+            el: '#app',
+            data() {
+                return {
+                    pageParams: {},
+                    loading: false,
+                    error: '',
+                    formData: {},
+                    displayData: {
+                        rclb: '',
+                        fzrymc: '',
+                        xm: '',
+                        kssj: '',
+                        jssj: '',
+                        ms: '',
+                        fjid: ''
+                    }
+                }
+            },
+            computed: {
+                attachmentText() {
+                    if (!this.displayData.fjid) return '暂无附件'
+                    return `已上传(附件ID:${this.displayData.fjid},后续接上传回显)`
+                }
+            },
+            async mounted() {
+                this.pageParams = this.getUrlParams()
+                await this.loadData()
+            },
+            methods: {
+                getUrlParams() {
+                    const params = {}
+                    const urlSearchParams = new URLSearchParams(window.location.search)
+                    for (const [key, value] of urlSearchParams) {
+                        params[key] = decodeURIComponent(value)
+                    }
+                    return params
+                },
+
+                parseParamObject(paramStr) {
+                    if (!paramStr) return {}
+                    try {
+                        return JSON.parse(paramStr)
+                    } catch (err) {
+                        try {
+                            const fixed = paramStr.replace(/([,{]\s*)([a-zA-Z_][a-zA-Z0-9_]*)\s*:/g, '$1"$2":')
+                            return JSON.parse(fixed)
+                        } catch (err2) {
+                            console.error('解析param失败:', err2)
+                            return {}
+                        }
+                    }
+                },
+
+                async loadData() {
+                    // 加载请假详情数据并回显 by xu 2026-02-28
+                    const service = this.pageParams.service
+                    if (!service) {
+                        this.error = '缺少service参数'
+                        return
+                    }
+
+                    this.loading = true
+                    this.error = ''
+
+                    try {
+                        const paramObj = this.parseParamObject(this.pageParams.param)
+                        const response = await request.post(
+                            `/service?ssServ=${service}&ssDest=data`,
+                            paramObj,
+                            { loading: false, formData: true }
+                        )
+
+                        const raw = this.pickLeaveData(response?.data)
+                        if (!raw) {
+                            this.error = '未获取到请假数据'
+                            return
+                        }
+
+                        this.formData = raw
+                        await this.buildDisplayData(raw)
+                    } catch (error) {
+                        console.error('加载请假基本信息失败:', error)
+                        this.error = '加载失败:' + (error.message || '未知错误')
+                    } finally {
+                        this.loading = false
+                    }
+                },
+
+                pickLeaveData(data) {
+                    if (!data) return null
+                    if (data.xyqj) return data.xyqj
+                    if (data.rcqj) return data.rcqj
+                    if (data.qj) return data.qj
+                    if (Array.isArray(data.objectList) && data.objectList.length > 0) return data.objectList[0]
+                    if (Array.isArray(data.data) && data.data.length > 0) return data.data[0]
+                    if (typeof data === 'object') return data
+                    return null
+                },
+
+                async buildDisplayData(raw) {
+                    const cloned = { ...raw }
+                    cloned.kssj = this.formatDate(raw.kssj)
+                    cloned.jssj = this.formatDate(raw.jssj)
+                    cloned.rclb = await this.translateLeaveType(raw.rclbm, raw.rclb)
+                    this.displayData = {
+                        ...this.displayData,
+                        ...cloned
+                    }
+                },
+
+                formatDate(value) {
+                    if (!value) return ''
+                    if (window.H5FieldFormatter && typeof window.H5FieldFormatter.formatDate === 'function') {
+                        return window.H5FieldFormatter.formatDate(value, 'YYYY-MM-DD HH:mm')
+                    }
+                    return value
+                },
+
+                async translateLeaveType(rclbm, fallbackName) {
+                    if (fallbackName) return fallbackName
+                    if (!rclbm) return ''
+                    try {
+                        if (typeof window.getDictOptions === 'function') {
+                            const options = await window.getDictOptions('xyqjlb')
+                            const target = (options || []).find(item => String(item.v) === String(rclbm))
+                            return target ? target.n : rclbm
+                        }
+                        return rclbm
+                    } catch (error) {
+                        console.error('请假类别翻译失败:', error)
+                        return rclbm
+                    }
+                }
+            }
+        })
+    })
+</script>
+</body>
+</html>

+ 217 - 0
page/mp_xyqj_inp.html

@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+    <title>新增请假</title>
+    <script src="/js/mp_base/base.js"></script>
+
+    <style>
+        [v-cloak] {
+            display: none !important;
+        }
+
+        #app {
+            background: #f5f5f5;
+            min-height: 100vh;
+            /* padding: 8px; */
+            box-sizing: border-box;
+
+        }
+
+
+
+
+        .table th {
+            
+            width: 140px;
+            max-width: 170px;
+
+
+        }
+
+        .table td {
+            position: relative;
+        }
+
+        .reason-input {
+            width: 100%;
+        }
+    </style>
+</head>
+<body>
+<div id="app" v-cloak>
+    <!-- 新增请假发起页面 by xu 2026-02-28 -->
+    <div class="form-wrap">
+        <table class="table">
+            <tr>
+                <th>请假类别</th>
+                <td>
+                    <!-- 功能说明:下拉选项改为组件内部按 cb/url 拉取(对齐PC ss-objp),页面不再手写加载方法 by xu 2026-02-28 -->
+                    <ss-select
+                        v-model="formData.rclbm"
+                        cb="xyqjlb"
+                        :auto-select-first="true"
+                        placeholder="请选择"
+                    >
+                    </ss-select>
+                </td>
+            </tr>
+            <tr>
+                <th>学员</th>
+                <td>
+                    <ss-select
+                        v-model="formData.fzryid"
+                        cb="xyByJzBySess"
+                        :auto-select-first="true"
+                        placeholder="请选择"
+                    >
+                    </ss-select>
+                </td>
+            </tr>
+            <tr>
+                <th>请假开始时间</th>
+                <td>
+                    <ss-datetime-picker
+                        v-model="formData.kssj"
+                        mode="datetime"
+                        placeholder="YYYY-MM-DD HH:mm"
+                        @change="onStartTimeChange"
+                    >
+                    </ss-datetime-picker>
+                </td>
+            </tr>
+            <tr>
+                <th>请假结束时间</th>
+                <td>
+                    <ss-datetime-picker
+                        v-model="formData.jssj"
+                        mode="datetime"
+                        :min-date="endMinDate"
+                        placeholder="YYYY-MM-DD HH:mm"
+                    >
+                    </ss-datetime-picker>
+                </td>
+            </tr>
+            <tr>
+                <th>事由</th>
+                <td>
+                    <ss-input
+                        v-model="formData.ms"
+                        name="ms"
+                        class="reason-input"
+                        placeholder="请录入"
+                    />
+                </td>
+            </tr>
+        </table>
+    </div>
+
+</div>
+
+<script>
+    window.SS.ready(function () {
+        window.SS.dom.initializeFormApp({
+            el: '#app',
+            data() {
+                return {
+                    // 初始化请假发起数据 by xu 2026-02-28
+                    pageParams: {},
+                    formData: {
+                        mc: '学员请假',
+                        fzryid: '',
+                        rclbm: '',
+                        kssj: '',
+                        jssj: '',
+                        ms: ''
+                    },
+                    endMinDate: ''
+                }
+            },
+            async mounted() {
+                this.pageParams = this.getUrlParams()
+                this.initDefaultTimeRange()
+
+                // 功能说明:向 mp_objInp 暴露统一取值方法,尽量减少表单页改动 by xu 2026-02-28
+                window.__mpObjInpGetFormData = () => {
+                    const message = this.validateForm()
+                    return {
+                        valid: !message,
+                        message: message || '',
+                        data: {
+                            mc: this.formData.mc,
+                            fzryid: this.formData.fzryid,
+                            rclbm: this.formData.rclbm,
+                            kssj: this.formData.kssj,
+                            jssj: this.formData.jssj,
+                            ms: (this.formData.ms || '').trim()
+                        }
+                    }
+                }
+            },
+
+            beforeUnmount() {
+                // 功能说明:页面卸载时清理桥接方法,避免污染其他表单页 by xu 2026-02-28
+                try {
+                    if (window.__mpObjInpGetFormData) {
+                        delete window.__mpObjInpGetFormData
+                    }
+                } catch (_) {}
+            },
+            methods: {
+                getUrlParams() {
+                    const params = {}
+                    const urlSearchParams = new URLSearchParams(window.location.search)
+                    for (const [key, value] of urlSearchParams) {
+                        params[key] = decodeURIComponent(value)
+                    }
+                    return params
+                },
+
+                initDefaultTimeRange() {
+                    // 设置默认开始/结束时间 by xu 2026-02-28
+                    const now = new Date()
+                    const start = new Date(now.getTime() + 30 * 60 * 1000)
+                    const end = new Date(start.getTime() + 2 * 60 * 60 * 1000)
+
+                    this.formData.kssj = this.formatToDateTime(start)
+                    this.formData.jssj = this.formatToDateTime(end)
+                    this.endMinDate = this.formData.kssj
+                },
+
+                formatToDateTime(date) {
+                    const year = date.getFullYear()
+                    const month = String(date.getMonth() + 1).padStart(2, '0')
+                    const day = String(date.getDate()).padStart(2, '0')
+                    const hour = String(date.getHours()).padStart(2, '0')
+                    const minute = String(date.getMinutes()).padStart(2, '0')
+                    return `${year}-${month}-${day} ${hour}:${minute}`
+                },
+
+                onStartTimeChange(val) {
+                    this.endMinDate = val || ''
+                    if (this.formData.jssj && val && new Date(this.formData.jssj).getTime() < new Date(val).getTime()) {
+                        this.formData.jssj = val
+                    }
+                },
+
+                validateForm() {
+                    if (!this.formData.rclbm) return '请选择请假类别'
+                    if (!this.formData.fzryid) return '请选择学员'
+                    if (!this.formData.kssj) return '请选择请假开始时间'
+                    if (!this.formData.jssj) return '请选择请假结束时间'
+                    // 功能说明:请假“事由/描述”改为前端非必填,仅保留其他必填项校验 by xu 2026-02-28
+
+                    const start = new Date(this.formData.kssj).getTime()
+                    const end = new Date(this.formData.jssj).getTime()
+                    if (!Number.isNaN(start) && !Number.isNaN(end) && end < start) {
+                        return '结束时间不能早于开始时间'
+                    }
+                    return ''
+                }
+            }
+        })
+    })
+</script>
+</body>
+</html>

BIN
skin/.DS_Store


+ 6914 - 0
skin/easy/css/base.css

@@ -0,0 +1,6914 @@
+@import url(var.css);
+
+/* 证件照 */
+.id-photo{
+  width: 153px;
+  height: 213px;
+  background: #e8e8e8;
+  background-size: cover;
+  cursor: pointer;
+  position: relative;
+}
+.id-photo::before {
+  content: "\e605";
+  font-size: 100px;
+  font-family: "iconfont";
+  color: #ccc;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  pointer-events: none; 
+  z-index: 1;  /* 确保图标在内容后面 */
+  /* 其他定位属性 */
+}
+/* 生活照 */
+.life-photo{
+  width: 144px;
+  height: 144px;
+  border-radius: 50%;
+  background:#e8e8e8;
+  background-size: contain;
+  cursor: pointer;
+  position: relative;
+}
+.life-photo::before {
+  content: "\e605";
+  font-size: 80px;
+  font-family: "iconfont";
+  color: #ccc;
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  pointer-events: none; 
+  z-index: 1;
+}
+.life-photo img{
+  border-radius: 50%;
+}
+/* 图标相关开始 */
+/* 通用icon */
+.common-icon{
+  font-size: 22px;
+  font-family: "iconfont";
+
+}
+.common-icon-add::before{
+  content: "\e60d";
+  color: #999999;
+
+}
+.common-icon-setting::before{
+  content: "\e630";
+  /* color: #999999; */
+}
+/* 减少 */
+.common-icon-reduce::before{
+  content: "\e6a2";
+  color: #EB6100;
+}
+/* 附件 */
+.common-icon-paper-clip::before{
+  content: "\e676";
+  color: #999999;
+}
+.common-icon-change::before{
+  font-size: 16px;
+  content: "→";
+  color: red;
+}
+/* 表情 */
+.common-icon-smile::before{
+  content: "\e616";
+  color: #999999;
+}
+/* 常用语 */
+.common-icon-common-words::before{
+  content: "\e69d";
+  color: #999999;
+}
+/* 手写 */
+.common-icon-handwriting::before{
+  content: "\e6ab";
+  color: #999999;
+}
+/* 视频播放 */
+.common-icon-video::before{
+  content: "\e699";
+  color: #999999;
+}
+/* 文件夹-关闭 */
+.common-icon-folder-close::before{
+  content: "\e60b";
+  color: #999999;
+}
+/* 文件夹-打开 */
+.common-icon-folder-open::before{
+  content: "\e60c";
+  color: #999999;
+}
+/* 文件 */
+.common-icon-file::before{
+  content: "\e69e";
+  color: #999999;
+}
+/* 刷新 */
+.common-icon-refresh::before{
+  content: "\e635";
+  color: #999;
+}
+/* 待办 */ 
+.common-icon-todo::before{
+  content: "\e65c";
+  color: #999;
+}
+/* 催办 */
+.common-icon-urge::before{
+  content: "\e664";
+  color: #999;
+}
+/* 快捷发起 */
+.common-icon-quick-start::before{
+  content: "\e6a6";
+  color: #999;
+}
+/* 公告 */
+.common-icon-notice::before{
+  content: "\e6ac";
+  color: #999;
+}
+/* 统计图 */
+.common-icon-chart::before{
+  content: "\e6a5";
+  color: #999;
+}
+/* 请假 */
+.common-icon-leave-big::before{
+  font-size: 36px;
+  content: "\e6af";
+  color: #999999;
+}
+/* 收付款 */
+.common-icon-receipt-big::before{
+  font-size: 36px;
+  content: "\e68e";
+  color: #999999;
+}
+/* 考勤 */
+.common-icon-attendance-big::before{
+  font-size: 36px;
+  content: "\e694";
+  color: #999999;
+}
+/* 会议 */
+.common-icon-meeting::before{
+  content: "\e692";
+  color: #999999;
+}
+/* 一卡通 */
+.common-icon-card::before{
+  content: "\e6a0";
+  color: #999999;
+}
+/* 个人网站 */
+.common-icon-site::before{
+  content: "\e6ae";
+  color: #999999;
+}
+.common-icon-arrow-right{
+  font-size: 14px;
+}
+.common-icon-arrow-right::before{
+  content: "\e640";
+}
+.common-icon-arrow-top::before{
+  content: "\e63f";
+}
+.common-icon-arrow-bottom:before {
+  content: "\e63e";
+}
+.common-icon-arrow-bottom:before {
+  content: "\e63e";
+}
+.common-icon-paixu:before {
+  content: "\e6e7";
+}
+.common-icon-jiaoshi:before {
+  content: "\e6e6";
+}
+.common-icon-renyuan2:before {
+  content: "\e6aa";
+}
+.common-icon-xueyuan:before {
+  content: "\e623";
+}
+
+/* 通用icon结束 */
+/* 全局左侧导航开始 */
+.nav-icon {
+  font-size: 22px;
+  font-family: "iconfont";
+  padding: 10px;
+  color: #565d6d;
+}
+.nav-icon-home::before {
+  content: "\e690";
+}
+.nav-icon-person::before {
+  content: "\e605";
+}
+.nav-icon-knowledge::before {
+  content: "\e62c";
+}
+.nav-icon-statistics::before {
+  content: "\e68a";
+}
+.nav-icon-category::before {
+  content: "\e652";
+}
+.nav-icon-add::before {
+  content: "\e60d";
+}
+.nav-icon-search::before {
+  content: "\e62f";
+}
+.nav-icon-folder-close::before{
+  content: "\e60b";
+  font-size: 19px;
+}
+.nav-icon-folder-open::before{
+  content: "\e60c";
+  font-size: 19px;
+}
+/* 岗位 */
+.nav-icon-post::before{
+  content: "\e69c";
+}
+/* 单位 */
+.nav-icon-unit::before{
+  content: "\e662";
+}
+/* 全局左侧导航结束 */
+/* 顶部导航开始 */
+.header-icon {
+  /* 基础样式 */
+  width: 36px;
+  height: 36px;
+  font-size: 22px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-family: "iconfont";
+  border-radius: 4px;
+  border: 1px solid var(--icon-item-border-color, transparent);
+}
+
+.header-icon:hover {
+  --icon-item-border-color: #edf1f5;
+}
+
+.header-help::after {
+  content: "\e6a7";
+  /* 帮助图标 */
+}
+
+.header-setting::after {
+  content: "\e60e";
+  /* 设置图标 */
+}
+
+.header-save::after {
+  content: "\e68d";
+  /* 设置图标 */
+}
+
+.header-menu::after {
+  content: "\e6ad";
+  /* 菜单图标 */
+}
+
+.header-home::after {
+  content: "\e600";
+  /* 主页图标 */
+}
+.header-skin::after {
+  content: "\e689";
+  /* 换肤图标 */
+}
+
+.header-lock::after {
+  content: "\e69f";
+  /* 锁定图标 */
+}
+
+.header-logout {
+  width: 48px;
+  height: 48px;
+  font-size: 33px;
+}
+
+.header-logout:hover {
+  background-color: #000;
+}
+
+.header-logout::after {
+  content: "\e6a1";
+  /* 退出图标 */
+}
+
+/* 顶部导航结束 */
+/* 全局菜单图标开始 */
+.global-menu-icon {
+  font-size: 16px;
+  font-family: "iconfont";
+  /* padding: 10px; */
+}
+/* 全局菜单图标结束 */
+/* 全局查询列表卡片 */
+.cart-list-icon {
+  font-size: 22px;
+  font-family: "iconfont";
+  /* padding: 10px; */
+}
+.cart-list-setting::before {
+  content: "\e630";
+  color: #dddddd;
+}
+
+/* 弹窗底部按钮icon */
+.bottom-div-icon{
+  font-size: 20px;
+  font-family: "iconfont";
+}
+.bottom-div-save::before{
+  content: "\e68d";
+}
+.bottom-div-close{
+  display: flex;
+  align-items: center;
+}
+.bottom-div-close::before{
+  content: "\e693";
+  font-size: 16px;
+}
+.bottom-div-back::before{
+  content: "\e638";
+}
+/* 表单组件icon开始 */
+.form-icon{
+  font-size: 18px;
+  font-family: "iconfont";
+}
+
+.form-icon-select::before{
+  content: "\e63e";
+}
+.form-icon-transform-select::before{
+  content: "\e63f";
+}
+.form-icon-select-checked::before{
+  content: "\e68d";
+}
+
+.form-icon-onoffbutton-checked::before{
+  content: "\e62d";
+}
+.form-icon-onoffbutton-unchecked::before{
+  content: "\e62b";
+}
+.form-icon-time::before{
+  content: "\e606";
+}
+.form-icon-prev-month::before{
+  content: "\e641";
+}
+.form-icon-prev-year::before{
+  content: "\e6b1";
+}
+.form-icon-next-month::before{
+  content: "\e640";
+}
+.form-icon-next-year::before{
+  content: "\e6b2";
+}
+/* 表单组件icon结束 */
+/* 登录页icon开始 */
+.login-icon{
+  font-size: 22px;
+  font-family: "iconfont";
+}
+.login-icon-user::before{
+  content: "\e605";
+}
+.login-icon-password::before{
+  content: "\e607";
+}
+.login-icon-check-code::before{
+  content: "\e608";
+}
+.login-icon-time::before{
+  content: "\e606";
+}
+.login-icon-loginin::before{
+  content: "\e6a1";
+}
+.login-icon-yaoyiyao::before{
+  content: "\e60f";
+}
+.login-icon-change-user{
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 22px !important;
+  height: 22px;
+  margin-right: 5px;
+  margin-top: 5px;
+  cursor: pointer;
+}
+.login-icon-change-user::before{
+  content: "\e609";
+}
+/* 登录页icon结束 */
+/* 弹窗icon开始 */
+.dialog-icon{
+  font-size: 22px;
+  font-family: "iconfont";
+}
+/* 关闭 */
+.dialog-icon-close::before{
+  content: "\e693";
+  font-size: 28px;
+}
+/* 下载 */
+.dialog-icon-download::before{
+  content: "\e691";
+}
+/* 打印 */
+.dialog-icon-print::before{
+  content: "\e66f";
+}
+/* 设置 */
+.dialog-icon-setting::before{
+  content: "\e630";
+}
+/* 收藏 */
+.dialog-icon-collect::before{
+  content: "\e698";
+}
+/* 帮助 */
+.dialog-icon-help::before{
+  content: "\e6cd";
+}
+/* 全屏 */
+.dialog-icon-full-screen::before{
+  content: "\e6ca";
+}
+/* 锁屏 */
+.dialog-icon-lock::before{
+  content: "\e697";
+}
+
+/* editor相关图标开始 */
+.editor-icon{
+  font-size: 16px;
+  font-family: "iconfont";
+  color: #666666;
+}
+/* 加粗 */
+.editor-icon-bold::before{
+  content: "\e68b";
+}
+/* 斜体 */
+.editor-icon-italic::before{
+  content: "\e66b";
+}
+/* 下划线 */
+.editor-icon-underline::before{
+  content: "\e685";
+}
+/* 删除线 */
+.editor-icon-strikethrough::before{
+  content: "\e66e";
+}
+
+/* 加粗 */
+.editor-icon-bold::before{
+  content: "\e68b";
+}
+/* 斜体 */
+.editor-icon-italic::before{
+  content: "\e66b";
+}
+/* 下划线 */
+.editor-icon-underline::before{
+  content: "\e685";
+}
+/* 删除线 */
+.editor-icon-strikethrough::before{
+  content: "\e66e";
+}
+
+/* 清除格式 */
+.editor-icon-eraser::before{
+  content: "\e6c2";
+}
+/* 复制格式 */
+.editor-icon-copyformat::before{
+  content: "\e68b";
+}
+/* 字体 */
+.editor-icon-font::before{
+  content: "\e680";
+}
+/* 字号 */
+.editor-icon-fontsize::before{
+  content: "\e67d";
+}
+/* 文字颜色 */
+.editor-icon-brush::before{
+  content: "\e681";
+}
+/* 段落 */
+.editor-icon-paragraph::before{
+  content: "\e682";
+}
+/* 左对齐 */
+.editor-icon-align-left::before{
+  content: "\e67e";
+}
+/* 居中对齐 */
+.editor-icon-align-center::before{
+  content: "\e67b";
+}
+/* 右对齐 */
+.editor-icon-align-right::before{
+  content: "\e67f";
+}
+/* 两端对齐 */
+.editor-icon-align-justify::before{
+  content: "\e67c";
+}
+/* 无序列表 */
+.editor-icon-ul::before{
+  content: "\e6c3";
+}
+/* 有序列表 */
+.editor-icon-ol::before{
+  content: "\e6c7";
+}
+/* 增加缩进 */
+.editor-icon-indent::before{
+  content: "\e6c8";
+}
+/* 减少缩进 */
+.editor-icon-outdent::before{
+  content: "\e6c9";
+}
+/* 图片 */
+.editor-icon-image::before{
+  content: "\e683";
+}
+/* 文件 */
+.editor-icon-file::before{
+  content: "\e668";
+}
+/* 视频 */
+.editor-icon-video::before{
+  content: "\e699";
+}
+/* 表格 */
+.editor-icon-table::before{
+  content: "\e677";
+}
+/* 链接 */
+.editor-icon-link::before{
+  content: "\e676";
+}
+/* 源代码 */
+.editor-icon-source::before{
+  content: "\e68b";
+}
+/* 预览 */
+.editor-icon-preview::before{
+  content: "\e642";
+}
+/* 全屏 */
+.editor-icon-fullsize::before{
+  content: "\e6c4";
+}
+/* 退出全屏 */
+.editor-icon-fullsize-exit::before{
+  content: "\e6cb";
+}
+/* 打印 */
+.editor-icon-print::before{
+  content: "\e66f";
+}
+/* 撤销 */
+.editor-icon-undo::before{
+  content: "\e688";
+}
+/* 重做 */
+.editor-icon-redo::before{
+  content: "\e686";
+}
+/* 查找 */
+.editor-icon-find::before{
+  content: "\e62f";
+}
+/* 全选 */
+.editor-icon-selectall::before{
+  content: "\e68b";
+}
+/* 隐藏原有的 SVG */
+.jodit-toolbar-button__trigger svg {
+  display: none;
+}
+
+/* 添加我们自己的图标 */
+.jodit-toolbar-button__trigger::after {
+  content: "\e63e";  
+  font-family: "iconfont" !important; 
+  font-size: 12px;
+}
+.jodit-toolbar-button_with-trigger_true{
+  margin-right: 4px !important;
+}
+.jodit-toolbar-button_with-trigger_true>.jodit-toolbar-button__button{
+  width: 20px !important;
+  min-width: 20px !important;
+}
+/* editor相关图标结束 */
+/* 弹窗icon结束 */
+/* 图标相关结束 */
+.self-block {
+  width: 100%;
+  height: 100%;
+}
+
+body * {
+  box-sizing: border-box;
+}
+
+html,
+body {
+  margin: 0;
+  padding: 0;
+}
+
+html,
+body,
+#app {
+  width: 100%;
+  height: 100%;
+  /* min-width: 1024px; */
+}
+
+i{
+  font-style: normal;
+}
+
+input::placeholder ,textarea::placeholder{
+  color: #999;
+}
+
+/*定义滚动条高宽及背景
+ 高宽分别对应横竖滚动条的尺寸*/
+::-webkit-scrollbar {
+  width: 8px;
+  /* background-color: #F5F5F5; */
+  border-radius: 4px;
+  margin: 0 6px;
+}
+
+/*定义滚动条轨道
+  内阴影+圆角*/
+::-webkit-scrollbar-track {
+  border-radius: 4px;
+  background-color: #F5F5F5;
+}
+
+/*定义滑块
+  内阴影+圆角*/
+::-webkit-scrollbar-thumb {
+  border-radius: 4px;
+  background-color: #c1c1c1;
+}
+
+
+/* 页面布局 */
+.layout-container {
+  --header-height: 70px;
+  --left-side-width: 60px;
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+}
+
+.layout-container .header {
+  height: var(--header-height);
+  width: 100%;
+  box-sizing: border-box;
+}
+
+.layout-container .content-area {
+  width: 100%;
+  height: calc(100% - var(--header-height));
+  padding-right: 6px;
+}
+
+.layout-container .content-area:after {
+  content: "";
+  display: block;
+  width: 0;
+  height: 0;
+  clear: both;
+}
+
+.layout-container .left-side {
+  float: left;
+  height: 100%;
+  width: var(--left-side-width);
+  position: relative;
+}
+
+.layout-container .left-side-container {
+  --left-size-width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: var(--left-size-width);
+  transition: width 0.2s;
+}
+
+.layout-container .left-side-container[size="max"] {
+  --left-size-width: 230px;
+}
+
+.layout-container .btn-size {
+  position: absolute;
+  bottom: 0;
+  left: calc(100% + 1px);
+  width: 16px;
+  height: 18px;
+  background: #fff;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  z-index: 100;
+  --border-color: #dddfe6;
+  border-bottom: 1px solid var(--border-color);
+  border-right: 1px solid var(--border-color);
+}
+
+.layout-container .btn-size::before {
+  content: "";
+  position: absolute;
+  left: -7px;
+  top: -22px;
+  width: 22px;
+  height: 22px;
+  background: #fff;
+  transform-origin: 100% 100%;
+  transform: rotate(-45deg);
+  z-index: 1;
+  border-right: 1px solid var(--border-color);
+}
+
+.layout-container .btn-size .icon-container {
+  position: absolute;
+  z-index: 2;
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  padding: 0 0 1px 1px;
+}
+
+.layout-container .left-side-content {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  overflow: hidden;
+  background: #fafbfe;
+  
+  /* display: flex;
+  flex-direction: column;
+  justify-content: flex-start;
+  align-items: center; */
+
+}
+
+.layout-container .left-side-content:hover {
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.layout-container .left-side-content:after {
+  content: "";
+  height: 100%;
+  width: 1px;
+  position: absolute;
+  left: 100%;
+  top: 0;
+  background: #dadee2;
+  box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.24);
+}
+
+.layout-container  .main-content {
+  float: right;
+  height: 100%;
+  width: calc(100% - var(--left-side-width));
+}
+
+.layout-container[sys-mode="edit"] {
+  --edit-box-tools-display: block;
+}
+
+.layout-container[sys-mode="edit"] .can-resize-box:hover {
+  --block-title-visible: hidden;
+}
+
+.flex-start-center {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.flex-between-center {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+/* 公共头部区域开始 */
+.global-header-container {
+  background: var(--lightdark);
+  color: var(--white);
+  height: 100%
+}
+
+.global-header-container>div {
+  height: 100%;
+}
+
+.global-header-container .logo {
+  height: 100%;
+  width: 183px;
+  cursor: pointer;
+  position: relative;
+  z-index: 200;
+}
+
+.global-header-container .logo::after {
+  position: absolute;
+  content: "";
+  width: 0;
+  height: 50%;
+  top: calc(50% - 50% / 2);
+  right: 0;
+  border-right: 1px solid var(--lightdark-sub);
+}
+
+.global-header-container .logo:hover {
+  --header-menu-visible: visible;
+}
+
+.global-header-container .logo .img {
+  padding: 12px 30px 12px calc(var(--space-size) / 2);
+  height: 100%;
+  width: 100%;
+}
+
+.global-header-container .img img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: cover;
+  object-fit: contain;
+}
+
+.global-header-container .menu {
+  visibility: var(--header-menu-visible, hidden);
+  transition: top 0.3s;
+  font-size: 14px;
+  color: #333;
+  top: 100%;
+  left: 0;
+  position: absolute;
+  /* width: 100%; */
+  background: #393d51;
+  border-top: 1px solid #dddfe6;
+  padding: 10px 2px;
+  border-bottom-right-radius: 4px;
+  border-bottom-left-radius: 4px;
+}
+
+.global-header-container .menu>div {
+  height: 36px;
+  color: var(--header-menu-color, #fff);
+  background: var(--header-menu-bg, transparent);
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  padding-left: 8px;
+  padding-right: 8px;
+  gap: 10px;
+  white-space: nowrap;
+}
+
+.global-header-container .menu>div:hover {
+  --header-menu-color: #333;
+  --header-menu-bg: #fffdfd;
+}
+
+.global-header-container .bread-crumb {
+  padding-left: 30px;
+  padding-top: 30px;
+  font-size: 18px;
+  color: var(--white);
+}
+
+.global-header-container.bread-crumb .content {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.global-header-container.bread-crumb .home-icon {
+  margin-right: calc(var(--space-size) / 2);
+  margin-top: -2px;
+}
+
+.global-header-container.bread-crumb .split-icon {
+  margin: 0 var(--space-size);
+}
+
+.global-header-container.bread-crumb .content a {
+  color: inherit;
+  text-decoration: none;
+}
+
+.global-header-container .menu-area {
+  padding-top: 10px;
+  padding-right: var(--space-size);
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 12px;
+}
+
+.global-header-container .search-area {
+  padding: 0 1em;
+  width: 350px;
+  height: 40px;
+}
+
+
+/* 顶部菜单全局搜索框 */
+.search-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  border: none;
+  border-radius: 5px;
+  overflow: hidden;
+  --search-input-font-size: 16px;
+  --search-bg: var(--white, #fff);
+  --search-color: var(--dark, #252835);
+  --input-placeholder-color: #666;
+  background: var(--search-bg);
+  color: var(--search-color);
+}
+
+.search-container.dark {
+  --search-bg: var(--dark, #252835);
+  --search-color: var(--white, #fff);
+  --input-placeholder-color: #9c9fa7;
+}
+
+.search-container input {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  z-index: 1;
+  color: inherit;
+  padding: 0 1em;
+  border: unset;
+  font-size: var(--search-input-font-size);
+  background: unset;
+}
+
+.search-container input:disabled {
+  cursor: pointer;
+}
+
+.search-container input:focus {
+  outline: unset;
+}
+
+.search-container input::-moz-placeholder {
+  color: var(--input-placeholder-color, #666);
+}
+
+.search-container input::placeholder {
+  color: var(--input-placeholder-color, #666);
+}
+
+.search-container .icon-container {
+  right: 15px;
+  top: 50%;
+  transform: translateY(-50%);
+  position: absolute;
+  z-index: 2;
+  pointer-events: none;
+  padding-left: 10px;
+  background: var(--search-bg);
+}
+
+/* 公共头部区域结束 */
+
+/* 全局左部边框开始 */
+.left-side-container {
+  width: 100%;
+  height: 100%;
+  padding: 0;
+  background: #fafbfe;
+  /* overflow-y: auto;
+  overflow-x: hidden; */
+}
+/* 菜单项小圆点 */
+.menu-item-dot {
+  width: 5px;
+  height: 5px;
+  border-radius: 50%;
+  background-color: #8e8e8e;
+  position: absolute;
+  right: 5px;
+  /* 调整右侧位置 */
+  bottom: 5px;
+  /* 调整底部位置 */
+  font-size: 16px;
+  /* 调整圆点大小 */
+}
+
+.left-side-container .menu-item {
+  height: 60px;
+  width: 100%;
+  box-sizing: border-box;
+  color: var(--lightdark);
+  cursor: pointer;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.left-side-container .add-menu-btn {
+  display: var(--menu-add-btn-display, none);
+}
+
+.left-side-container .add {
+  width: 45px;
+  height: 45px;
+  border-radius: 5px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  box-shadow: 0 1px 4px #ccc;
+}
+
+.left-side-container .menu-item-content {
+  width: 100%;
+  height: 100%;
+  padding: 0 8px;
+  gap: 5px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  position: relative;
+  --edit-mark-display: none;
+}
+
+.left-side-container .menu-item-content:hover {
+  --edit-mark-display: var(--menu-edit-mark-display, none);
+}
+
+.left-side-container .menu-item-label {
+  height: 1.5em;
+  overflow: hidden;
+  transition: all 0.5s;
+  font-size: 18px;
+  color: #565d6d;
+  opacity: var(--menu-item-label-opacity, 0);
+  visibility: var(--menu-item-label-visible, hidden);
+}
+
+.left-side-container .edit-mark {
+  display: var(--edit-mark-display);
+  right: -1px;
+  top: -1px;
+  position: absolute;
+  color: #c3c6ca;
+}
+
+.left-side-container .edit-mark:hover {
+  color: #333;
+}
+
+.left-side-container .menu-item-content:hover {
+  background: #edf1f5;
+}
+
+.left-side-container .menu-item-content:hover.edit .mark {
+  display: block;
+}
+
+.left-side-container .icon-container {
+  position: relative;
+  padding: 10px;
+  box-sizing: content-box;
+  width: 24px;
+  height: 24px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  flex-shrink: 0;
+}
+.left-side-container .active{
+  background: #edf1f5;
+}
+/* 全局左部边框结束 */
+
+/* 一级页面搜索开始 */
+.search-bar-contaienr {
+  height: 100%;
+  width: 100%;
+  padding: 0 20px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 20px;
+  --col-lg-3: 270px;
+  position: relative;
+
+}
+
+.search-bar-contaienr input {
+  padding: 8px 10px;
+  margin: 0;
+  outline: unset;
+  border: 1px solid #c4c7cc;
+  border-radius: 3px;
+  font-size: 14px;
+}
+
+
+
+.search-bar-contaienr button {
+  font-size: 14px;
+  outline: unset;
+  border: unset;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  /* gap: 5px; */
+  padding: 8px 20px;
+  border-radius: 3px;
+  margin: 0;
+  cursor: pointer;
+  border: 1px solid #c4c7cc;
+}
+
+.search-bar-contaienr button:hover {
+  background: #393d51;
+  border: 1px solid #393d51;
+  color: #fff;
+}
+
+.search-bar-contaienr .add {
+  position: relative;
+}
+
+.search-bar-contaienr .popup {
+  position: absolute;
+  top: calc(100% + 15px);
+  right: 0;
+  width: 100%;
+  height: 130px;
+  background-color: #393d51;
+  border-radius: 5px;
+  padding: 10px 0;
+  z-index: 10;
+}
+
+.search-bar-contaienr .popup::after {
+  content: "";
+  width: 1em;
+  height: 1em;
+  background: #393d51;
+  position: absolute;
+  right: 1.5em;
+  top: -0.5em;
+  transform: rotate(45deg);
+}
+.search-bar-contaienr .popup >div {
+  border-bottom: 1px solid #303445;
+}
+.search-bar-contaienr .popup >div:last-child {
+  border-bottom: unset;
+}
+
+.search-bar-contaienr button.add>div.popup>div {
+  color: #d8d8d8;
+  font-size: 14px;
+  padding: 8px 20px;
+  cursor: pointer;
+}
+
+.search-bar-contaienr button.add>div.popup>div:not(:last-child) {
+  border-bottom: 1px solid #303445;
+}
+
+.search-bar-contaienr button.add>div.popup>div:hover {
+  background: #fffdfd;
+  color: #333;
+}
+
+
+.search-bar-contaienr>div {
+  height: 32px;
+}
+
+.search-bar-contaienr>.input {
+  border: 1px solid #c4c7cc;
+  border-radius: 3px;
+  font-size: 14px;
+}
+
+.search-bar-contaienr>div.input>input {
+  height: 100%;
+  width: 100%;
+  padding: 8px 10px;
+  margin: 0;
+  outline: unset;
+  border: unset;
+  border-radius: inherit;
+  background: unset;
+  font-size: inherit;
+}
+
+
+
+.search-bar-contaienr button {
+  height: 32px;
+  font-size: 14px;
+  outline: unset;
+  border: unset;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  /* gap: 5px; */
+  padding: 0 20px;
+  border-radius: 3px;
+  margin: 0;
+  cursor: pointer;
+  background: var(--btn-bg, unset);
+  color: var(--btn-color, #999999);
+  border: 1px solid var(--btn-border-color, #c4c7cc);
+}
+
+.search-bar-contaienr button:hover {
+  --btn-bg: #393d51;
+  --btn-color: #fff;
+}
+
+.search-bar-contaienr .input {
+  width: var(--col-lg-3);
+  height: 32px;
+  /* border: 1px solid #c4c7cc; */
+  border-radius: 4px;
+  position: relative;
+}
+
+.search-bar-contaienr .input>input {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 1;
+  outline: unset;
+  border: unset;
+  background: unset;
+  padding: 0 15px;
+  line-height: 32px;
+  font-size: 14px;
+}
+
+.search-bar-contaienr .input>input::-moz-placeholder {
+  color: #999;
+}
+
+.search-bar-contaienr .input>input::placeholder {
+  color: #999;
+}
+
+.search-bar-contaienr .bar {
+  padding: 0 10px;
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  color: #c4c7cc;
+  gap: 12px;
+}
+
+.search-bar-contaienr .bar .icon-container {
+  position: relative;
+  display: block;
+}
+
+.search-bar-contaienr .bar .icon-container:not(:last-child)::after {
+  content: "";
+  position: absolute;
+  top: 50%;
+  left: calc(100% + 12px);
+  transform: translateY(-50%);
+  width: 0;
+  height: 20px;
+  border-right: 1px solid #c4c7cc;
+}
+
+
+
+.search-bar-contaienr button {
+  font-size: 16px;
+  outline: unset;
+  border: unset;
+  color: #999;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  /* gap: 5px; */
+  padding: 0 20px;
+  border-radius: 4px;
+  margin: 0;
+  /* line-height: 2em; */
+  cursor: pointer;
+  background: var(--btn-bg, unset);
+  color: var(--btn-color, #999999);
+  border: 1px solid var(--btn-border-color, #c4c7cc);
+}
+
+.search-bar-contaienr button:hover {
+  --btn-bg: #393d51;
+  --btn-color: #fff;
+}
+.search-bar-contaienr .popup-win.bottom{
+  top: 100% !important;
+}
+.search-bar-contaienr .select-container .suffix>div{
+  width: 30px !important;
+  height: 30px !important;
+}
+/* 一级页面搜索结束 */
+
+/* 知识库卡片开始 */
+.knowledge-item-container {
+  width: 573px;
+  /* height: 220px; */
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: #fafbfe;
+  border-radius: 4px;
+  position: relative;
+  cursor: pointer;
+  border: 1px solid #dddfe6;
+  --header-display: none;
+  --header-color: #999;
+}
+
+.knowledge-item-container:hover {
+  --header-display: flex;
+  box-shadow: 8px 8px 0px #3a3e513d;
+}
+
+.knowledge-item-container:hover .cart-list-setting{
+  display: block;
+}
+/* .knowledge-item-container:hover::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #dddfe6;
+} */
+.knowledge-item-container.active{
+  box-shadow: 8px 8px 0px #3a3e513d;
+}
+.knowledge-item-container.active .header{
+  border-radius: unset;
+}
+/* .knowledge-item-container.active::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #585e6e;
+} */
+
+.knowledge-item-container .active {
+  border: 1px solid #ddd;
+}
+
+.knowledge-item-container .action-bar {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 0;
+  right: 0;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.knowledge-item-container>div.action-bar:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+
+.knowledge-item-container .header {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 0;
+  right: 0;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.knowledge-item-container .header:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+.knowledge-item-container .header:hover .cart-list-setting::before{
+  color: #ffffff;
+}
+.knowledge-item-container .body {
+  padding: 20px 25px;
+}
+
+.knowledge-item-container .box-header>div:first-child {
+  color: #333333;
+  font-size: 20px;
+  line-height: 2em;
+}
+
+.knowledge-item-container .box-body {
+  /* height: 140px; */
+  color: #333;
+  padding: 10px 0;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: flex-end;
+  --right-padding-left: 20px;
+  --right-width: calc(100% - 86px);
+}
+
+.knowledge-item-container .no-thumb {
+  --right-padding-left: 0;
+  --right-width: 100%;
+}
+
+.knowledge-item-container .left {
+  width: 88px !important;
+  height: 121px !important;
+  border: 1px solid #f6f6f6;
+}
+
+.knowledge-item-container .left>img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: cover;
+  object-fit: cover;
+  display: block;
+}
+
+.knowledge-item-container .right {
+  width: var(--right-width);
+  padding-left: var(--right-padding-left);
+  padding-top: 5px;
+}
+
+.knowledge-item-container .right .title {
+  font-size: 16px;
+  color: #666666;
+  line-height: 2em;
+  /* padding-right: 1em; */
+  line-height: 24px;
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+.knowledge-item-container  .desc {
+  margin-top: 10px;
+  font-size: 14px;
+  color: #999999;
+  line-height: 1.8em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  gap: 20px;
+}
+/* 知识库卡片结束 */
+/* 知识库文件夹开始 */
+.ss-folder-list-trapezoid {
+  width: 100px;
+  height: 0;
+  /* border-left: 25px solid transparent; */
+  border-right: 30px solid transparent;
+  border-bottom: 20px solid #fafbfe;
+  border-radius: 4px 0 0 0;
+  position: absolute;
+  top: 0px;
+  left: 0;
+  z-index: 11;
+}
+.ss-folder-list-top-transparent {
+  width: 100%;
+  height: 10px;
+  position: absolute;
+  top: 0;
+  left: 0;
+  background: transparent;
+  border-radius: 4px 4px 0 0;
+  z-index: 1;
+}
+.ss-folder-list-top {
+  width: 100%;
+  height: 10px;
+  position: absolute;
+  top: 10px;
+  left: 0;
+  background: #dcdee5;
+  border-radius: 4px 4px 0 0;
+}
+.ss-folder-list-right {
+  width: 8px;
+  height: calc(100% - 10px);
+  position: absolute;
+  bottom: 0;
+  right: 0px;
+  background: #dcdee5;
+  border-radius: 0 4px 4px 0;
+}
+
+.ss-folder-list {
+  width: 573px;
+  /* height: 220px; */
+  box-shadow: -3px 6px 5px #a1a3a6;
+  background: transparent;
+  border-radius: 4px;
+  position: relative;
+  cursor: pointer;
+  /* border: 1px solid #dddfe6; */
+  --header-display: none;
+  --header-color: #999;
+}
+
+.ss-folder-list:hover {
+  --header-display: flex;
+  box-shadow: 8px 8px 0px #3a3e513d;
+}
+.ss-folder-list:hover
+  .ss-folder-list-top-transparent {
+  background: #dcdee5;
+  border-radius: unset;
+}
+.ss-folder-list:hover .ss-folder-list-top,
+.ss-folder-list:hover .ss-folder-list-right {
+  border-radius: unset;
+}
+.ss-folder-list:hover .cart-list-setting {
+  display: block;
+}
+/* .ss-folder-list:hover::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #dcdee5;
+} */
+.ss-folder-list.active{
+  box-shadow: 8px 8px 0px #3a3e513d;
+}
+.ss-folder-list.active .header {
+  border-radius: unset;
+}
+.ss-folder-list.active
+  .ss-folder-list-top-transparent {
+  background: #585e6e;
+  border-radius: unset;
+}
+/* .ss-folder-list.active::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #585e6e;
+} */
+
+.ss-folder-list .active {
+  border: 1px solid #ddd;
+}
+
+.ss-folder-list .action-bar {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 0;
+  right: 0;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.ss-folder-list > div.action-bar:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+
+.ss-folder-list .header {
+  background-color: #fafbfe;
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 20px;
+  right: 8px;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.ss-folder-list .header:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+.ss-folder-list .header:hover .cart-list-setting::before {
+  color: #ffffff;
+}
+.ss-folder-list .body {
+  margin: 20px 0 0 0;
+  height: calc(100% - 20px);
+  background: #fafbfe;
+}
+
+.ss-folder-list .box-header > div:first-child {
+  color: #333333;
+  font-size: 20px;
+  line-height: 2em;
+}
+.ss-folder-list .box-header,
+.ss-folder-list .box-body {
+  background-color: #fafbfe;
+  padding: 0 25px;
+}
+.ss-folder-list .box-body {
+  /* height: 140px; */
+  color: #333;
+  padding: 10px 25px ;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: flex-end;
+  --right-padding-left: 20px;
+  --right-width: calc(100% - 210px);
+}
+
+.ss-folder-list .no-thumb {
+  --right-padding-left: 0;
+  --right-width: 100%;
+}
+
+.ss-folder-list .left {
+  width: 215px !important;
+  height: 121px !important;
+  border: 1px solid #f6f6f6;
+}
+
+.ss-folder-list .left > img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: cover;
+  object-fit: cover;
+  display: block;
+}
+
+.ss-folder-list .right {
+  width: var(--right-width);
+  padding-left: var(--right-padding-left);
+  padding-top: 5px;
+}
+
+.ss-folder-list .right .title {
+  font-size: 16px;
+  color: #666666;
+  line-height: 2em;
+  /* padding-right: 1em; */
+  line-height: 24px;
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 1;
+  -webkit-box-orient: vertical;
+}
+
+.ss-folder-list .desc {
+  margin-top: 10px;
+  font-size: 14px;
+  color: #999999;
+  line-height: 1.8em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  gap: 20px;
+}
+/* 知识库文件夹结束 */
+
+/* 一级页面卡片左右结构开始 */
+.item-container {
+  width: 702px;
+  height: 148px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: #fafbfe;
+  border-radius: 4px;
+  position: relative;
+  cursor: pointer;
+  --header-display: none;
+  --header-color: #999;
+}
+
+.item-container:hover {
+  --header-display: flex;
+}
+
+.item-container:hover::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #dddfe6;
+}
+
+.item-container .active {
+  border: 1px solid #ddd;
+}
+
+.item-container .header {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 0;
+  right: 0;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.item-container .header:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+
+.item-container .body {
+  height: 100%;
+  padding: 12px var(--space-size, 20px);
+  box-sizing: border-box;
+  color: #333;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.item-container .left {
+  padding: 10px;
+  width: 150px;
+}
+
+.item-container .right {
+  width: calc(100% - 150px);
+}
+
+.item-container .right .title {
+  font-size: 22px;
+  color: #333;
+  line-height: 2em;
+  padding-right: 1em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.item-container .desc {
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.item-container .progress {
+  margin-top: 5px;
+  background: #edeff0;
+  width: 100%;
+  height: 22px;
+  border-radius: 11px;
+  padding: 0 65px 0 10px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  cursor: pointer;
+}
+
+.item-container .progress:hover {
+  --progress-color: #006cf1;
+}
+
+.item-container  .progress>div {
+  height: 4px;
+  position: relative;
+  border-radius: 2px;
+  background: var(--progress-color, #666666);
+}
+
+.item-container  .progress>div>div {
+  position: absolute;
+  top: 50%;
+  left: calc(100% + 10px);
+  transform: translateY(-50%);
+  font-size: 12px;
+}
+
+.page-container .content-area .left {
+  width: calc(100% - var(--project-detail-width) - 6px);
+  height: 100%;
+  overflow-y: auto;
+  /* margin-right: 6px; */
+}
+
+.page-container .content-area .left .left-content {
+  width: 100%;
+  padding: 10px;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  gap: 16px;
+  overflow-y: hidden;
+}
+
+/* 一级页面卡片左右结构结束 */
+
+/* 一级页面卡片上下结构开始 */
+.item-container2 {
+  width: 573px;
+  height: 170px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: #fafbfe;
+  border-radius: 4px;
+  position: relative;
+  cursor: pointer;
+  border: 1px solid #dddfe6;
+  --header-display: none;
+  --header-color: #999;
+}
+
+.item-container2:hover {
+  --header-display: flex;
+}
+
+.item-container2:hover::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #dddfe6;
+}
+
+.item-container2 .active {
+  border: 1px solid #ddd;
+}
+
+.item-container2 .header {
+  padding: 10px 60px 2px 20px;
+}
+
+.item-container2 .title {
+  font-size: 22px;
+  color: #333;
+  line-height: 2em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.item-container2 .body {
+  padding: 0 var(--space-size, 20px);
+  box-sizing: border-box;
+  color: #333;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.item-container2 .body>div {
+  height: 100px;
+}
+
+.item-container2 .body .left {
+  width: 180px;
+  background: #fafbfe;
+  border: 1px solid #f6f6f6;
+}
+
+.item-container2 .body .left img {
+  width: 100%;
+  height: 100%;
+}
+
+.item-container2 .body .right {
+  width: calc(100% - 180px);
+  padding: 5px 0 0 20px;
+}
+
+.item-container2  .content {
+  height: 60px;
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.item-container2  .tip {
+  margin-top: 5px;
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.item-container2 .action-bar {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 0;
+  right: 0;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.item-container2 .action-bar:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+
+.item-container2 .progress {
+  margin-top: 5px;
+  background: #edeff0;
+  width: 100%;
+  height: 22px;
+  border-radius: 11px;
+  padding: 0 65px 0 10px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  cursor: pointer;
+}
+
+.item-container2 .progress:hover {
+  --progress-color: #006cf1;
+}
+
+.item-container2 .progress>div {
+  height: 4px;
+  position: relative;
+  border-radius: 2px;
+  background: var(--progress-color, #666666);
+}
+
+.item-container2 .progress>div>div {
+  position: absolute;
+  top: 50%;
+  left: calc(100% + 10px);
+  transform: translateY(-50%);
+  font-size: 12px;
+}
+
+/* 一级页面卡片上下结构结束 */
+
+
+.form-container {
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  padding-right: 6px;
+  position: relative;
+}
+
+.form-container .content-box {
+  height: calc(100% - 80px);
+  overflow-y: auto;
+  padding: 25px 24px;
+  box-sizing: border-box;
+}
+
+.form-container .content-box .table-container {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 16px;
+  color: #333;
+  table-layout: fixed;
+}
+
+
+.form-container th {
+  /* width: 200px; */
+  height: 60px;
+  text-align: right;
+  font-weight: normal;
+  background: #f8f9fb;
+  padding: 15px;
+  color: #666666;
+  border: 1px solid #e2e4ec;
+  font-size: 16px;
+}
+
+.form-container td { 
+  /* width: 360px; //不写死 td 的宽度 Xu(20251208)*/ 
+  height: 60px;
+  padding: 0;
+  box-sizing: border-box;
+  border: 1px solid #e2e4ec;
+  position: relative;
+}
+
+.form-container .required {
+  position: absolute;
+
+  height: 100%;
+  width: 0;
+  top: 0;
+  left: 0;
+  border-left: 2px solid #ff0000;
+}
+
+.form-container .photo {
+  width: 100%;
+  /* height: 240px; */
+  display: flex;
+  flex-direction: row;
+  /* justify-content: space-between; */
+  align-items: flex-end;
+  padding: 15px;
+  gap: 15px;
+}
+
+
+
+/* .form-container .photo>.left {
+  height: 214px;
+  border-radius: 2px;
+  position: relative;
+  padding: 0 5px;
+} */
+
+.form-container .left>div.content {
+  width: 100%;
+  height: 100%;
+  background: #ebebeb;
+}
+
+.form-container .left>div.content>div.icon {
+  position: absolute;
+  top: 30px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.form-container .left>div.content>div.text {
+  position: absolute;
+  left: 50%;
+  bottom: 35px;
+  transform: translateX(-50%);
+  font-size: 16px;
+}
+
+.form-container .right {
+  display: flex;
+  align-items: flex-end;
+  justify-content: center;
+}
+
+.form-container .right>div.content {
+  width: 144px;
+  height: 144px;
+  background: #ebebeb;
+  opacity: 1;
+  border: 1px solid #edeff0;
+  border-radius: 50%;
+  position: relative;
+}
+
+.form-container .right>div.content>div.icon {
+  position: absolute;
+  top: 20px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.form-container .right>div.content>div.text {
+  position: absolute;
+  bottom: 25px;
+  left: 50%;
+  transform: translateX(-50%);
+}
+
+.form-container td>.input {
+  width: 100%;
+  height: 100%;
+}
+
+.form-container td>.area {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  width: 100%;
+  height: 100%;
+}
+
+.form-container td>.area>div {
+  height: 100%;
+}
+
+.form-container  td>div.img {
+  height: 120px;
+  display: block;
+  padding: 8px 22px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.form-container  td>div.img>img {
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+.form-container  td>div.p {
+  font-size: 16px;
+  color: #333333;
+  padding: 20px 22px;
+}
+
+
+
+.input-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.input-container>div {
+  position: absolute;
+}
+
+.input-container .input {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  position: relative;
+  --input-required-display: block;
+  display: flex;
+  align-items: center;
+}
+
+.input-container>.input>input,
+.input-container>.input>textarea {
+  outline: unset;
+  border: unset;
+  background: unset;
+  width: 100%;
+  height: 100%;
+  font-size: inherit;
+  /* 不在这里设置padding,由后面的规则或JS设置 */
+  box-sizing: border-box;
+  text-overflow: ellipsis;
+  resize: none; /* 禁止textarea手动调整大小 */
+  overflow: hidden; /* 完全隐藏滚动条 */
+  line-height: 20px; /* 设置行高 */
+}
+.input-container>.input>textarea {
+  padding-left: 15px; /* 左右padding保持 */
+  padding-right: 15px;
+  font-family: inherit; /* 继承字体 */
+  /* 上下padding由JS动态设置,不在这里固定 */
+}
+.input-container>.input>input {
+  padding: 0 15px; /* input元素的padding */
+}
+.input-container>.input>input::placeholder,
+.input-container>.input>textarea::placeholder,
+.select-container .input>input::placeholder,
+.custom-date-picker .el-input__inner::placeholder,
+.custom-date-picker .el-input__inner::placeholder{
+  color: #999;
+}
+
+/* 附件按钮样式 */
+.fj-button {
+  position: absolute;
+  right: 0;
+  bottom: 0;
+  width: 60px;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  color: #666666;
+  font-size: 16px;
+  gap: 5px;
+  background: #f0f0f0;
+  border: none;
+  border-left: 1px solid #ddd;
+  cursor: pointer;
+  z-index: 10;
+  padding: 0;
+}
+.fj-button:hover {
+  background: #e0e0e0;
+}
+/* 级联 */
+.ss-ccp-container{
+  float: left;
+}
+/* 选择框样式开始 */
+.select-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.select-container>div {
+  position: absolute;
+  z-index: 4;
+}
+
+.select-container .input {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  cursor: pointer;
+}
+
+.select-container .input>input {
+  outline: unset;
+  border: unset;
+  background: unset;
+  width: 100%;
+  height: 100%;
+  padding: 0 15px;
+  font-size: inherit;
+  color: #000;
+  /* pointer-events: none; */
+}
+.select-container .input>input::placeholder{
+  color: #999;
+}
+.select-container .suffix {
+  position: absolute;
+  top: 0;
+  right: 0;
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding: 15px 0;
+  z-index: 11;
+}
+
+.select-container .suffix>div{
+  width: 37px;
+  height: 37px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  opacity: 1;
+  background: var(--icon-bg, transparent);
+  color: #c3c7cb;
+  border-radius: 4px;
+}
+
+.select-container .suffix>div.select {
+  --icon-bg: #393d51;
+  color: #ffffff !important;
+}
+
+
+.select-container .popup-win {
+  width: auto;
+  min-width: 100%;
+  left: 0;
+  /* transform: translateX(-50%); */
+  z-index: 999;
+  padding-top: 10px;
+  --popup-content-bg: #393d51;
+}
+
+.select-container .popup-win.bottom {
+  top: calc(100% - var(--select-popup-bottom-offset, 10px));
+}
+
+.select-container .popup-win.bottom::after {
+  content: "";
+  width: 14px;
+  height: 14px;
+  background: var(--popup-content-bg);
+  position: absolute;
+  right: 28px;
+  transform: rotate(45deg);
+  top: 5px;
+}
+.select-container .popup-win.top {
+  bottom: calc(100%);
+}
+
+.select-container .popup-win.top::after {
+  content: "";
+  width: 14px;
+  height: 14px;
+  background: var(--popup-content-bg);
+  position: absolute;
+  right: 28px;
+  transform: rotate(45deg);
+  bottom: -5px;
+}
+
+.select-container .popup-content {
+  background: var(--popup-content-bg);
+  position: relative;
+  border-radius: 4px;
+  color: #fff;
+  padding: 15px 2px;
+}
+
+.select-container .popup-win .content-area {
+  width: auto;
+  max-height: 200px;
+  overflow-y: auto;
+  --scrollbar-track-color: transparent;
+  --scrollbar-thumb-color: #000;
+}
+
+
+.select-container .popup-win .content-area::-webkit-scrollbar {
+  width: 10px;
+  height: 10px;
+}
+
+.select-container .popup-win .content-area::-webkit-scrollbar-thumb {
+  border-radius: 10px;
+  background: var(--scrollbar-thumb-color, transparent);
+}
+
+.select-container .popup-win .content-area::-webkit-scrollbar-track {
+  border-radius: 10px;
+  background: var(--scrollbar-track-color, transparent);
+}
+
+.select-container .popup-win .content-area>div {
+  width: auto;
+  white-space: nowrap;
+  position: relative;
+  height: 36px;
+  padding: 0 22px;
+  font-size: 14px;
+  color: var(--item-color, #fff);
+  background: var(--item-bg, transparent);
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  cursor: pointer;
+}
+
+.select-container .popup-win .content-area>div:hover {
+  --item-color: #000;
+  --item-bg: #fffdfd;
+}
+
+.select-container .popup-win .content-area>div>span.check-icon {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%) scale(0.75);
+  left: 0;
+  padding-right: 8px;
+  visibility: var(--check-icon-visible, hidden);
+}
+
+.select-container .popup-win .content-area>div.active {
+  --check-icon-visible: visible;
+}
+
+/* radio */
+.radio-container {
+  display: flex;
+  align-content: flex-start;
+  gap: 20px;
+  padding: 0 20px;
+}
+
+.radio-container>div {
+  position: relative;
+  padding: 5px 10px;
+  border: 1px solid #bfc1c6;
+  border-radius: 2px;
+  overflow: hidden;
+  cursor: pointer;
+  text-align: center;
+  --radio-color: #c3c6ca;
+  color: var(--radio-color);
+}
+
+.radio-container .checked {
+  --radio-color: #393d51;
+}
+
+.radio-container .mark {
+  position: absolute;
+  bottom: 1px;
+  right: 0;
+  color: inherit;
+  width: 18px;
+  height: 18px;
+}
+
+.radio-container2 {
+  display: inline-flex;
+  align-content: flex-start;
+  gap: 20px;
+  margin-left: 15px;
+}
+
+.radio-container2>div {
+  position: relative;
+  padding: 5px 18px;
+  border: 1px solid #bfc1c6;
+  border-radius: 2px;
+  overflow: hidden;
+  cursor: pointer;
+  text-align: center;
+  --radio-color: #c3c6ca;
+  color: var(--radio-color);
+}
+
+.radio-container2 .checked {
+  --radio-color: #393d51;
+}
+
+.radio-container2 .mark {
+  position: absolute;
+  bottom: 1px;
+  right: 0;
+  color: inherit;
+  width: 18px;
+  height: 18px;
+}
+/* textarea */
+
+.textarea-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.textarea-container>div {
+  position: absolute;
+}
+
+.textarea-container .textarea {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+}
+
+.textarea-container textarea {
+  outline: unset;
+  border: unset;
+  background: unset;
+  width: 100%;
+  height: 100%;
+  padding: 15px;
+  line-height: 2em;
+  font-size: inherit;
+  resize: unset;
+}
+.textarea-container textarea::placeholder{
+  color: #999;
+}
+/* 错误提示开始 */
+.err-tip {
+  --err-tip-color: #ef5d0c;
+  font-size: 12px;
+  color: var(--err-tip-color);
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  cursor: pointer;
+  position: relative;
+  line-height: 15px;
+}
+
+.err-tip>div.tip {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 100%;
+  padding: 5px 15px 2px 15px;
+}
+
+.err-tip::after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  left: 2px;
+  width: calc(100% - 2px* 2);
+  /* border-bottom: 2px solid var(--err-tip-color); */
+  z-index: 1;
+}
+
+.err-tip:hover {
+  --more-tip-visible: visible;
+  --more-tip-opacity: 1;
+}
+
+.err-tip>.tip-more {
+  transition: opacity 0.2s ease-in-out 0.6s;
+  opacity: var(--more-tip-opacity, 0);
+  visibility: var(--more-tip-visible, hidden);
+  background: #ffffff;
+  border-radius: 4px;
+  border: 1px solid var(--err-tip-color);
+  position: absolute;
+  top: 0;
+  left: 2px;
+  width: calc(100% - 2px* 2);
+  padding: 4px 12.5px;
+  z-index: 5;
+}
+.err-tip>.tip-more>p{
+  margin: 0;
+  background-color: #ffffff;
+}
+
+/* 错误提示开始 */
+.validate-vline{
+  width:6px;
+  height:100%;
+  position:absolute;
+  left:0;
+  top:0;
+  background-color:#ff0000;
+}
+.validate-tip {
+  --err-tip-color: #ef5d0c;
+  font-size: 16px;
+  color: var(--err-tip-color);
+  bottom: 0;
+  left: 0;
+  width: 100%;
+  cursor: pointer;
+  position: relative;
+  line-height: 15px;
+}
+
+.validate-tip>div.tip {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  width: 100%;
+  padding: 5px 15px 2px 10px;
+}
+
+.validate-tip::after {
+  content: "";
+  position: absolute;
+  bottom: 0;
+  left: 2px;
+  width: calc(100% - 2px* 2);
+  /* border-bottom: 2px solid var(--err-tip-color); */
+  z-index: 1;
+}
+
+.validate-tip:hover {
+  --more-tip-visible: visible;
+  --more-tip-opacity: 1;
+}
+
+.validate-tip>.tip-more {
+  transition: opacity 0.2s ease-in-out 0.6s;
+  opacity: var(--more-tip-opacity, 0);
+  visibility: var(--more-tip-visible, hidden);
+  background: #fff;
+  border-radius: 4px;
+  border: 1px solid var(--err-tip-color);
+  position: absolute;
+  top: 0;
+  left: 2px;
+  width: calc(100% - 2px* 2);
+  padding: 4px 12.5px;
+  z-index: 3;
+}
+/* 错误提示结束 */
+.floating-div {
+  background-color: white;
+  position: absolute;
+  padding: 5px;
+  width: 100%;
+  /* top和bottom会通过JS动态设置 */
+  z-index: 11;
+  left: 0;
+  border: 1px solid #ccc;
+  height: auto;
+  box-sizing: border-box;
+  /* 其他样式 */
+}
+
+.floating-div textarea {
+  /* 设置 textarea 的样式 */
+  width: 100%;
+  resize: none;
+  border: none;
+  outline: none;
+  height: auto;
+  /* overflow-y: hidden; 隐藏滚动条 */
+  max-height: calc(20px * 5 + 10px); /* 统一为5行 + padding */
+  line-height: 20px;
+  font-size: 16px;
+  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+  /* padding: 5px; */
+}
+
+/* 弹窗头部样式开始 */
+.header-container {
+  width: 100%;
+  height: 100%;
+  padding-left: var(--space-size);
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.header-container .title {
+  font-size: 18px;
+}
+
+.header-container .title.visibility {
+  --title-visibility: visible;
+}
+
+.header-container .title:hover {
+  --header-icon-refresh: block;
+  --header-icon-normal: none;
+}
+
+.header-container .normal {
+  display: var(--header-icon-normal);
+}
+
+.header-container .hover {
+  display: var(--header-icon-refresh);
+  cursor: pointer;
+}
+
+.header-container .handle-bar {
+  --pop-win-header-color: #c3c6ca;
+  color: var(--pop-win-header-color);
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.header-container .left-bar {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 6px;
+  padding-right: 14px;
+  color: inherit;
+  margin-right: 54px;
+}
+
+.header-container .left-bar .dialog-icon {
+  cursor: pointer;
+  padding: 6px;
+  width: 36px;
+  height: 36px;
+  border-radius: 4px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.header-container .left-bar .dialog-icon:hover {
+  background: #c4c7cc;
+  color: #fff;
+}
+
+.header-container .setting {
+  visibility: var(--block-title-visible, hidden);
+  width: 60px;
+  height: 60px;
+  border-top-right-radius: 4px;
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+}
+
+.header-container .setting:hover {
+  color: #fff;
+  background: #575d6d;
+}
+
+.header-container .setting:hover::before {
+  display: none;
+}
+
+.header-container .close-bar {
+  width: 60px;
+  height: 60px;
+  border-top-right-radius: 4px;
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  position: absolute;
+  right: 0;
+  top: 0;
+}
+
+.header-container .close-bar::before {
+  width: 0;
+  height: 24px;
+  border-left: 1px solid #c3c6ca;
+  content: "";
+  position: absolute;
+  top: 50%;
+  left: 0;
+  transform: translateY(-50%);
+}
+
+.header-container .close-bar:hover {
+  color: #fff;
+  background: #575d6d;
+}
+
+.header-container .close-bar:hover::before {
+  display: none;
+}
+/* 弹窗头部样式结束 */
+
+
+/* 弹窗样式开始 */
+.popup-container {
+  height: 100%;
+  width: 100%;
+  background: var(--white);
+  font-size: initial;
+  box-shadow: 10px 0px 10px 0px rgba(0, 0, 0, 0.3);
+  border-radius: 4px;
+}
+
+
+.popup-container>div {
+  width: 100%;
+}
+
+.popup-container .header {
+  height: 60px;
+  border-bottom: 2px solid var(--lightgray);
+  background-color: white;
+  border-radius: 4px;
+}
+
+.popup-container .body {
+  height: calc(100% - 60px);
+}
+
+.popup-container .dialog-body {
+  width: 500px;
+  height: 300px;
+  background: red;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  position: absolute;
+}
+
+.dialog-container {
+  width: 100%;
+  height: 100%;
+  position: fixed;
+  top: 0;
+  left: 0;
+  pointer-events: none;
+  background: transparent;
+  font-size: 0;
+  z-index: 1000;
+}
+
+.dialog-container .mark-content {
+  width: 100%;
+  height: 100%;
+  pointer-events: all;
+  position: relative;
+  -webkit-backdrop-filter: blur(3px);
+  backdrop-filter: blur(3px);
+  background: rgba(0, 0, 0, 0.2);
+}
+
+/* 登录部分 */
+
+.login-container {
+  width: 100%;
+  height: 100%;
+  background-size: cover;
+  background-color: antiquewhite;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  position: fixed;
+  z-index: 99999;
+  left: 0;
+  top: 0;
+}
+
+.login-container>div {
+  height: 100%;
+}
+
+.login-container .left {
+  width: calc(100% - 510px);
+}
+
+.login-container .right {
+  width: 510px;
+  background: rgba(255, 255, 255, 0.2);
+  position: relative;
+}
+
+.login-container .right>div.content-area {
+  height: calc(100% - 70px);
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.login-container .login-box {
+  width: 432px;
+  height: 494px;
+  background: rgba(255, 255, 255, 0.9);
+  box-shadow: 0px 5px 5px 0px rgba(0, 0, 0, 0.2);
+  border-radius: 4px;
+}
+
+.login-container .login-box>div {
+  width: 100%;
+}
+
+.login-container .box-header {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding: 42px;
+}
+
+.login-container .box-header>img {
+  width: 348px;
+  height: 98px;
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+.login-container .box-container {
+  height: calc(100% - 182px - 40px);
+  position: relative;
+}
+
+.login-container .box-form {
+  padding: 0 37px;
+}
+
+.login-container>div.right>div.content-area>div.login-box>div.box-container>form.box-form>div.form-item {
+  width: 100%;
+  height: 37px;
+  position: relative;
+  display: flex;
+  margin-bottom: 20px;
+  background: #ffffff;
+  border: 1px solid #575d6d;
+  border-radius: 4px;
+}
+
+.login-container .icon {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  width: 46px;
+  height: 100%;
+  background: #575d6d;
+  color: #fff;
+}
+
+.login-container .icon-container {
+  display: block;
+}
+
+.login-container .check-code {
+  width: 140px;
+  height: 35px;
+}
+
+.login-container .check-code img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: cover;
+  object-fit: cover;
+}
+
+.login-container .input,
+.login-container .select {
+  flex: 1;
+  height: 100%;
+}
+
+.login-container .input input,
+.login-container .select input,
+.login-container .input select,
+.login-container .select select {
+  width: 100%;
+  height: 100%;
+  outline: none;
+  border: unset;
+  background: transparent;
+  padding: 0 10px;
+  font-size: 18px;
+  color: #333;
+}
+
+.login-container .form-bar {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.login-button {
+  cursor: pointer;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 10px;
+  width: 161px;
+  height: 37px;
+  background: #ffffff;
+  border-radius: 4px;
+  outline: unset;
+  font-size: 18px;
+  --btn-color: #333333;
+  --btn-bg: #fff;
+  --btn-border-color: #575d6d;
+  color: var(--btn-color);
+  background-color: var(--btn-bg);
+  border: 1px solid var(--btn-border-color);
+}
+
+.login-button[theme="primary"],
+.login-button:hover {
+  --btn-bg: #575d6d;
+  --btn-color: #fff;
+}
+
+.login-container .err-tip {
+  bottom: -32px;
+  position: absolute;
+  padding-left: 40px;
+}
+
+.login-container .err-tip::after {
+  bottom: -8px;
+}
+
+.login-container .time {
+  position: absolute;
+  left: 10px;
+  width: 12px;
+  bottom: 0;
+}
+
+.login-container .time>div {
+  width: 12px;
+  height: 12px;
+  opacity: 1;
+  margin-bottom: 8px;
+  border-radius: 50%;
+  border: 1px solid #ef5d0c;
+}
+
+.login-container .fill {
+  background: #ef5d0c;
+}
+
+.login-container .box-container>div.err-tip>div.tip {
+  color: #ef5d0c;
+  font-size: 18px;
+  bottom: -30px;
+  left: 10px;
+  padding: 5px 15px 2px 0;
+}
+
+.login-container .box-footer {
+  height: 40px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.login-container .box-footer>div {
+  height: 100%;
+}
+
+.login-container .box-footer>div.left {
+  width: calc(100% - 40px);
+}
+
+.login-container .box-footer>div.right {
+  width: 40px;
+  cursor: pointer;
+}
+
+.login-container .footer {
+  padding: 0 40px;
+  height: 70px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  font-size: 18px;
+  color: #fff;
+}
+
+.login-container .mask {
+  width: 100%;
+  height: 100%;
+  top: 0;
+  left: 0;
+  z-index: 100;
+  position: absolute;
+  background: rgba(255, 255, 255, 0.2);
+  padding-bottom: 70px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.login-container .qrcode-box {
+  width: 432px;
+  height: 494px;
+  background: rgba(0, 0, 0, 0.2);
+  border-radius: 4px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+}
+
+.login-container .qrcode-box::after {
+  content: "";
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: 0;
+  background: rgba(0, 0, 0, 0.2);
+  filter: blur(1px);
+}
+
+.login-container .qr {
+  width: 340px;
+  height: 340px;
+  position: absolute;
+  z-index: 1;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  background: #f7f7f7;
+  box-shadow: 5px 0px 5px 0px rgba(0, 0, 0, 0.2);
+  border-radius: 4px;
+  padding: 20px;
+}
+
+.login-container .qr>img {
+  user-drag: none;
+  -webkit-user-drag: none;
+  -khtml-user-drag: none;
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  width: 100%;
+  height: 100%;
+}
+
+.login-container>div.right>div.mask>div.qrcode-box>div.tip {
+  position: absolute;
+  bottom: 1em;
+  left: 0;
+  width: 100%;
+  text-align: center;
+  z-index: 10;
+  font-size: 18px;
+  color: #ff0000;
+}
+
+.lockscreen-container .login-box{
+  height: auto;
+  position: relative;
+}
+.lockscreen-container .box-header{
+  display: flex;
+  flex-direction: column;
+  padding: 15px;
+  margin: 41px 0;
+}
+.lockscreen-container .box-header img{
+  width: 100px;
+  height: 100px;
+  border-radius: 50%;
+}
+.lockscreen-container .right{
+  opacity: 0;
+  visibility: hidden;
+  transition: opacity 0.3s ease, visibility 0.3s ease;
+}
+  
+.lockscreen-container .right--visible {
+  opacity: 1;
+  visibility: visible;
+}
+.lockscreen-container  .left {
+  cursor: pointer;
+}
+/* 登陆相关结束  */
+
+/* 首页组件头部 */
+.edit-box-header-container {
+  width: 100%;
+  height: 100%;
+  padding-left: var(--space-size);
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.edit-box-header-container .title {
+  font-size: 18px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 10px;
+  visibility: var(--title-visibility, hidden);
+  --header-icon-refresh: none;
+  --header-icon-normal: block;
+}
+
+.edit-box-header-container .title.visibility {
+  --title-visibility: visible;
+}
+
+.edit-box-header-container .title:hover {
+  --header-icon-refresh: block;
+  --header-icon-normal: none;
+}
+
+.edit-box-header-container .normal {
+  display: var(--header-icon-normal);
+}
+
+.edit-box-header-container .hover {
+  display: var(--header-icon-refresh);
+  cursor: pointer;
+}
+
+.edit-box-header-container .handle-bar {
+  --pop-win-header-color: #c3c6ca;
+  color: var(--pop-win-header-color);
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.edit-box-header-container .left-bar {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 12px;
+  padding-right: 14px;
+  color: inherit;
+}
+
+.edit-box-header-container .left-bar .icon-container {
+  cursor: pointer;
+  padding: 6px;
+  width: 36px;
+  height: 36px;
+  border-radius: 4px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.edit-box-header-container .left-bar .icon-container:hover {
+  background: #c4c7cc;
+  color: #fff;
+}
+
+.edit-box-header-container .setting {
+  visibility: var(--block-title-visible, hidden);
+  width: 60px;
+  height: 60px;
+  border-top-right-radius: 4px;
+  position: relative;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+}
+
+.edit-box-header-container .setting:hover {
+  color: #fff;
+  background: #575d6d;
+}
+
+.edit-box-header-container .setting:hover::before {
+  display: none;
+}
+
+.user-info-container {
+  width: 590px;
+  height: 400px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: var(--white);
+  border-radius: 2px;
+  --block-title-visible: hidden;
+}
+
+.user-info-container:hover {
+  --block-title-visible: visible;
+  box-shadow: 0 0 0 8px #dddfe6;
+  border-radius: 1px;
+}
+
+
+.user-info-container .body {
+  padding: 0 var(--space-size, 20px) var(--space-size, 20px) var(--space-size, 20px);
+}
+
+.user-info-container  .user-info {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.user-info-container .avatar {
+  width: 94px;
+  height: 94px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+}
+
+.user-info-container  .info {
+  width: calc(100% - 120px);
+}
+
+.user-info-container  .info>p {
+  padding: 0;
+  margin: 0;
+  cursor: pointer;
+}
+
+.user-info-container  .info>p:first-child {
+  font-size: 16px;
+  color: #333;
+  margin-bottom: 10px;
+}
+
+.user-info-container  .info>p:not(:first-child) {
+  font-size: 14px;
+  color: #808080;
+  line-height: 22px;
+}
+
+.user-info-container  .progress-bar {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.user-info-container .progress {
+  width: calc(100% - 0px);
+  height: 20px;
+  font-size: 12px;
+  padding: 0 90px 0 10px;
+  background: var(--lightgray);
+  border-radius: 10px;
+  --progress-color: #666666;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.user-info-container .progress:hover {
+  --progress-color: #0072f1;
+}
+
+.user-info-container .line {
+  background: var(--progress-color);
+  width: 0%;
+  height: 4px;
+  position: relative;
+  border-radius: 2px;
+}
+
+.user-info-container .line>div {
+  position: absolute;
+  top: 50%;
+  left: calc(100% + 1em);
+  transform: translateY(-50%);
+  width: 5.5em;
+}
+
+.user-info-container .bar {
+  padding-left: 10px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  height: 0px;
+  cursor: pointer;
+}
+
+.user-info-container .other-info {
+  padding: 10px 0;
+  color: #666;
+  font-size: 16px;
+}
+
+.user-info-container  .other-info>div {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 20px;
+  padding: 15px 0;
+}
+
+.edit-box-container {
+  width: 100%;
+  height: 100%;
+  position: relative;
+  overflow: hidden;
+}
+
+.edit-box-container>div {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+}
+
+.edit-box-container .edit-tools {
+  --direction-icon-visible: hidden;
+  display: var(--edit-box-tools-display, none);
+  z-index: 2;
+}
+
+.edit-box-container .edit-tools:hover,
+.edit-box-container .edit-tools.active {
+  --direction-icon-visible: visible;
+}
+
+.edit-box-container .edit-tools>div {
+  position: absolute;
+  pointer-events: all;
+  cursor: pointer;
+  display: flex;
+}
+
+.edit-box-container .close {
+  height: 36px;
+  width: 36px;
+  top: 0;
+  right: 0;
+  color: var(--edit-tool-bar-close-color, #c3c6ca);
+  visibility: var(--direction-icon-visible);
+}
+
+.edit-box-container .close:hover {
+  --edit-tool-bar-close-color: #575d6d;
+}
+
+.edit-box-container .right-center {
+  visibility: var(--direction-icon-visible);
+  height: 30px;
+  width: 30px;
+  top: 50%;
+  transform: translateY(-50%);
+  right: 0;
+  justify-content: flex-end;
+  align-items: flex-end;
+}
+
+.edit-box-container .right-center::before {
+  content: "";
+  right: 0;
+  top: 0;
+  width: 4px;
+  height: 100%;
+  background: #999999;
+  border-radius: 2px;
+  position: absolute;
+}
+
+.edit-box-container .right-center .icon {
+  height: 30px;
+  width: 30px;
+  transform: rotate(90deg);
+}
+
+.edit-box-container .right-bottom {
+  visibility: var(--direction-icon-visible);
+  width: 36px;
+  height: 36px;
+  bottom: 0;
+  right: 0;
+  justify-content: flex-end;
+  align-items: flex-end;
+}
+
+.edit-box-container .right-bottom::before {
+  content: "";
+  right: 0;
+  top: 0;
+  width: 3px;
+  height: 100%;
+  background: #999999;
+  border-radius: 2px;
+  position: absolute;
+}
+
+.edit-box-container .right-bottom::after {
+  content: "";
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  height: 3px;
+  background: #999999;
+  border-radius: 2px;
+  position: absolute;
+}
+
+.edit-box-container .right-bottom>.icon {
+  width: 30px;
+  height: 30px;
+  transform: rotate(-45deg);
+}
+
+.edit-box-container .bottom-center {
+  visibility: var(--direction-icon-visible);
+  height: 36px;
+  width: 36px;
+  left: 50%;
+  transform: translateX(-50%);
+  bottom: 0;
+  justify-content: center;
+  align-items: flex-end;
+}
+
+.edit-box-container .bottom-center::before {
+  content: "";
+  left: 0;
+  bottom: 0;
+  width: 100%;
+  height: 4px;
+  background: #999999;
+  border-radius: 2px;
+  position: absolute;
+  z-index: 0;
+}
+
+.edit-box-container .bottom-center>.icon {
+  height: 30px;
+  width: 30px;
+  transform: rotate(180deg);
+}
+
+.edit-box-container>.content-area {
+  z-index: 1;
+}
+
+.page-content {
+  width: 100%;
+  height: 100%;
+  padding: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  gap: 15px;
+  overflow-y: hidden;
+  background: var(--lightgray);
+}
+
+.page-content:hover {
+  overflow-y: auto;
+}
+
+.folder-container {
+  width: 100%;
+  height: 100%;
+  overflow-y: hidden;
+}
+
+.folder-container:hover {
+  overflow-y: auto;
+}
+
+.folder-container .group-item {
+  --hover-bg: #edf1f5;
+  padding-right: 6px;
+  color: #333;
+}
+
+.folder-container  .group-title {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 10px;
+  color: #333;
+  cursor: pointer;
+  padding: 15px var(--space-size, 20px);
+}
+
+.folder-container .group-title:hover {
+  background: var(--hover-bg);
+}
+
+.folder-container .group-title>div.folder {
+  position: relative;
+}
+
+.folder-container .group-title>div.folder::after {
+  content: attr(data-num);
+  position: absolute;
+  font-size: 14px;
+  color: #999;
+  top: 50%;
+  left: 50%;
+  transform: translate(-50%, -45%) scale(0.9);
+}
+
+.folder-container .group-childs {
+  padding-left: 40px;
+  margin: 0;
+}
+
+.folder-container .group-childs>li {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  list-style-type: none;
+  cursor: pointer;
+  position: relative;
+  padding: 12px 5px 12px 20px;
+  border-bottom: 1px solid #eee;
+}
+
+.folder-container .group-childs>li::before {
+  position: absolute;
+  content: "";
+  top: 50%;
+  left: 5px;
+  transform: translateY(-50%);
+  width: 0.25em;
+  height: 0.25em;
+  border-radius: 50%;
+  background-color: #666;
+}
+
+.folder-container .group-childs>li:hover {
+  background: var(--hover-bg);
+}
+
+.folder-container .group-childs>li>div:first-child {
+  color: #333;
+  font-size: 16px;
+}
+
+.folder-container .group-childs>li>div:nth-child(2) {
+  color: #999;
+}
+
+.todo-list-container {
+  width: 590px;
+  height: 400px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: var(--white);
+  border-radius: 2px;
+  --block-title-visible: hidden;
+}
+
+.todo-list-container:hover {
+  --block-title-visible: visible;
+  box-shadow: 0 0 0 8px #dddfe6;
+  border-radius: 1px;
+}
+
+.todo-list-container .body {
+  border-top: 1px solid #eaeaea;
+  height: calc(100% - 57px);
+  overflow-y: hidden;
+  padding: 12px 6px 12px 0px;
+  box-sizing: border-box;
+}
+
+.notice-list-container {
+  width: 590px;
+  height: 242px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: var(--white);
+  border-radius: 2px;
+  --block-title-visible: hidden;
+}
+
+.notice-list-container:hover {
+  --block-title-visible: visible;
+  box-shadow: 0 0 0 8px #dddfe6;
+  border-radius: 1px;
+}
+
+.notice-list-container .header {
+  border-bottom: 1px solid #eaeaea;
+}
+
+.notice-list-container .body {
+  width: calc(100% - 6px);
+
+  height: calc(100% - 57px);
+  overflow-y: hidden;
+  padding: 12px var(--space-size, 20px);
+  box-sizing: border-box;
+  color: #333;
+}
+
+.notice-list-container .body:hover {
+  overflow-y: auto;
+}
+
+.notice-list-container .body>div {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 15px 0;
+}
+
+.notice-list-container .body>div>div:first-child {
+  gap: 5px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.notice-list-container .body>div>div:nth-child(2) {
+  color: #999999;
+}
+
+.launch-container {
+  width: 590px;
+  height: 204px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: var(--white);
+  border-radius: 2px;
+  --block-title-visible: hidden;
+}
+
+.launch-container:hover {
+  --block-title-visible: visible;
+  box-shadow: 0 0 0 8px #dddfe6;
+  border-radius: 1px;
+}
+
+.launch-container .body {
+  border-top: 1px solid #eaeaea;
+  height: calc(100% - 57px);
+  overflow-y: auto;
+  padding: 12px var(--space-size, 20px);
+  box-sizing: border-box;
+  color: #808080;
+  display: flex;
+  align-content: flex-start;
+  flex-wrap: wrap;
+  font-size: 12px;
+  gap: 20px;
+}
+
+.launch-container .body>div {
+  cursor: pointer;
+  width: 60px;
+  height: 100px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+}
+
+.launch-container .body>div:hover {
+  --popup-visible: visible;
+}
+
+.launch-container .body>div>.common-icon {
+  color: #b4b5b5;
+  width: 60px;
+  height: 60px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  background: var(--item-bg, transparent);
+  position: relative;
+  --item-mark-color: #999999;
+}
+
+.launch-container .body>div>.common-icon:hover {
+  --item-bg: #edf1f5;
+  --item-mark-color: #3a3e51;
+}
+
+.launch-container .body>div>.common-icon.mark-down::after {
+  content: "";
+  width: 0;
+  height: 0;
+  border-width: 6px 6px 6px 6px;
+  border-style: solid;
+  border-color: transparent var(--item-mark-color) var(--item-mark-color) transparent;
+  position: absolute;
+  bottom: 0;
+  right: 0;
+}
+
+.launch-container  .text {
+  padding-top: 4px;
+  font-size: 16px;
+}
+
+.launch-container .popup {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 130px;
+  height: 90px;
+  --popup-bg: #3a3e51;
+  visibility: var(--popup-visible, hidden);
+  background: var(--popup-bg);
+  padding: 16px 2px 0 2px;
+  border-radius: 4px;
+  z-index: 1111;
+}
+
+.launch-container .popup::before {
+  width: 100%;
+  height: 12px;
+  background: transparent;
+  content: "";
+  position: absolute;
+  top: -12px;
+  left: 0;
+}
+
+.launch-container .popup::after {
+  content: "";
+  background: var(--popup-bg);
+  width: 12px;
+  height: 12px;
+  position: absolute;
+  top: -6px;
+  right: 30px;
+  transform: rotate(45deg);
+}
+
+.launch-container .popup>div {
+  height: 36px;
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  padding: 0 15px;
+  font-size: 14px;
+  background: var(--item-bg, transparent);
+  color: var(--item-color, #fff);
+}
+
+.launch-container .popup>div:hover {
+  --item-bg: #fff;
+  --item-color: #333;
+}
+
+.statistics-container {
+  width: 1200px;
+  height: 475px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: var(--white);
+  border-radius: 2px;
+  --block-title-visible: hidden;
+}
+
+.statistics-container:hover {
+  --block-title-visible: visible;
+  box-shadow: 0 0 0 8px #dddfe6;
+  border-radius: 1px;
+}
+
+.statistics-container .body {
+  /* border-top: 1px solid #eaeaea; */
+  height: calc(100% - 57px);
+  overflow-y: auto;
+  padding: 12px var(--space-size, 20px);
+  box-sizing: border-box;
+  color: #333;
+}
+
+.statistics-container .body .chart-container {
+  width: 100%;
+  height: 100%;
+}
+
+.page-container {
+  width: 100%;
+  height: 100%;
+  /* padding: 20px 0; */
+}
+
+.page-container[sys-mode="edit"] {
+  --edit-box-tools-display: block;
+}
+
+.page-container .search-bar {
+  margin-top: 20px;
+  width: 100%;
+  height: 40px;
+  position: relative;
+}
+
+.page-container .item-content-area {
+  height: calc(100% - 75px);
+  width: 100%;
+  margin-top: 10px;
+  padding: 10px 0 100px 20px;
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  overflow-y: auto;
+  --project-detail-width: 370px;
+}
+
+.page-container .content-area .pager-bar {
+  width: 100%;
+  height: 80px;
+  padding: 20px;
+}
+
+.page-container .content-area>div.project-detail {
+  padding-right: 20px;
+  width: var(--project-detail-width);
+  height: 100%;
+}
+
+/* 分页开始 */
+.pager-container {
+  width: 98%;
+  margin: 0 auto;
+  /* height: 100%; */
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+.pager-container .pager-content {
+  border-radius: 4px;
+  height: 32px;
+  color: #999999;
+  font-size: 18px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+}
+
+.pager-container .pager-content>div {
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.pager-container  .info {
+  padding: 0 20px;
+  border: 1px solid var(--btn-border, #c4c7cc);
+  border-top-left-radius: 4px;
+  border-bottom-left-radius: 4px;
+}
+
+.pager-container .btn>button {
+  height: 100%;
+  background: #edf1f5;
+  width: 84px;
+  outline: unset;
+  cursor: pointer;
+  font-size: inherit;
+  color: var(--btn-color, #999999);
+  border: 1px solid var(--btn-border, #c4c7cc);
+}
+
+.pager-container .btn>button:hover {
+  --btn-border: transparent;
+  --btn-color: #fff;
+  background: #393d51;
+}
+
+.pager-container .btn:last-child>button {
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+}
+
+/* 分页结束 */
+/* 一级页面右边信息栏开始 */
+.info-container {
+  width: 100%;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: #fafbfe;
+  border-radius: 2px;
+  position: relative;
+  overflow-y: auto;
+  height: 100%;
+}
+
+.info-container .header {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 20px;
+  border-bottom: 1px solid #cfd2d7;
+}
+
+.info-container .header>div {
+  height: 40px;
+  display: flex;
+  align-items: center;
+}
+
+.info-container .header>div:first-child {
+  width: 80px;
+}
+
+.info-container .header>div:last-child {
+  padding-left: 5px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: flex-end;
+  width: calc(70%);
+  color: #333333;
+  font-size: 18px;
+}
+
+.info-container .header>div:last-child>div {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.info-container .section-container {
+  padding: 0 20px;
+}
+
+.info-container  .section {
+  padding: 8px 0;
+  --title-tip-height: 6px;
+}
+
+.info-container   .section:not(:last-child) {
+  border-bottom: 1px solid #edf1f5;
+}
+
+.info-container   .section:hover {
+  --title-tip-height: 18px;
+}
+
+.info-container   .section>div.title {
+  color: #333333;
+  font-size: 18px;
+  line-height: 3em;
+  position: relative;
+  padding-left: 20px;
+}
+
+.info-container   .section .title:before {
+  transition: height 0.5s;
+  content: "";
+  width: 6px;
+  height: var(--title-tip-height);
+  border-radius: 3px;
+  position: absolute;
+  left: 5px;
+  top: 50%;
+  transform: translateY(-50%);
+  background: #333;
+}
+
+.info-container   .section> .a {
+  --a-color: #333;
+  color: var(--a-color);
+  font-size: 16px;
+  line-height: 2.5em;
+  cursor: pointer;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.info-container   .section .a:hover {
+  --a-color: #006cf1;
+}
+
+.info-container   .section .text {
+  color: #333333;
+  font-size: 16px;
+  line-height: 2.5em;
+}
+
+/* 一级页面右边信息栏结束 */
+
+/* 统计图 */
+.page-statistics .content-area {
+  padding: 20px;
+  height: calc(100% - 80px);
+  display: flex;
+  flex-wrap: wrap;
+  align-content: flex-start;
+  gap: 20px;
+  overflow-y: auto;
+}
+
+.project-report-statistics .statistics-container:hover {
+  --block-title-visible: visible;
+}
+
+.project-report-statistics  .body {
+  /* border-top: 1px solid #eaeaea; */
+  height: calc(100% - 57px);
+  overflow-y: auto;
+  padding: 12px var(--space-size, 20px);
+  box-sizing: border-box;
+  color: #333;
+}
+
+.project-report-statistics  .table-container {
+  border: 1px solid #dddfe6;
+  margin-bottom: 22px;
+  overflow-x: auto;
+  background: var(--table-bg, #fff);
+}
+
+.project-report-statistics  .table-container::-webkit-scrollbar {
+  width: 0;
+  height: 0;
+}
+
+.project-report-statistics  .table-container::-webkit-scrollbar-thumb {
+  border-radius: 0;
+  -webkit-box-shadow: inset 0 0 0 rgba(0, 0, 0, 0.2);
+  background: var(--scrollbar-thumb-color, #c1c1c1);
+}
+
+.project-report-statistics  .table-container::-webkit-scrollbar-track {
+  border-radius: 0;
+  -webkit-box-shadow: inset 0 0 0 rgba(0, 0, 0, 0.2);
+  background: var(--scrollbar-track-color, transparent);
+}
+
+.project-report-statistics  .table-container:hover {
+  --table-bg: #edf1f5;
+}
+
+.project-report-statistics  .title {
+  position: relative;
+  padding: 15px 20px 15px 35px;
+  font-size: 16px;
+  border-bottom: 1px solid #dddfe6;
+}
+
+.project-report-statistics  .title::before {
+  content: "";
+  position: absolute;
+  top: 50%;
+  left: 20px;
+  transform: translateY(-50%);
+  width: 5px;
+  height: 5px;
+  background: #333333;
+  border-radius: 50%;
+}
+
+.project-report-statistics  .table-columns {
+  display: flex;
+  flex-wrap: nowrap;
+}
+
+.project-report-statistics  .table-columns>div {
+  flex-shrink: 0;
+  padding: 10px 20px;
+  border-right: 1px solid #dddfe6;
+}
+
+/* 跨对象弹窗开始 */
+.search-dialog-content {
+  width: 900px;
+  height: 100vh;
+  position: relative;
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  font-size: initial;
+  color: #333;
+  /* padding: 0 100px; */
+  background: #ffffff;
+  /* box-shadow: 0px 10px 5px 0px rgba(0, 0, 0, 0.3); */
+  border-radius: 4px;
+  /* height: 100%; */
+  padding: 0 100px 30px 100px;
+}
+
+.search-dialog-content>div {
+  width: 100%;
+}
+
+.search-dialog-content .header {
+  height: 130px;
+}
+
+.search-dialog-content .input-area {
+  width: 100%;
+  height: 53px;
+  color: #666;
+  font-size: 18px;
+  position: relative;
+  border: 1px solid #999999;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.search-dialog-content .input-area>input {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  line-height: 53px;
+  padding: 0 20px;
+  font-size: inherit;
+  outline: unset;
+  border: unset;
+  z-index: 1;
+}
+
+.search-dialog-content .search-btn {
+  position: absolute;
+  top: 0;
+  right: 0;
+  font-size: inherit;
+  color: inherit;
+  height: 100%;
+  padding: 0 17px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  cursor: pointer;
+  z-index: 2;
+  --search-btn-bg: transparent;
+  --search-btn-color: #666666;
+  --search-btn-left-border-visible: visible;
+  background: var(--search-btn-bg);
+  color: var(--search-btn-color);
+}
+
+.search-dialog-content .search-btn:hover {
+  --search-btn-color: #fff;
+  --search-btn-bg: #242835;
+  --search-btn-left-border-visible: hidden;
+}
+
+.search-dialog-content .search-btn::after {
+  visibility: var(--search-btn-left-border-visible);
+  position: absolute;
+  top: 50%;
+  left: 0;
+  transform: translateY(-50%);
+  width: 0;
+  height: 60%;
+  border-left: 1px solid #999999;
+  content: "";
+}
+
+.search-dialog-content .sort {
+  padding: 10px 0;
+  font-size: 14px;
+  color: #a9a9a9;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.search-dialog-content .list {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 10px;
+}
+
+.search-dialog-content .list>div {
+  padding: 5px 10px;
+  cursor: pointer;
+}
+
+.search-dialog-content .active {
+  background-color: #e8f9fc;
+  color: #0066ff;
+  border-radius: 2px;
+}
+
+.search-dialog-content .time {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  gap: 10px;
+}
+
+.search-dialog-content .res {
+  color: #999999;
+  font-size: 12px;
+}
+
+.search-dialog-content .search-res {
+  height: calc(100% - 130px);
+  overflow-y: auto;
+  padding: 10px 10px 0 0;
+  margin-bottom: 5px;
+}
+
+.search-dialog-content .search-item {
+  margin-bottom: 20px;
+}
+
+.search-dialog-content .search-item.style-1>div.title {
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+}
+
+.search-dialog-content .search-item.style-1>div.detail {
+  padding: 5px 0;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.search-dialog-content .search-item.style-1>div.detail>div.thumb {
+  width: 77px;
+  height: 95px;
+}
+
+.search-dialog-content .search-item.style-1>div.detail>div.thumb>img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+.search-dialog-content .search-item.style-1>div.detail>div.info {
+  width: calc(100% - 77px);
+  padding-left: 10px;
+}
+
+.search-dialog-content .search-item.style-1>div.detail>div.info>div.title,
+.search-dialog-content .search-item.style-1>div.detail>div.info>div.subtitle {
+  color: #333;
+  font-size: 16px;
+  line-height: 1.5em;
+}
+
+.search-dialog-content .search-item.style-1>div.detail>div.info>div.desc {
+  margin-top: 5px;
+  color: #666666;
+  font-size: 14px;
+}
+
+.search-dialog-content .search-item.style-2>div.title {
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+}
+
+.search-dialog-content .search-item.style-2>div.desc {
+  color: #666666;
+  font-size: 14px;
+}
+
+.search-dialog-content .search-item.style-3>div.title {
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+}
+
+.search-dialog-content .search-item.style-3>div.group-list {
+  margin-top: 10px;
+}
+
+.search-dialog-content .search-item.style-3 .gitem {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  align-items: flex-start;
+}
+
+.search-dialog-content .search-item.style-3 .gitem:not(:last-child) {
+  margin-bottom: 20px;
+}
+
+.search-dialog-content .search-item.style-3  .thumb {
+  width: 180px;
+  height: 99px;
+}
+
+.search-dialog-content .search-item.style-3  .thumb>img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+.search-dialog-content .search-item.style-3  .info {
+  width: calc(100% - 180px);
+  padding-left: 10px;
+}
+
+.search-dialog-content .search-item.style-3  .info>div.title {
+  color: #666666;
+  font-size: 16px;
+}
+
+.search-dialog-content .search-item.style-3  .info>div.desc {
+  margin-top: 5px;
+  color: #666666;
+  font-size: 14px;
+}
+
+.search-dialog-content .search-item.style-3  .info>div.source {
+  margin-top: 5px;
+  color: #666666;
+  font-size: 12px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 5px;
+}
+
+.search-dialog-content .search-item.style-3  .info>div.source .icon-container {
+  color: rgba(0, 0, 0, 0.3);
+}
+
+/* 跨对象弹窗结束 */
+
+/* 项目基本信息开始 */
+.project-edit-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  overflow: hidden;
+}
+
+.project-edit-container>div {
+  height: 100%;
+}
+
+.project-edit-container .left-side {
+  width: 180px !important;
+  border-right: 1px solid #e2e4ec;
+  background-color: #edf1f5;
+}
+
+.project-edit-container .left-side>div {
+  width: 100%;
+}
+
+.project-edit-container .menu-header {
+  height: 120px;
+  border-bottom: 1px solid #d8dae3;
+}
+
+.project-edit-container .menu-content {
+  height: calc(100% - 60px);
+}
+
+.project-edit-container .menu-content .scroll-view {
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+  font-size: 18px;
+}
+
+.project-edit-container .menu-item,
+.project-edit-container .group .menu-item {
+  padding: 20px 12px 20px 30px;
+  cursor: pointer;
+  position: relative;
+  color: #333333;
+  display: flex;
+  align-items: center;
+}
+
+.project-edit-container .menu-item .arrow{
+  position: absolute;
+  left: 180px;
+}
+
+.menu-item-point{
+  width: 6px;
+  height: 6px;
+  display: block;
+  border-radius: 50%;
+  background: #666;
+  
+}
+
+.project-edit-container .menu-item>.icon-container,
+.project-edit-container .group .menu-item>.icon-container {
+  margin-left: 10px;
+}
+
+.project-edit-container .menu-item::after,
+.project-edit-container .group .menu-item::after {
+  content: "";
+  width: calc(100% - 10px * 2);
+  height: 0;
+  display: block;
+  position: absolute;
+  bottom: -1px;
+  left: 10px;
+  border-bottom: 1px solid #d8dae3;
+}
+
+.project-edit-container .menu-item.active,
+.project-edit-container .group .menu-item.active {
+  background: #fff;
+}
+
+.project-edit-container .menu-item.active::after,
+.project-edit-container .group .menu-item.active::after {
+  border-bottom-color: transparent;
+}
+
+.project-edit-container .group {
+  width: 100%;
+}
+
+/* .project-edit-container  .group-detail {
+  padding-left: 10px;
+} */
+
+.project-edit-container  .group-detail .menu-item {
+  padding-left: 44px;
+}
+
+/* .project-edit-container .group-detail .menu-item::before {
+  content: "";
+  height: 6px;
+  width: 6px;
+  border-radius: 50%;
+  position: absolute;
+  top: 50%;
+  left: 30px;
+  background: #666;
+  transform: translateY(-50%);
+  position: relative;
+} */
+
+.sub-tab-menu-footer {
+  height: 60px;
+  flex-shrink: 0;
+  background: #afb8d0;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 10px;
+  font-size: 20px;
+  color: #fff;
+  cursor: pointer;
+  position: relative;
+}
+
+.sub-tab-menu-footer:hover {
+  background: #393d51;
+}
+
+.sub-tab-menu-popup {
+  position: absolute;
+  bottom: 76px;
+  left: 0;
+  right: 0;
+  display: none;
+
+}
+
+.sub-tab-menu-popup>div {
+  height: 76px;
+  flex-shrink: 0;
+  background: #afb8d0;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 10px;
+  font-size: 20px;
+  color: #fff;
+  cursor: pointer;
+  position: relative;
+}
+
+.sub-tab-menu-popup>div::after {
+  content: "";
+  width: 90%;
+  height: 1px;
+  display: block;
+  position: absolute;
+  bottom: 0;
+  left: 50%;
+  transform: translateX(-50%);
+  background: #ffffff;
+}
+
+.sub-tab-menu-popup>div:hover {
+  background: #393d51;
+}
+
+/* 鼠标悬浮时显示弹出菜单 */
+.sub-tab-menu-footer:hover .sub-tab-menu-popup {
+  display: block;
+}
+
+.project-edit-container>div.content-area {
+  width: calc(100% - 180px);
+  overflow-y: auto;
+}
+
+/* 项目基本信息结束 */
+
+/* 项目基本信息中的基本信息表格 */
+
+.base-container {
+  width: 100%;
+  padding: 32px;
+}
+
+.base-container .base-content {
+  width: 100%;
+  --border-color: #e2e4ec;
+  border-left: 1px solid var(--border-color);
+  border-top: 1px solid var(--border-color);
+}
+
+.base-container  .tr {
+  display: flex;
+  width: 1200px;
+  border-bottom: 1px solid var(--border-color);
+}
+
+.base-container  .tr>div {
+  flex-shrink: 0;
+  border-right: 1px solid var(--border-color);
+}
+
+.base-container  .th {
+  width: 200px;
+  height: 60px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  padding-right: 10px;
+  background: #f8f9fb;
+}
+
+.base-container  .td {
+  width: 400px;
+  height: 60px;
+  padding-left: 10px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  background: #ffffff;
+}
+
+.base-container  .th-1 {
+  width: 200px;
+  line-height: 3em;
+  padding-right: 10px;
+  background: #f8f9fb;
+  text-align: right;
+}
+
+.base-container  .td-1 {
+  width: 1000px;
+  padding: 10px;
+  background: #ffffff;
+}
+
+.base-container  .td-1>p {
+  padding: 5px;
+  margin: 0;
+}
+
+/* 项目基本信息中的内容表格 */
+.itembaseinfo-content-container {
+  width: 100%;
+  height: 100%;
+  padding-left: 32px;
+}
+
+.itembaseinfo-content-container .search-bar {
+  width: 100%;
+  height: 60px;
+}
+
+.itembaseinfo-content-container .content-area {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 20px;
+  height: calc(100% - 80px);
+  width: calc(100% - 8px);
+  padding: 12px;
+  overflow-y: auto;
+
+}
+
+.itembaseinfo-content-container .table-container {
+  width: 1200px;
+  border-collapse: collapse;
+}
+
+.itembaseinfo-content-container   th {
+  background: #f5f6fa;
+}
+
+.itembaseinfo-content-container   td,
+.itembaseinfo-content-container   th {
+  border: 1px solid #d8dae3;
+  padding: 20px;
+}
+
+.itembaseinfo-content-container   tr:nth-child(odd) {
+  background: #fafafa;
+}
+
+.itembaseinfo-content-container   td[data-num]::before {
+  content: attr(data-num);
+  width: 100%;
+  font-size: inherit;
+  color: #333;
+  padding-right: 5px;
+}
+
+.itembaseinfo-content-container   td.layer-2 {
+  text-indent: 2em;
+}
+
+.itembaseinfo-content-container   td.danger {
+  color: #ff0000;
+}
+
+.itembaseinfo-item-container {
+  width: 573px;
+  height: 170px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: #fafbfe;
+  border-radius: 4px;
+  position: relative;
+  cursor: pointer;
+  border: 1px solid #dddfe6;
+  --header-display: none;
+  --header-color: #999;
+}
+
+.itembaseinfo-item-container:hover {
+  --header-display: flex;
+}
+
+.itembaseinfo-item-container:hover::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #dddfe6;
+}
+
+.itembaseinfo-item-container .active {
+  border: 1px solid #ddd;
+}
+
+.itembaseinfo-item-container .action-bar {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 0;
+  right: 0;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.itembaseinfo-item-container .action-bar:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+
+.itembaseinfo-item-container .header {
+  padding: 10px 60px 2px 20px;
+}
+
+.itembaseinfo-item-container .title {
+  font-size: 22px;
+  color: #333;
+  line-height: 2em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.itembaseinfo-item-container .body {
+  padding: 0 var(--space-size, 20px);
+  box-sizing: border-box;
+  color: #333;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.itembaseinfo-item-container .body>div {
+  height: 100px;
+}
+
+.itembaseinfo-item-container .left {
+  width: 71px;
+  background: #fafbfe;
+  border: 1px solid #f6f6f6;
+}
+
+.itembaseinfo-item-container .left img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: cover;
+  object-fit: cover;
+}
+
+.itembaseinfo-item-container .right {
+  width: calc(100% - 71px);
+  padding: 5px 0 0 20px;
+}
+
+.itembaseinfo-item-container .content {
+  height: 60px;
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.itembaseinfo-item-container .tip {
+  margin-top: 5px;
+  font-size: 18px;
+  color: #666;
+  line-height: 1.8em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+/* 智能识别开始 */
+.ocr-container {
+  width: 100%;
+  height: 100%;
+}
+
+.ocr-container>div {
+  width: 100%;
+}
+
+.ocr-container .content-area {
+  height: calc(100% - 60px);
+  overflow-y: hidden;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.ocr-container .content-area>div {
+  width: 50%;
+  height: 100%;
+  overflow-y: hidden;
+  margin-right: 6px;
+}
+
+.ocr-container .left-side {
+  --scroll-track-color: #e2e4ec;
+}
+
+.ocr-img-box {
+  width: 100%;
+  height: 100%;
+  background-color: #e2e4ec;
+}
+
+.ocr-img-box .img-bar {
+  height: 60px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+  background: #f8f6f7;
+  font-size: 24px;
+  color: #666666;
+  box-sizing: border-box;
+  border-right: 10px solid var(--scroll-track-color);
+}
+
+.ocr-img-box .img-bar>div {
+  width: 50%;
+  flex-shrink: 0;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.ocr-img-box  .action-bar {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  color: #d5d4db;
+  border-top: 0;
+}
+
+.ocr-img-box .img-bar .action-bar .progress {
+  width: 185px;
+  height: 4px;
+  border-radius: 2px;
+  background: #d5d4db;
+  margin-right: 20px;
+  position: relative;
+  cursor: pointer;
+}
+
+.ocr-img-box .img-bar .action-bar .progress .buoy {
+  position: absolute;
+  width: 18px;
+  height: 18px;
+  border-radius: 50%;
+  border: 2px solid #c3c6ca;
+  top: 50%;
+  transform: translateY(-50%);
+  left: 0;
+  background: #fff;
+  cursor: pointer;
+}
+
+.ocr-img-box .img-bar .action-bar .icon-container {
+  cursor: pointer;
+}
+
+.ocr-img-box .img-viewer {
+  height: calc(100% - 60px);
+  width: calc(100% - 10px);
+  position: relative;
+  overflow: hidden;
+  background: #e2e4ec;
+}
+
+.ocr-img-range-box input[type=range] {
+  position: relative;
+  z-index: 12;
+  -webkit-appearance: none;
+  /*清除系统默认样式*/
+  width: 185px;
+  height: 4px;
+  border-radius: 2px;
+  background: #d5d4db;
+  background-size: 75% 100%;
+}
+
+/*拖动块的样式*/
+.ocr-img-range-box input[type=range]::-webkit-slider-thumb {
+  -webkit-appearance: none;
+  /*清除系统默认样式*/
+  width: 18px;
+  height: 18px;
+  background: #fff;
+  /*拖动块背景*/
+  border-radius: 50%;
+  /*外观设置为圆形*/
+  border: 2px solid #c3c6ca;
+}
+
+.ocr-img-range-box {
+  position: relative;
+  width: 185px;
+  display: flex;
+  align-items: center;
+  /* 垂直居中 */
+}
+
+.ocr-img-range-box .line {
+  width: 5px;
+  height: 18px;
+  background-color: #d5d4db;
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+  height: 18px;
+}
+
+.ocr-img-range-box {
+  display: flex;
+  align-items: center;
+}
+
+.zoomable-img {
+  object-fit: contain;
+  transition: transform 0.3s, width 0.3s, height 0.3s;
+}
+
+.ocr-img-box .img-viewer .img-box {
+  width: 100%;
+  overflow-y: hidden;
+  background: white;
+}
+
+.ocr-img-box .img-viewer .img-box::-webkit-scrollbar {
+  width: 8px;
+  height: 8px;
+}
+
+.ocr-img-box .img-viewer .img-box::-webkit-scrollbar-thumb {
+  border-radius: 4px;
+  background: #575d6d;
+}
+
+.ocr-img-box .img-viewer .img-box::-webkit-scrollbar-track {
+  background: var(--scroll-track-color);
+}
+
+.ocr-img-box .img-viewer .img-box>img {
+  user-drag: none;
+  -webkit-user-drag: none;
+  -khtml-user-drag: none;
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  user-select: none;
+  margin: 0 auto;
+  display: block;
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+.ocr-container .content-area .right-side {
+  overflow-y: auto;
+
+  padding: 20px;
+}
+
+.ocr-container .content-area .table-container {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.ocr-container .content-area .table-container.table-header {
+  margin-bottom: 20px;
+}
+
+.ocr-container .table-container th,
+.ocr-container .table-container td {
+  border: 1px solid #e2e4ec;
+}
+
+.ocr-container .table-container th {
+  width: 30%;
+  text-align: right;
+  font-weight: normal;
+  background: #f8f9fb;
+  padding: 15px;
+  color: #666666;
+  font-size: 16px;
+}
+
+.ocr-container .table-container td {
+  width: 70%;
+  padding: 0;
+}
+
+.ocr-container .table-container td>input {
+  width: 100%;
+  outline: none;
+  padding: 15px;
+  line-height: 1em;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+}
+
+.ocr-container .table-container td>textarea {
+  width: 100%;
+  height: 120px;
+  resize: none;
+  outline: none;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+  padding: 15px;
+}
+
+.ocr-container .table-container td>select {
+  width: 100%;
+  outline: none;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+  padding: 15px;
+}
+
+
+
+.ocr-container .content-box {
+  height: calc(100% - 60px);
+  overflow-y: auto;
+  padding: 20px;
+}
+
+.ocr-container .table-container {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.ocr-container .table-container th,
+.ocr-container .table-container td {
+  border: 1px solid #e2e4ec;
+}
+
+.ocr-container .table-container th {
+  width: 30%;
+  text-align: right;
+  font-weight: normal;
+  background: #f8f9fb;
+  padding: 15px;
+  color: #666666;
+  font-size: 16px;
+}
+
+.ocr-container .table-container td {
+  width: 70%;
+  padding: 0;
+}
+
+.ocr-container .table-container td>input {
+  width: 100%;
+  outline: none;
+  padding: 15px;
+  line-height: 1em;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+}
+
+.ocr-container .table-container td>textarea {
+  width: 100%;
+  height: 120px;
+  resize: none;
+  outline: none;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+  padding: 15px;
+}
+
+.ocr-container .table-container td>select {
+  width: 100%;
+  outline: none;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+  padding: 15px;
+}
+
+.ocr-container .table-container td>div.p {
+  font-size: 16px;
+  color: #333333;
+  padding: 20px 22px;
+}
+
+.ocr-container .table-container td>div.img {
+  height: 120px;
+  display: block;
+  padding: 8px 22px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.ocr-container .table-container td>div.img>img {
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+/* 智能识别结束 */
+/* 智能识别列表开始 */
+.ocr-list-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  position: relative;
+}
+
+.ocr-list-container>div {
+  height: 100%;
+}
+
+.ocr-list-container .left-side {
+  width: 220px;
+}
+
+.ocr-list-container .left-side .action-bar {
+  border-right: 2px solid var(--action-bar-bg);
+  height: 60px;
+  --action-bar-bg: #afb8d0;
+  --action-bar-color: #fff;
+  background: var(--action-bar-bg);
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 20px;
+  color: var(--action-bar-color);
+  font-size: 18px;
+  cursor: pointer;
+}
+
+.ocr-list-container .left-side .action-bar:hover {
+  --action-bar-bg: #242835;
+  --action-bar-color: #fff;
+}
+
+.ocr-list-container .left-side>div {
+  background: #edf1f5;
+}
+
+.ocr-list-container .left-side .content-box {
+  border-right: 2px solid #e2e4ec;
+  height: calc(100% - 60px);
+  overflow-y: auto;
+  padding: 20px 10px;
+}
+
+.ocr-list-container .left-side  .item {
+  width: 100%;
+  height: 143px;
+  border: 1px solid #e2e4ec;
+  margin-bottom: 25px;
+  background: #f8f9fb;
+  position: relative;
+  cursor: pointer;
+  padding: 2px 0;
+}
+
+.ocr-list-container .left-side  .thumb {
+  height: 100px;
+  width: 100%;
+  overflow: hidden;
+}
+
+.ocr-list-container .left-side .thumb>img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+.ocr-list-container .left-side .title {
+  color: #333333;
+  font-size: 16px;
+  padding: 10px;
+}
+
+.ocr-list-container .left-side .status-tip {
+  position: absolute;
+  width: 28px;
+  height: 28px;
+  bottom: 0;
+  right: 0;
+}
+
+.ocr-list-container .left-side .status-tip.error {
+  color: #ff0000;
+}
+
+.ocr-list-container .left-side .status-tip.warn {
+  color: #ffa800;
+}
+
+.ocr-list-container .left-side .status-tip.success {
+  color: #3cb700;
+}
+
+.ocr-list-container .left-side .item.active {
+  outline: 8px solid #afb8d0;
+}
+
+.ocr-list-container .left-side .item:not(.ocr-list-container .left-side .item.active):hover {
+  outline: 15px solid #dddfe6;
+}
+
+.ocr-list-container .right-side {
+  width: calc(100% - 220px);
+}
+
+.ocr-list-container .right-side .content-box {
+  height: calc(100% - 60px);
+  overflow-y: auto;
+  padding: 20px;
+}
+
+.ocr-list-container .right-side .table-container {
+  width: 100%;
+  border-collapse: collapse;
+}
+
+.ocr-list-container .right-side .table-container.table-header {
+  margin-bottom: 20px;
+}
+
+.ocr-list-container .right-side .table-container tr.hide {
+  visibility: hidden;
+}
+
+.ocr-list-container .right-side .table-container tr.hide * {
+  padding: 0 !important;
+  margin: 0 !important;
+  border: unset !important;
+  outline: unset !important;
+}
+
+.ocr-list-container .right-side .table-container th,
+.ocr-list-container .right-side .table-container td {
+  border: 1px solid #e2e4ec;
+}
+
+.ocr-list-container .right-side .table-container th {
+  width: 200px;
+  text-align: right;
+  font-weight: normal;
+  background: #f8f9fb;
+  padding: 15px;
+  color: #666666;
+  font-size: 16px;
+}
+
+.ocr-list-container .right-side .table-container td {
+  padding: 0;
+}
+
+.ocr-list-container .right-side .table-container td:not([colspan]) {
+  width: 360px;
+}
+
+.ocr-list-container .right-side .table-container td[colspan="3"] {
+  width: calc(100% - 200px);
+}
+
+.ocr-list-container .right-side .table-container td>input {
+  width: 100%;
+  outline: none;
+  padding: 15px;
+  line-height: 1em;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+}
+
+.ocr-list-container .right-side .table-container td>textarea {
+  width: 100%;
+  height: 120px;
+  resize: none;
+  outline: none;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+  padding: 15px;
+}
+
+.ocr-list-container .right-side .table-container td>select {
+  width: 100%;
+  outline: none;
+  border: unset;
+  color: #333333;
+  font-size: 16px;
+  padding: 15px;
+}
+
+/* 智能识别列表结束 */
+/* 成功弹窗开始 */
+.success-popup {
+  position: absolute;
+  bottom: var(--message-dialog-bottom, calc(100% + 5px));
+  right: var(--message-dialog-right, 0);
+  width: 459px;
+  height: 150px;
+  background: #ffffff;
+  border: 1px solid #ade791;
+  box-shadow: 10px 10px 5px 0px rgba(61, 187, 0, 0.3);
+  border-radius: 4px;
+  color: #333;
+  font-size: 18px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.success-popup>div.left {
+  width: 180px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.success-popup .icon {
+  width: 82px;
+  height: 82px;
+  background: #3dbb00;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  color: #fff;
+}
+
+.success-popup>.right {
+  width: calc(100% - 180px);
+}
+
+.success-popup>.right>div {
+  text-align: left;
+}
+
+.success-popup .title {
+  font-size: 36px;
+  color: #666666;
+}
+
+.success-popup .desc {
+  margin-top: 10px;
+  font-size: 24px;
+  color: #333;
+}
+
+/* 失败弹窗开始 */
+.errorDialog {
+  position: absolute;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  width: 587px;
+  height: 300px !important;
+  background: #ffffff;
+  border: 1px solid #e1adad;
+  box-shadow: 0px 10px 5px 0px rgba(236, 59, 56, 0.3);
+  border-radius: 4px;
+  color: #333;
+  font-size: 18px;
+}
+
+.errorDialog .body {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  padding: 50px 20px;
+}
+
+.errorDialog .left {
+  width: 180px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.errorDialog .icon {
+  width: 82px;
+  height: 82px;
+  background: #ec3b38;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  border-radius: 50%;
+  color: #fff;
+}
+
+.errorDialog .body>.right {
+  width: calc(100% - 180px);
+}
+
+.errorDialog .body>.right>div {
+  text-align: left;
+}
+
+.errorDialog .title {
+  font-size: 36px;
+  color: #666666;
+}
+
+.errorDialog .desc {
+  margin-top: 10px;
+  font-size: 24px;
+  color: #333;
+}
+
+.errorDialog .footer {
+  padding: 10px 40px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.errorDialog .btn {
+  cursor: pointer;
+  width: 120px;
+  height: 42px;
+  background: #ef5d0c;
+  border: 1px solid #f5904b;
+  border-radius: 4px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 15px;
+  color: #fff;
+}
+
+/* 审核开始 */
+.verify-container {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.verify-container>div {
+  height: 100%;
+}
+
+/* 左侧主内容区域 */
+.verify-container .left-side {
+  width: 100%; /* 默认状态下,右侧只占10px */
+  transition: width 0.3s ease;
+  position: relative;
+}
+
+.verify-container .left-side>div {
+  border-right: 2px solid #e2e4ec;
+}
+
+.verify-container  .content-box {
+  height: calc(100% - 60px * 2 - 30px);
+  padding: 20px 24px;
+  border-bottom: 1px solid #e2e4ec;
+  overflow-y: auto;
+}
+
+.verify-container .content-box>div {
+  background: #ffffff;
+  border: 1px solid #dddfe6;
+  border-radius: 4px;
+  padding: 24px;
+  position: relative;
+}
+
+.verify-container .content-box>div:not(:last-child) {
+  margin-bottom: 20px;
+}
+
+.verify-container .handle-bar {
+  position: absolute;
+  top: 1px;
+  left: 1px;
+  font-weight: bold;
+  cursor: pointer;
+}
+
+.verify-container .member-box .box-header .title {
+  color: #333;
+  font-size: 18px;
+  line-height: 1.8em;
+}
+
+.verify-container .member-box .box-body {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  gap: 6px;
+}
+
+.verify-container .box-body>div {
+  height: 112px;
+}
+
+.verify-container .avatar {
+  width: 81px;
+}
+
+.verify-container .left-side .box-body .avatar>img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: contain;
+  object-fit: contain;
+}
+
+.verify-container .left-side .box-body .info {
+  width: calc(100% - 81px);
+  padding-left: 24px;
+}
+
+.verify-container .left-side .box-body .info .table {
+  padding: 5px 0 15px 0;
+}
+
+.verify-container .left-side .box-body .info .table>table {
+  border-collapse: collapse;
+}
+
+.verify-container .left-side .box-body .info .table>table td {
+  border: 1px solid #dddfe6;
+  padding: 5px 32px;
+  text-align: center;
+  color: #333;
+  font-size: 16px;
+}
+
+.verify-container .left-side .box-body .info .description {
+  color: #666;
+  font-size: 16px;
+}
+
+.verify-container .left-side .zhengshu-box .box-header .title {
+  color: #333;
+  font-size: 18px;
+  line-height: 1.8em;
+}
+
+.verify-container .left-side .zhengshu-box .box-body {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.verify-container .left-side .box-body>div {
+  height: 103px;
+}
+
+.verify-container .left-side .avatar {
+  width: 145px;
+}
+
+.verify-container .left-side .avatar>img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: cover;
+  object-fit: cover;
+}
+
+.verify-container .left-side .info {
+  padding-left: 24px;
+  width: calc(100% - 145px);
+}
+
+.verify-container .left-side .p {
+  font-size: 16px;
+  color: #333;
+  padding: 5px 0 15px 0;
+}
+
+.verify-container .left-side .p>p {
+  line-height: 1.5em;
+  padding: 0;
+  margin: 0;
+}
+
+.verify-container .left-side .tip {
+  font-size: 16px;
+  color: #666666;
+}
+
+.verify-container .action-bar-1 {
+  border-top: 1px solid #e2e4ec;
+  position: absolute;
+  width: 100%;
+  bottom: 60px;
+  height: 90px;
+  background: #ffffff;
+}
+
+.verify-container .action-list {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  height: 50px;
+  padding: 0 28px;
+  gap: 28px;
+}
+
+.verify-container .action-list>.span {
+  cursor: pointer;
+}
+
+.verify-container .input-area {
+  height: 40px;
+  padding: 0 28px;
+}
+
+.verify-container .input-area>textarea,
+.verify-container .input-area>input {
+  padding: 0;
+  line-height: 40px;
+  width: 100%;
+  height: 100%;
+  resize: none;
+  font-size: 18px;
+  outline: unset;
+  border: unset;
+  background: unset;
+}
+
+.verify-container .action-bar-2 {
+  height: 50px;
+  background: #ffffff;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 10px 20px 10px 20px;
+  gap: 20px;
+  color: #999999;
+  font-size: 18px;
+}
+
+.verify-container .action-bar-2>button {
+  font-size: inherit;
+  --action-bar-bg: transparent;
+  --action-bar-color: inherit;
+  --action-bar-border: #bfc1c6;
+  background: var(--action-bar-bg);
+  border: 1px solid var(--action-bar-border);
+  color: var(--action-bar-color);
+  outline: none;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 10px;
+  padding: 5px 20px;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.verify-container .btn-primary:hover {
+  --action-bar-bg: #242835;
+  --action-bar-border: #242835;
+  --action-bar-color: #fff;
+}
+
+.verify-container .btn-danger:hover {
+  --action-bar-bg: #f19149;
+  --action-bar-color: #fff;
+  --action-bar-border: #f19149;
+}
+
+.verify-container .right-side {
+  width: 16px; 
+  height: 100%;
+  position: absolute;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  background: transparent;
+  transition: width 0.3s ease;
+  overflow: hidden; /* 超出部分隐藏 */
+  z-index: 10001; /* 确保在左侧内容之上 */
+}
+
+.verify-container .right-side-div {
+  width: 280px;
+  overflow-y: auto;
+  background: #edf1f5;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+}
+.verify-container .right-side-div .header {
+  border-bottom: 2px solid #e2e4ec;
+}
+
+/* 右侧边栏 - 展开状态 */
+.verify-container .right-side.expanded {
+  width: 296px;
+}
+
+.verify-container .right-side .btn-toggle::before{
+	content: "";
+    position: absolute;
+    left: -8px;
+    top: -4px;
+    width: 20px;
+    height: 20px;
+    transform-origin: 100% 100%;
+    transform: rotate(49deg);
+    z-index: 1;
+    border-left: 1px solid #d2d2d2;
+    background: #ffffff;
+}
+.verify-container .right-side .btn-toggle {
+  position: absolute;
+  bottom: 25%;
+  left: 0;
+  transform: translateY(30%);
+  width: 16px;
+  height: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  z-index: 10;
+  border:1px solid #d2d2d2;
+  border-right: none;
+  border-top: none;
+  background: #ffffff;
+}
+.verify-container .right-side .icon-container{
+	background: #fff;
+	position:relative;
+	z-index: 1000;
+}
+/* 右侧内容容器 - 只在展开状态显示 */
+.verify-container .right-side-content {
+  opacity: 0;
+  transition: opacity 0.2s ease;
+  width: 100%;
+  height: 100%;
+  padding-left: 16px; /* 留出箭头区域 */
+  box-sizing: border-box;
+  position: relative;
+  z-index: 1000;
+}
+
+/* 展开状态下显示内容 */
+.verify-container .right-side.expanded .right-side-content {
+  opacity: 1;
+}
+
+.verify-nodes {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px 0;
+  margin-right: 6px;
+}
+
+.verify-nodes .group-item {
+  margin-bottom: 12px;
+  padding: 8px 0 8px 20px;
+}
+
+.verify-nodes .group-item {
+  position: relative;
+}
+/* .verify-nodes .group-item::before {
+  position: absolute;
+  content: "";
+  width: 0;
+  height: 50%;
+  border-left: 1px solid #a0a0a0;
+  top: 37px;
+  left: 30px;
+}*/
+.verify-nodes .group-item:not(:last-child)::after {
+  position: absolute;
+  content: "";
+  width: 0;
+  height: calc(100% - 20px);
+  border-left: 1px solid #a0a0a0;
+  top: 37px;
+  left: 30px;
+} 
+.verify-nodes .group-item-last-open::after {
+  position: absolute;
+  content: "";
+  width: 0;
+  height: var(--group-line-height);
+  border-left: 1px solid #a0a0a0;
+  top: 37px;
+  left: 30px;
+}
+.verify-nodes .group-item-title {
+  cursor: pointer;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 10px;
+}
+
+.verify-nodes .group-item .icon {
+  color: #666666;
+  position: relative;
+}
+
+.verify-nodes .group-item .num {
+  position: absolute;
+  top: 60%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  color: #999999;
+  font-size: 14px;
+}
+
+.verify-nodes .group-item-children {
+
+  padding-left: 20px;
+}
+
+.verify-node-container {
+  padding: 16px 0;
+}
+
+.verify-node-container>.info {
+  padding: 0 10px;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  height: 60px;
+}
+
+
+.verify-node-container>.info>.avatar {
+  width: 50px;
+  height: 50px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.verify-node-container>.info>.desc {
+  width: calc(100% - 60px - 60px);
+  color: #333333;
+  font-size: 14px;
+  line-height: 1.6em;
+  padding: 0 10px;
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+
+.verify-node-container>.info>.desc>div {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.verify-node-container>.info>.link {
+  width: 60px;
+}
+
+.verify-node-container>.info>.link>div {
+  padding-top: 30px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 10px;
+  color: #555c6b;
+}
+
+.verify-node-container .description {
+  /* margin-top: 5px; */
+  padding: 10px;
+}
+
+.verify-node-container .description.link {
+  position: relative;
+}
+
+.verify-node-container .description.link::before {
+  position: absolute;
+  width: 20px;
+  content: "";
+  height: 0;
+  border-bottom: 1px solid #a0a0a0;
+  top: 30px;
+  left: -10px;
+}
+
+.verify-node-container .description.link::after {
+  position: absolute;
+  width: 16px;
+  content: attr(data-num);
+  height: 16px;
+  border-radius: 50%;
+  background: #edf1f5;
+  border: 1px solid #a0a0a0;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  font-size: 12px;
+  top: 21px;
+  left: -18px;
+  z-index: 100;
+}
+
+.verify-node-container .description>div {
+  width: 100%;
+  background: #fff;
+  border-radius: 3px;
+  position: relative;
+  padding: 8px 17px;
+  color: #333333;
+  font-size: 14px;
+}
+
+.verify-node-container .description>div::before {
+  content: "";
+  width: 10px;
+  height: 10px;
+  background: #fff;
+  position: absolute;
+  left: 26px;
+  top: -5px;
+  transform: rotate(45deg);
+  border-radius: 2px;
+}
+
+.verify-node-container .time {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  color: #666666;
+  font-size: 14px;
+  padding: 0 10px;
+}
+
+/* 审核结束 */
+/* 知识库弹窗查看项目 */
+.know-item-container {
+  width: 587px;
+  height: 157px;
+  box-shadow: 2px 2px 5px #e7e8e8;
+  background: #fafbfe;
+  border-radius: 4px;
+  position: relative;
+  cursor: pointer;
+  --header-display: none;
+  --header-color: #999;
+}
+
+.know-item-container:hover {
+  --header-display: flex;
+}
+
+.know-item-container:hover::after {
+  --border-size: 8px;
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  top: calc(-1 * var(--border-size));
+  left: calc(-1 * var(--border-size));
+  content: "";
+  border-radius: 4px;
+  pointer-events: none;
+  box-sizing: content-box;
+  border: var(--border-size) solid #dddfe6;
+}
+
+.know-item-container.active {
+  border: 1px solid #ddd;
+}
+
+.know-item-container .header {
+  width: 48px;
+  height: 48px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  display: var(--header-display);
+  color: var(--header-color);
+  top: 0;
+  right: 0;
+  position: absolute;
+  border-top-right-radius: 4px;
+}
+
+.know-item-container .header:hover {
+  --header-color: #fff;
+  background: #575d6d;
+}
+
+.know-item-container .body {
+  padding: 0px 25px;
+}
+
+.know-item-container .box-header {
+  height: 50px;
+  padding: 15px 0 5px 0;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.know-item-container .box-header .title {
+  width: 100%;
+  color: #333333;
+  font-size: 22px;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 10px;
+}
+
+.know-item-container .box-header .title>div {
+  flex-shrink: 0;
+}
+
+.know-item-container .box-header .title .p {
+  width: calc(100% - 50px);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.know-item-container .box-body {
+  height: 57px;
+  color: #333;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  --right-padding-left: 12px;
+  --right-width: calc(100% - 100px);
+}
+
+.know-item-container .no-thumb {
+  --right-padding-left: 0;
+  --right-width: 100%;
+}
+
+.know-item-container .left {
+  width: 100px;
+  height: 100%;
+}
+
+.know-item-container .left>img {
+  width: 100%;
+  height: 100%;
+  -o-object-fit: cover;
+  object-fit: cover;
+  display: block;
+}
+
+.know-item-container .right {
+  width: var(--right-width);
+  padding-left: var(--right-padding-left);
+}
+
+.know-item-container .right .title {
+  font-size: 16px;
+  color: #666666;
+  line-height: 24px;
+  display: -webkit-box;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+}
+
+.know-item-container .box-footer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+  height: 47px;
+}
+
+.know-item-container .box-footer>div {
+  width: 100%;
+}
+
+.know-item-container .box-footer .info {
+  margin-top: 5px;
+  font-size: 14px;
+  color: #999999;
+  line-height: 1.2em;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  gap: 20px;
+}
+
+.know-item-container .progress {
+  margin-top: 2px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  --status-color: #3172f1;
+}
+
+.know-item-container .progress.error {
+  --status-color: #ff0000;
+}
+
+.know-item-container .progress.waiting {
+  --status-color: #3172f1;
+}
+
+.know-item-container .status {
+  font-size: 12px;
+  padding-right: 1em;
+  color: var(--status-color);
+}
+
+.know-item-container .line {
+  flex: 1;
+  padding: 10px 0;
+  position: relative;
+}
+
+.know-item-container .line::after {
+  content: "";
+  background: #edf1f5;
+  width: 100%;
+  height: 2px;
+  border-radius: 1px;
+  position: absolute;
+  top: 50%;
+  left: 0;
+  transform: translateY(-50%);
+  z-index: 1;
+}
+
+.know-item-container .line>div {
+  z-index: 2;
+  width: 100%;
+  height: 2px;
+  border-radius: 1px;
+  position: absolute;
+  top: 50%;
+  left: 0;
+  transform: translateY(-50%);
+  background: var(--status-color);
+}
+
+.know-item-container .bar {
+  color: #c3c7cb;
+  padding-left: 1em;
+}
+
+/* 增加预警 */
+.editor-container {
+  width: 100%;
+  height: 100%;
+}
+
+.editor-container .content-box {
+  height: calc(100% - 60px);
+  padding: 25px 24px;
+}
+
+.editor-container .header {
+  height: 172px;
+}
+
+.editor-container .header>table {
+  border-collapse: collapse;
+}
+
+.editor-container .header th,
+.editor-container .header td {
+  border: 1px solid #e2e4ec;
+  padding: 20px;
+}
+
+.editor-container .header th {
+  width: 160px;
+  background: #f8f9fb;
+  color: #666666;
+  font-size: 16px;
+  text-align: right;
+  font-weight: normal;
+}
+
+.editor-container .header td {
+  color: var(--td-default-color, #333);
+  font-size: 16px;
+}
+
+.editor-container .desc {
+  --td-default-color: #666;
+}
+
+.editor-container .content-box .header select {
+  width: 100px;
+  font-size: inherit;
+  height: 100%;
+  outline: unset;
+  border: unset;
+  color: #999;
+}
+
+.editor-container .content-box .body {
+  height: calc(100% - 172px);
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+  gap: 20px;
+}
+
+.editor-container .content-box .body>div {
+  width: 50%;
+  height: 100%;
+  border: 1px solid #e7e8e8;
+  box-shadow: 2px 3px 5px 0px rgba(0, 0, 0, 0.06);
+}
+
+.editor-container .content-box .body>div>div {
+  width: 100%;
+}
+
+.editor-container .item-header {
+  height: 60px;
+  border-bottom: 1px solid #eaeaea;
+  padding: 0 20px;
+  font-size: 22px;
+  color: #4d4d4d;
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.editor-container .item-header-left {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  gap: 10px;
+}
+
+.editor-container .item-header-left .icon-container {
+  color: #989898;
+}
+
+.editor-container .item-header-right {
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 5px;
+}
+
+.editor-container .item-header-right input {
+  width: 100px;
+  height: 24px;
+  border: 1px solid #eaeaea;
+  border-radius: 2px;
+  outline: none;
+  padding: 0 10px;
+}
+
+.editor-container .item-header-right .icon-container {
+  color: #c3c7cb;
+}
+
+.editor-container .item-body {
+  padding-right: 6px;
+  height: calc(100% - 60px);
+  overflow-y: auto;
+}
+
+.editor-container .box-body {
+  width: 100%;
+  height: 100%;
+  overflow-y: auto;
+  overflow: hidden;
+  border: 1px solid #e2e4ec;
+}
+
+.editor-container .content-area {
+  width: 100%;
+}
+
+.editor-container .action-bar {
+  height: 60px;
+  color: #c3c6ca;
+  border-bottom: 1px solid #dedfdf;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+}
+
+.editor-container .action-bar .group {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-start;
+  align-items: center;
+  position: relative;
+}
+
+.editor-container .action-bar .group::after {
+  content: "";
+  width: 0;
+  height: 60%;
+  top: 50%;
+  position: absolute;
+  right: 0;
+  border-right: 1px solid #efeff0;
+  transform: translateY(-50%);
+}
+
+.editor-container .action-bar .group>div {
+  width: 50px;
+  height: 60px;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+}
+
+.editor-container .content-area {
+  height: calc(100% - 60px);
+}
+
+.editor-container .content-area textarea {
+  width: 100%;
+  height: 100%;
+  resize: none;
+  outline: unset;
+  border: unset;
+  padding: 10px 20px;
+  font-size: 16px;
+  color: #333;
+}
+
+/* 弹窗底部按钮样式 */
+.bottom-div{
+  height: 60px;
+  background: white;
+  border-top: 1px solid #e2e4ec;
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 0 20px;
+  gap: 20px;
+  color: #999999;
+  font-size: 18px;
+  position: absolute;
+  width: 100%;
+  bottom: 0;
+  background: #fff !important;
+}
+
+.bottom-div button {
+  font-size: 18px;
+  --bottom-div-bg: transparent;
+  --bottom-div-color: inherit;
+  --bottom-div-border: #bfc1c6;
+  background: var(--bottom-div-bg);
+  border: 1px solid var(--bottom-div-border);
+  color: var(--bottom-div-color);
+  outline: none;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  gap: 5px;
+  padding: 5px 20px;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.bottom-div button:hover {
+  --bottom-div-bg: #242835;
+  --bottom-div-color: #fff;
+}
+.bottom-div  .bottom-button-reject:hover{
+  --bottom-div-bg: #e65505 ;
+  --bottom-div-color: #fff;
+  --bottom-div-border: #e65505;
+}
+
+/* 下拉按钮开始 */
+.ss-drop-button {
+  height: 36px;
+  font-size: 16px;
+  color: #999;
+  display: flex;
+  flex-direction: row;
+  justify-content: center;
+  align-items: center;
+  padding: 0 20px;
+  border-radius: 4px;
+  margin: 0;
+  /* line-height: 2em; */
+  cursor: pointer;
+  background: var(--btn-bg, unset);
+  color: var(--btn-color, #999999);
+  border: 1px solid var(--btn-border-color, #c4c7cc);
+  position: relative;
+}
+
+.ss-drop-button:hover {
+  --btn-bg: #393d51;
+  --btn-color: #fff;
+}
+
+.ss-drop-button .popup {
+  position: absolute;
+  top: calc(100% + 12px);
+  right: 0;
+  background-color: #393d51;
+  border-radius: 5px;
+  padding: 10px 4px;
+  z-index: 10;
+  height: auto;
+}
+
+.ss-drop-button .popup::before {
+  content: "";
+  position: absolute;
+  top: -12px;
+  /* 与弹窗的 top: calc(100% + 12px) 对应 */
+  left: 0;
+  right: 0;
+  height: 12px;
+  background: transparent;
+  /* 透明背景 */
+}
+
+.ss-drop-button .popup::after {
+  content: "";
+  width: 0.8em;
+  height: 0.8em;
+  background: #393d51;
+  position: absolute;
+  right: 1.5em;
+  top: -0.4em;
+  transform: rotate(45deg);
+
+}
+
+.ss-drop-button .popup div {
+  padding: 5px 15px;
+  cursor: pointer;
+  white-space: nowrap;
+}
+
+.ss-drop-button .popup div:hover {
+  background: #fffdfd;
+  color: #333;
+}
+.ss-drop-button-more::before{
+  width: 6px;
+  height: 6px;
+  content: '';
+  border-radius: 50%;
+  background-color: #8e8e8e;
+  position: absolute;
+  right: 4px;
+  top: 4px;
+}
+/* 下拉按钮结束 */
+/* 通用查询卡片下拉按钮开始 */
+.cart-list-button-popup{
+  position: absolute;
+  top: calc(100% + 8px);
+  right: -8px;
+  background-color: #393d51;
+  border-radius: 5px;
+  padding: 10px 4px;
+  z-index: 10;
+}
+.cart-list-button-popup::before {
+  content: "";
+  position: absolute;
+  top: -10px;
+  left: 0;
+  right: 0;
+  height: 10px;
+  background: transparent;
+}
+
+.cart-list-button-popup::after {
+  content: "";
+  width: 0.6em;
+  height: 0.6em;
+  background: #393d51;
+  position: absolute;
+  right: 1.7em;
+  top: -0.3em;
+  transform: rotate(45deg);
+}
+.cart-list-button-popup>div {
+  padding: 5px 15px;
+  cursor: pointer;
+  white-space: nowrap;
+  font-size: 16px;
+  display: flex;
+  align-items: center;
+}
+.cart-list-button-popup .cart-list-icon{
+  margin-right: 5px;
+  font-size: 16px;
+}
+.cart-list-button-popup>div:hover {
+  background: #fffdfd;
+  color: #333;
+}
+.cart-list-button-popup>div:hover .cart-list-icon::before{
+  color: #333333 !important;
+}
+/* 通用查询卡片下拉按钮结束 */
+
+/* 一级页面面包屑 */
+.bread-crumb{
+  display: flex;
+  align-items: center;
+  position: absolute;
+  left: 20px;
+  gap: 10px;
+  height: 100%;
+  z-index: 111;
+  color: #999999;
+}
+.bread-crumb>div{
+  cursor: pointer;
+}
+
+
+/* 日期选择器样式 */
+
+.el-date-table,.el-time-spinner__item{
+  font-size: 14px;
+}
+.custom-date-picker  .el-icon{
+  font-size: 16px;
+  color: #333;
+}
+.custom-date-picker .el-input__wrapper {
+  box-shadow: none !important;
+  display: flex;
+  align-items: center;
+}
+.custom-date-picker .el-input__inner::placeholder {
+  color: #999;
+
+}
+.custom-date-picker .el-input__inner {
+  font-size: 16px !important;
+  font-weight: normal;
+  height: 32px;
+  line-height: 33px;
+  color: #000;
+}
+.today .el-date-table-cell__text{
+  border: 1px solid #000;
+  border-radius: 50%;
+}
+.el-picker-panel__icon-btn{
+  margin-top: 0;
+}
+.el-date-picker__time-header{
+  padding: 12px 5px;
+  border-bottom: none;
+}
+.el-picker-panel__footer,.el-time-panel__footer{
+  border-top: none;
+  padding: 5px 10px 10px;
+}
+.el-button--small{
+  font-size: 14px;
+  padding: 14px 16px;
+
+}
+.el-time-panel__footer{
+  height: auto;
+  line-height: 25px;
+  display: flex;
+  justify-content: flex-end;
+}
+.el-time-panel__btn{
+  font-size: 14px;
+  height: 24px;
+  padding: 14px 16px;
+  margin:0;
+  line-height: unset;
+  display: flex;
+  align-items: center;
+}
+.el-input--small{
+  font-size: 16px;
+}
+.el-input--small .el-input__wrapper{
+  padding: 8px 7px 6px;
+}
+.jodit-container:not(.jodit_inline){
+  border: none !important;
+}
+/* 搜索区域的日期选择器 */
+.ss-search-date-picker input:focus,.el-picker-panel .el-input__inner:focus{
+  border:1px solid #c4c7cc !important;
+  outline: none;
+}
+.ss-search-date-picker .custom-date-picker  .el-icon{
+  font-size: 16px;
+  color: #333;
+}
+.ss-search-date-picker  .el-input--large .el-input__wrapper{
+  padding:0;
+}
+.ss-search-date-picker .el-input__wrapper {
+  height: 32px;
+  width: 100%;
+  padding:0;
+}
+.ss-search-date-picker .el-input__prefix, .ss-search-date-picker .el-input__suffix {
+  display: none;
+}
+.ss-search-date-picker .custom-date-picker .el-input__inner::placeholder {
+  color: #333;
+
+}
+.ss-search-date-picker .custom-date-picker .el-input__inner {
+  width: 100%;
+  font-size: 14px !important;
+  font-weight: normal;
+  height: 32px;
+  line-height: 33px;
+  font-family: 'SimHei', 'Microsoft YaHei', 'Helvetica Neue', Helvetica;
+}
+.ss-search-date-picker .el-input__inner::placeholder {
+  font-family: 'SimHei', 'Microsoft YaHei', 'Helvetica Neue', Helvetica !important;
+  color: #999999 !important;
+}
+.el-picker-panel .el-input__inner{
+  width: 100% !important;
+}

+ 305 - 0
skin/easy/css/cropper.css

@@ -0,0 +1,305 @@
+/*!
+ * Cropper v4.0.0
+ * https://github.com/fengyuanchen/cropper
+ *
+ * Copyright (c) 2014-2018 Chen Fengyuan
+ * Released under the MIT license
+ *
+ * Date: 2018-04-01T06:26:32.417Z
+ */
+
+ .cropper-container {
+    direction: ltr;
+    font-size: 0;
+    line-height: 0;
+    position: relative;
+    -ms-touch-action: none;
+    touch-action: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+  }
+  
+  .cropper-container img {/*Avoid margin top issue (Occur only when margin-top <= -height)
+   */
+    display: block;
+    height: 100%;
+    image-orientation: 0deg;
+    max-height: none !important;
+    max-width: none !important;
+    min-height: 0 !important;
+    min-width: 0 !important;
+    width: 100%;
+  }
+  
+  .cropper-wrap-box,
+  .cropper-canvas,
+  .cropper-drag-box,
+  .cropper-crop-box,
+  .cropper-modal {
+    bottom: 0;
+    left: 0;
+    position: absolute;
+    right: 0;
+    top: 0;
+  }
+  
+  .cropper-wrap-box,
+  .cropper-canvas {
+    overflow: hidden;
+  }
+  
+  .cropper-drag-box {
+    background-color: #fff;
+    opacity: 0;
+  }
+  
+  .cropper-modal {
+    background-color: #000;
+    opacity: .5;
+  }
+  
+  .cropper-view-box {
+    display: block;
+    height: 100%;
+    outline-color: rgba(51, 153, 255, 0.75);
+    outline: 1px solid #39f;
+    overflow: hidden;
+    width: 100%;
+  }
+  
+  .cropper-dashed {
+    border: 0 dashed #eee;
+    display: block;
+    opacity: .5;
+    position: absolute;
+  }
+  
+  .cropper-dashed.dashed-h {
+    border-bottom-width: 1px;
+    border-top-width: 1px;
+    height: 33.33333%;
+    left: 0;
+    top: 33.33333%;
+    width: 100%;
+  }
+  
+  .cropper-dashed.dashed-v {
+    border-left-width: 1px;
+    border-right-width: 1px;
+    height: 100%;
+    left: 33.33333%;
+    top: 0;
+    width: 33.33333%;
+  }
+  
+  .cropper-center {
+    display: block;
+    height: 0;
+    left: 50%;
+    opacity: .75;
+    position: absolute;
+    top: 50%;
+    width: 0;
+  }
+  
+  .cropper-center:before,
+  .cropper-center:after {
+    background-color: #eee;
+    content: ' ';
+    display: block;
+    position: absolute;
+  }
+  
+  .cropper-center:before {
+    height: 1px;
+    left: -3px;
+    top: 0;
+    width: 7px;
+  }
+  
+  .cropper-center:after {
+    height: 7px;
+    left: 0;
+    top: -3px;
+    width: 1px;
+  }
+  
+  .cropper-face,
+  .cropper-line,
+  .cropper-point {
+    display: block;
+    height: 100%;
+    opacity: .1;
+    position: absolute;
+    width: 100%;
+  }
+  
+  .cropper-face {
+    background-color: #fff;
+    left: 0;
+    top: 0;
+  }
+  
+  .cropper-line {
+    background-color: #39f;
+  }
+  
+  .cropper-line.line-e {
+    cursor: ew-resize;
+    right: -3px;
+    top: 0;
+    width: 5px;
+  }
+  
+  .cropper-line.line-n {
+    cursor: ns-resize;
+    height: 5px;
+    left: 0;
+    top: -3px;
+  }
+  
+  .cropper-line.line-w {
+    cursor: ew-resize;
+    left: -3px;
+    top: 0;
+    width: 5px;
+  }
+  
+  .cropper-line.line-s {
+    bottom: -3px;
+    cursor: ns-resize;
+    height: 5px;
+    left: 0;
+  }
+  
+  .cropper-point {
+    background-color: #39f;
+    height: 5px;
+    opacity: .75;
+    width: 5px;
+  }
+  
+  .cropper-point.point-e {
+    cursor: ew-resize;
+    margin-top: -3px;
+    right: -3px;
+    top: 50%;
+  }
+  
+  .cropper-point.point-n {
+    cursor: ns-resize;
+    left: 50%;
+    margin-left: -3px;
+    top: -3px;
+  }
+  
+  .cropper-point.point-w {
+    cursor: ew-resize;
+    left: -3px;
+    margin-top: -3px;
+    top: 50%;
+  }
+  
+  .cropper-point.point-s {
+    bottom: -3px;
+    cursor: s-resize;
+    left: 50%;
+    margin-left: -3px;
+  }
+  
+  .cropper-point.point-ne {
+    cursor: nesw-resize;
+    right: -3px;
+    top: -3px;
+  }
+  
+  .cropper-point.point-nw {
+    cursor: nwse-resize;
+    left: -3px;
+    top: -3px;
+  }
+  
+  .cropper-point.point-sw {
+    bottom: -3px;
+    cursor: nesw-resize;
+    left: -3px;
+  }
+  
+  .cropper-point.point-se {
+    bottom: -3px;
+    cursor: nwse-resize;
+    height: 20px;
+    opacity: 1;
+    right: -3px;
+    width: 20px;
+  }
+  
+  @media (min-width: 768px) {
+    .cropper-point.point-se {
+      height: 15px;
+      width: 15px;
+    }
+  }
+  
+  @media (min-width: 992px) {
+    .cropper-point.point-se {
+      height: 10px;
+      width: 10px;
+    }
+  }
+  
+  @media (min-width: 1200px) {
+    .cropper-point.point-se {
+      height: 5px;
+      opacity: .75;
+      width: 5px;
+    }
+  }
+  
+  .cropper-point.point-se:before {
+    background-color: #39f;
+    bottom: -50%;
+    content: ' ';
+    display: block;
+    height: 200%;
+    opacity: 0;
+    position: absolute;
+    right: -50%;
+    width: 200%;
+  }
+  
+  .cropper-invisible {
+    opacity: 0;
+  }
+  
+  .cropper-bg {
+    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAK6wGCiw1aAAAAHHRFWHRTb2Z0d2FyZQBBZG9iZSBGaXJld29ya3MgQ1M26LyyjAAAABFJREFUCJlj+M/AgBVhF/0PAH6/D/HkDxOGAAAAAElFTkSuQmCC');
+  }
+  
+  .cropper-hide {
+    display: block;
+    height: 0;
+    position: absolute;
+    width: 0;
+  }
+  
+  .cropper-hidden {
+    display: none !important;
+  }
+  
+  .cropper-move {
+    cursor: move;
+  }
+  
+  .cropper-crop {
+    cursor: crosshair;
+  }
+  
+  .cropper-disabled .cropper-drag-box,
+  .cropper-disabled .cropper-face,
+  .cropper-disabled .cropper-line,
+  .cropper-disabled .cropper-point {
+    cursor: not-allowed;
+  }

File diff suppressed because it is too large
+ 0 - 0
skin/easy/css/element-plus.css


+ 126 - 0
skin/easy/css/font_4273728_tnvxftfb8j.css

@@ -0,0 +1,126 @@
+@font-face {
+    font-family: "common-iconfont"; /* Project id 4273728 */
+    src: 
+        url('../fonts/iconfont_4273728.woff2') format('woff2');
+  }
+  
+  .common-iconfont {
+    font-family: "common-iconfont" !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+  
+  .common-icon-photo:before {
+    content: "\e89c";
+  }
+  
+  .common-icon-arrow-double-right:before {
+    content: "\eb9a";
+  }
+  
+  .common-icon-arrow-double-left:before {
+    content: "\eb9b";
+  }
+  
+  .common-icon-arrow-double-down:before {
+    content: "\eb9c";
+  }
+  
+  .common-icon-arrow-double-up:before {
+    content: "\eb9d";
+  }
+  
+  .common-icon-video:before {
+    content: "\e61e";
+  }
+  
+  .common-icon-smile1:before {
+    content: "\e761";
+  }
+  
+  .common-icon-fontSize:before {
+    content: "\e63f";
+  }
+  
+  .common-icon-paperClip:before {
+    content: "\ea36";
+  }
+  
+  .common-icon-edit:before {
+    content: "\e668";
+  }
+  
+  .common-icon-notice:before {
+    content: "\e614";
+  }
+  
+  .common-icon-site:before {
+    content: "\e67b";
+  }
+  
+  .common-icon-card:before {
+    content: "\e620";
+  }
+  
+  .common-icon-question:before {
+    content: "\e666";
+  }
+  
+  .common-icon-print:before {
+    content: "\e6c9";
+  }
+  
+  .common-icon-full-screen:before {
+    content: "\eb99";
+  }
+  
+  .common-icon-love:before {
+    content: "\e83f";
+  }
+  
+  .common-icon-activity:before {
+    content: "\e621";
+  }
+  
+  .common-icon-layer:before {
+    content: "\e603";
+  }
+  
+  .common-icon-home:before {
+    content: "\e6b8";
+  }
+  
+  .common-icon-double-arrow-right:before {
+    content: "\e624";
+  }
+  
+  .common-icon-slant:before {
+    content: "\e6ae";
+  }
+  
+  .common-icon-close:before {
+    content: "\e723";
+  }
+  
+  .common-icon-close-circle:before {
+    content: "\e725";
+  }
+  
+  .common-icon-menu-square:before {
+    content: "\e651";
+  }
+  
+  .common-icon-menu:before {
+    content: "\eb71";
+  }
+  
+  .common-icon-empty:before {
+    content: "\e708";
+  }
+  
+  .common-icon-add:before {
+    content: "\eb72";
+  }
+  

+ 750 - 0
skin/easy/css/font_4279221_5h4vbt6831w.css

@@ -0,0 +1,750 @@
+@font-face {
+    font-family: "ss-iconfont"; /* Project id 4279221 */
+    src: 
+        url('../fonts/iconfont_4279221.woff2') format('woff2');
+  }
+  
+  .ss-iconfont {
+    font-family: "ss-iconfont" !important;
+    font-size: 16px;
+    font-style: normal;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+  }
+  
+  .ss-icon-quanxuan:before {
+    content: "\e6b6";
+  }
+  
+  .ss-icon-quanbuguoqu:before {
+    content: "\e6b7";
+  }
+  
+  .ss-icon-biaoqian:before {
+    content: "\e6b5";
+  }
+  
+  .ss-icon-grxzFuyongshanggezhi:before {
+    content: "\e6b2";
+  }
+  
+  .ss-icon-grxzMorenzhi:before {
+    content: "\e6b3";
+  }
+  
+  .ss-icon-grxzBuxuan:before {
+    content: "\e6b4";
+  }
+  
+  .ss-icon-yaoyiyaocu:before {
+    content: "\e6b1";
+  }
+  
+  .ss-icon-yaoyiyao:before {
+    content: "\e6b0";
+  }
+  
+  .ss-icon-PE:before {
+    content: "\e6af";
+  }
+  
+  .ss-icon-fenzhi:before {
+    content: "\e6ac";
+  }
+  
+  .ss-icon-xiangliang:before {
+    content: "\e6ad";
+  }
+  
+  .ss-icon-huihe:before {
+    content: "\e6ae";
+  }
+  
+  .ss-icon-linggan:before {
+    content: "\e6ab";
+  }
+  
+  .ss-icon-kaishi:before {
+    content: "\e6a9";
+  }
+  
+  .ss-icon-jieshu:before {
+    content: "\e6aa";
+  }
+  
+  .ss-icon-lieshu:before {
+    content: "\e6a1";
+  }
+  
+  .ss-icon-biaoqing:before {
+    content: "\e68c";
+  }
+  
+  .ss-icon-shanchuhang:before {
+    content: "\e665";
+  }
+  
+  .ss-icon-shanchulie:before {
+    content: "\e697";
+  }
+  
+  .ss-icon-shanchubiaoge:before {
+    content: "\e69a";
+  }
+  
+  .ss-icon-neirongku:before {
+    content: "\e63a";
+  }
+  
+  .ss-icon-charulie:before {
+    content: "\e65c";
+  }
+  
+  .ss-icon-cangdan:before {
+    content: "\e67e";
+  }
+  
+  .ss-icon-charuhang:before {
+    content: "\e662";
+  }
+  
+  .ss-icon-tuozhuai:before {
+    content: "\e601";
+  }
+  
+  .ss-icon-word:before {
+    content: "\e689";
+  }
+  
+  .ss-icon-excl:before {
+    content: "\e63f";
+  }
+  
+  .ss-icon-zhenghe:before {
+    content: "\e636";
+  }
+  
+  .ss-icon-xiuxue:before {
+    content: "\e600";
+  }
+  
+  .ss-icon-bugouxuan:before {
+    content: "\e6a8";
+  }
+  
+  .ss-icon-caozuoshibai1:before {
+    content: "\e69b";
+  }
+  
+  .ss-icon-anjian-1:before {
+    content: "\e6a3";
+  }
+  
+  .ss-icon-bangzhu:before {
+    content: "\e6a4";
+  }
+  
+  .ss-icon-baocunbintijiao:before {
+    content: "\e6a5";
+  }
+  
+  .ss-icon-baocun-youshangjiao:before {
+    content: "\e6a6";
+  }
+  
+  .ss-icon-baocun:before {
+    content: "\e6a7";
+  }
+  
+  .ss-icon-cuiban:before {
+    content: "\e68d";
+  }
+  
+  .ss-icon-biyanjing:before {
+    content: "\e68e";
+  }
+  
+  .ss-icon-a-fangda1:before {
+    content: "\e68f";
+  }
+  
+  .ss-icon-folderShare:before {
+    content: "\e690";
+  }
+  
+  .ss-icon-fenlei:before {
+    content: "\e691";
+  }
+  
+  .ss-icon-baoguandan:before {
+    content: "\e692";
+  }
+  
+  .ss-icon-chaifen:before {
+    content: "\e693";
+  }
+  
+  .ss-icon-diannao:before {
+    content: "\e694";
+  }
+  
+  .ss-icon-dingwei:before {
+    content: "\e695";
+  }
+  
+  .ss-icon-danwei:before {
+    content: "\e696";
+  }
+  
+  .ss-icon-fanhui:before {
+    content: "\e698";
+  }
+  
+  .ss-icon-canshuxiang:before {
+    content: "\e699";
+  }
+  
+  .ss-icon-chexiao:before {
+    content: "\e69c";
+  }
+  
+  .ss-icon-biaoge:before {
+    content: "\e69d";
+  }
+  
+  .ss-icon-buhuanrao:before {
+    content: "\e69e";
+  }
+  
+  .ss-icon-bingtu:before {
+    content: "\e69f";
+  }
+  
+  .ss-icon-caogaoxiang:before {
+    content: "\e6a0";
+  }
+  
+  .ss-icon-anjian:before {
+    content: "\e6a2";
+  }
+  
+  .ss-icon-gerenxuanzhi:before {
+    content: "\e677";
+  }
+  
+  .ss-icon-fanxuan:before {
+    content: "\e678";
+  }
+  
+  .ss-icon-a-chenggong1:before {
+    content: "\e679";
+  }
+  
+  .ss-icon-chongzuo:before {
+    content: "\e67a";
+  }
+  
+  .ss-icon-gongshigonggao:before {
+    content: "\e67b";
+  }
+  
+  .ss-icon-dianji-02:before {
+    content: "\e67c";
+  }
+  
+  .ss-icon-fangda:before {
+    content: "\e67d";
+  }
+  
+  .ss-icon-duihua2:before {
+    content: "\e67f";
+  }
+  
+  .ss-icon-daiban:before {
+    content: "\e680";
+  }
+  
+  .ss-icon-huanfu:before {
+    content: "\e681";
+  }
+  
+  .ss-icon-duomeiti:before {
+    content: "\e682";
+  }
+  
+  .ss-icon-guangbiao:before {
+    content: "\e683";
+  }
+  
+  .ss-icon-guangbiao-tuozhuai:before {
+    content: "\e684";
+  }
+  
+  .ss-icon-dianzan:before {
+    content: "\e685";
+  }
+  
+  .ss-icon-bianqian:before {
+    content: "\e686";
+  }
+  
+  .ss-icon-denglu:before {
+    content: "\e687";
+  }
+  
+  .ss-icon-guanbi_jiaobiao:before {
+    content: "\e688";
+  }
+  
+  .ss-icon-daochu:before {
+    content: "\e68a";
+  }
+  
+  .ss-icon-changyongyu:before {
+    content: "\e68b";
+  }
+  
+  .ss-icon-faxian:before {
+    content: "\e65f";
+  }
+  
+  .ss-icon-saoyisao:before {
+    content: "\e660";
+  }
+  
+  .ss-icon-gouxuan:before {
+    content: "\e661";
+  }
+  
+  .ss-icon-fuzhi:before {
+    content: "\e663";
+  }
+  
+  .ss-icon-kuaijiefangwen:before {
+    content: "\e664";
+  }
+  
+  .ss-icon-shijian:before {
+    content: "\e666";
+  }
+  
+  .ss-icon-suoping:before {
+    content: "\e667";
+  }
+  
+  .ss-icon-laiyuan:before {
+    content: "\e668";
+  }
+  
+  .ss-icon-dayin:before {
+    content: "\e669";
+  }
+  
+  .ss-icon-quanping:before {
+    content: "\e66a";
+  }
+  
+  .ss-icon-guanli:before {
+    content: "\e66b";
+  }
+  
+  .ss-icon-fasong:before {
+    content: "\e66c";
+  }
+  
+  .ss-icon-kehuguanli:before {
+    content: "\e66d";
+  }
+  
+  .ss-icon-duihua1:before {
+    content: "\e66e";
+  }
+  
+  .ss-icon-fukuan:before {
+    content: "\e66f";
+  }
+  
+  .ss-icon-i:before {
+    content: "\e670";
+  }
+  
+  .ss-icon-hebing:before {
+    content: "\e671";
+  }
+  
+  .ss-icon-olderOpen:before {
+    content: "\e672";
+  }
+  
+  .ss-icon-jiacu:before {
+    content: "\e673";
+  }
+  
+  .ss-icon-guangbiao-bianji:before {
+    content: "\e674";
+  }
+  
+  .ss-icon-jiahao:before {
+    content: "\e675";
+  }
+  
+  .ss-icon-chanyeleibie:before {
+    content: "\e676";
+  }
+  
+  .ss-icon-qiandaizi:before {
+    content: "\e647";
+  }
+  
+  .ss-icon-a-lock1:before {
+    content: "\e648";
+  }
+  
+  .ss-icon-renwu:before {
+    content: "\e649";
+  }
+  
+  .ss-icon-wenzhang:before {
+    content: "\e64a";
+  }
+  
+  .ss-icon-a-liucheng1:before {
+    content: "\e64b";
+  }
+  
+  .ss-icon-a-lianjie1:before {
+    content: "\e64c";
+  }
+  
+  .ss-icon-shunpai:before {
+    content: "\e64d";
+  }
+  
+  .ss-icon-shouyeshezhi:before {
+    content: "\e64e";
+  }
+  
+  .ss-icon-tupu:before {
+    content: "\e64f";
+  }
+  
+  .ss-icon-leibie:before {
+    content: "\e650";
+  }
+  
+  .ss-icon-tuichu:before {
+    content: "\e651";
+  }
+  
+  .ss-icon-huiyi:before {
+    content: "\e652";
+  }
+  
+  .ss-icon-quanwenjiansuo:before {
+    content: "\e653";
+  }
+  
+  .ss-icon-duixiang:before {
+    content: "\e654";
+  }
+  
+  .ss-icon-jinyong:before {
+    content: "\e655";
+  }
+  
+  .ss-icon-kaoqin:before {
+    content: "\e656";
+  }
+  
+  .ss-icon-huodong:before {
+    content: "\e657";
+  }
+  
+  .ss-icon-shouqi:before {
+    content: "\e658";
+  }
+  
+  .ss-icon-guanwang:before {
+    content: "\e659";
+  }
+  
+  .ss-icon-a-liangduanduiqi1:before {
+    content: "\e65a";
+  }
+  
+  .ss-icon-shouxie:before {
+    content: "\e65b";
+  }
+  
+  .ss-icon-shoucang:before {
+    content: "\e65d";
+  }
+  
+  .ss-icon-gongwen:before {
+    content: "\e65e";
+  }
+  
+  .ss-icon-guanbi:before {
+    content: "\e643";
+  }
+  
+  .ss-icon-shangyizhang:before {
+    content: "\e644";
+  }
+  
+  .ss-icon-shangjiaobiao:before {
+    content: "\e645";
+  }
+  
+  .ss-icon-wentiliebiao:before {
+    content: "\e646";
+  }
+  
+  .ss-icon-youduiqi:before {
+    content: "\e62d";
+  }
+  
+  .ss-icon-shaugxin:before {
+    content: "\e62e";
+  }
+  
+  .ss-icon-youpaihuanrao:before {
+    content: "\e62f";
+  }
+  
+  .ss-icon-xiazai:before {
+    content: "\e630";
+  }
+  
+  .ss-icon-zhengshu:before {
+    content: "\e631";
+  }
+  
+  .ss-icon-yewu:before {
+    content: "\e632";
+  }
+  
+  .ss-icon-tuihui:before {
+    content: "\e633";
+  }
+  
+  .ss-icon-fujian:before {
+    content: "\e634";
+  }
+  
+  .ss-icon-xiangxiajiantou:before {
+    content: "\e635";
+  }
+  
+  .ss-icon-shubiaojiantou:before {
+    content: "\e637";
+  }
+  
+  .ss-icon-a-ljt1:before {
+    content: "\e638";
+  }
+  
+  .ss-icon-suoxiao:before {
+    content: "\e639";
+  }
+  
+  .ss-icon-xiajiaobiao:before {
+    content: "\e63b";
+  }
+  
+  .ss-icon-fanhuishangyibu:before {
+    content: "\e63c";
+  }
+  
+  .ss-icon-X:before {
+    content: "\e63d";
+  }
+  
+  .ss-icon-qian:before {
+    content: "\e63e";
+  }
+  
+  .ss-icon-wenjianjia_zhankai:before {
+    content: "\e640";
+  }
+  
+  .ss-icon-home:before {
+    content: "\e641";
+  }
+  
+  .ss-icon-tingyong:before {
+    content: "\e642";
+  }
+  
+  .ss-icon-xuanzhong_jiaobiao:before {
+    content: "\e613";
+  }
+  
+  .ss-icon-zaixianbangzhu:before {
+    content: "\e614";
+  }
+  
+  .ss-icon-qingjia:before {
+    content: "\e615";
+  }
+  
+  .ss-icon-xiayizhang:before {
+    content: "\e616";
+  }
+  
+  .ss-icon-shengqing:before {
+    content: "\e617";
+  }
+  
+  .ss-icon-wupin:before {
+    content: "\e618";
+  }
+  
+  .ss-icon-zitiyanse:before {
+    content: "\e619";
+  }
+  
+  .ss-icon-suofang:before {
+    content: "\e61a";
+  }
+  
+  .ss-icon-yanjing:before {
+    content: "\e61b";
+  }
+  
+  .ss-icon-tupian:before {
+    content: "\e61c";
+  }
+  
+  .ss-icon-piliang:before {
+    content: "\e61d";
+  }
+  
+  .ss-icon-xiangshangjiantou:before {
+    content: "\e61e";
+  }
+  
+  .ss-icon-shezhi:before {
+    content: "\e61f";
+  }
+  
+  .ss-icon-wenben:before {
+    content: "\e620";
+  }
+  
+  .ss-icon-yonghuming:before {
+    content: "\e621";
+  }
+  
+  .ss-icon-shoufukuan:before {
+    content: "\e622";
+  }
+  
+  .ss-icon-xueyuan:before {
+    content: "\e623";
+  }
+  
+  .ss-icon-xiangxiahebing:before {
+    content: "\e624";
+  }
+  
+  .ss-icon-sanheng:before {
+    content: "\e625";
+  }
+  
+  .ss-icon-zihao:before {
+    content: "\e626";
+  }
+  
+  .ss-icon-jianshao:before {
+    content: "\e627";
+  }
+  
+  .ss-icon-a-mima1:before {
+    content: "\e628";
+  }
+  
+  .ss-icon-zuopaihuanrao:before {
+    content: "\e629";
+  }
+  
+  .ss-icon-xiangyouhebing:before {
+    content: "\e62a";
+  }
+  
+  .ss-icon-zitiyangshi:before {
+    content: "\e62b";
+  }
+  
+  .ss-icon-renyuan-:before {
+    content: "\e62c";
+  }
+  
+  .ss-icon-xiangmushezhi:before {
+    content: "\e602";
+  }
+  
+  .ss-icon-yiban:before {
+    content: "\e603";
+  }
+  
+  .ss-icon-wangzhan:before {
+    content: "\e604";
+  }
+  
+  .ss-icon-yujing:before {
+    content: "\e605";
+  }
+  
+  .ss-icon-zitishanchu:before {
+    content: "\e606";
+  }
+  
+  .ss-icon-yikatong:before {
+    content: "\e607";
+  }
+  
+  .ss-icon-xieti:before {
+    content: "\e608";
+  }
+  
+  .ss-icon-xietong:before {
+    content: "\e609";
+  }
+  
+  .ss-icon-zuanshi:before {
+    content: "\e60a";
+  }
+  
+  .ss-icon-zihao2:before {
+    content: "\e60b";
+  }
+  
+  .ss-icon-zhognjianduiqi:before {
+    content: "\e60c";
+  }
+  
+  .ss-icon-zuoduiqi:before {
+    content: "\e60d";
+  }
+  
+  .ss-icon-wenjianjia-shouqi:before {
+    content: "\e60e";
+  }
+  
+  .ss-icon-xiahuaxian:before {
+    content: "\e60f";
+  }
+  
+  .ss-icon-zhifangtu:before {
+    content: "\e610";
+  }
+  
+  .ss-icon-ziti:before {
+    content: "\e611";
+  }
+  
+  .ss-icon-yanzhengma:before {
+    content: "\e612";
+  }
+  

+ 984 - 0
skin/easy/css/iconfont.css

@@ -0,0 +1,984 @@
+@font-face {
+  font-family: "iconfont"; /* Project id 4810824 */
+  src: url('../fonts/iconfont.woff2') format('woff2'),
+}
+.iconfont {
+  font-family: "iconfont" !important;
+  font-size: 16px;
+  font-style: normal;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-kehulaifang:before {
+  content: "\e6f2";
+}
+
+.icon-qiehuan_xi:before {
+  content: "\e6f1";
+}
+
+.icon-qiehuan:before {
+  content: "\e6f0";
+}
+
+.icon-xunchajilu:before {
+  content: "\e6ef";
+}
+
+.icon-jinchujilu:before {
+  content: "\e6ee";
+}
+
+.icon-richeng:before {
+  content: "\e6ed";
+}
+
+.icon-men:before {
+  content: "\e6ec";
+}
+
+.icon-gangwei:before {
+  content: "\e6eb";
+}
+
+.icon-banji:before {
+  content: "\e6e9";
+}
+
+.icon-qunzu:before {
+  content: "\e6ea";
+}
+
+.icon-cheliang:before {
+  content: "\e6e8";
+}
+
+.icon-paixu:before {
+  content: "\e6e7";
+}
+
+.icon-jiaoshi:before {
+  content: "\e6e6";
+}
+
+.icon-a-binghangfenzhi:before {
+  content: "\e6e5";
+}
+
+.icon-banjie:before {
+  content: "\e6e4";
+}
+
+.icon-huiqian:before {
+  content: "\e6e3";
+}
+
+.icon-zhihang:before {
+  content: "\e6e2";
+}
+
+.icon-chaosong:before {
+  content: "\e6e1";
+}
+
+.icon-lianxian:before {
+  content: "\e6e0";
+}
+
+.icon-sanjiaogantanhao:before {
+  content: "\e6df";
+}
+
+.icon-huidaodingbu:before {
+  content: "\e6de";
+}
+
+.icon-yishuzhao_nv:before {
+  content: "\e6dc";
+}
+
+.icon-yishuzhao_nan:before {
+  content: "\e6dd";
+}
+
+.icon-xiawu:before {
+  content: "\e6d9";
+}
+
+.icon-night:before {
+  content: "\e6da";
+}
+
+.icon-shangwu:before {
+  content: "\e6db";
+}
+
+.icon-a-dianhua01:before {
+  content: "\e6d8";
+}
+
+.icon-cheliangyuyue:before {
+  content: "\e6d7";
+}
+
+.icon-xiaofeijilu:before {
+  content: "\e6d6";
+}
+
+.icon-dianming:before {
+  content: "\e6d5";
+}
+
+.icon-xuncha:before {
+  content: "\e6d3";
+}
+
+.icon-a-yuanxingren2:before {
+  content: "\e6d4";
+}
+
+.icon-yuyan:before {
+  content: "\e6d2";
+}
+
+.icon-tongjitu:before {
+  content: "\e6d1";
+}
+
+.icon-gerenxinxi:before {
+  content: "\e6d0";
+}
+
+.icon-yishuzhao:before {
+  content: "\e6cf";
+}
+
+.icon-zhengjianzhao:before {
+  content: "\e6ce";
+}
+
+.icon-wenhao_xi:before {
+  content: "\e6cd";
+}
+
+.icon-a-quanping_xi:before {
+  content: "\e6ca";
+}
+
+.icon-tuichuquanping:before {
+  content: "\e6cb";
+}
+
+.icon-tuichuquanping_xi:before {
+  content: "\e6cc";
+}
+
+.icon-fansuojin:before {
+  content: "\e6c9";
+}
+
+.icon-bianhao:before {
+  content: "\e6c7";
+}
+
+.icon-suojin:before {
+  content: "\e6c8";
+}
+
+.icon-xuanxiangka:before {
+  content: "\e6c6";
+}
+
+.icon-gerenxuanzhi:before {
+  content: "\e6c5";
+}
+
+.icon-a-quanping:before {
+  content: "\e6c4";
+}
+
+.icon-fuhao:before {
+  content: "\e6c3";
+}
+
+.icon-saozhou:before {
+  content: "\e6c2";
+}
+
+.icon-gouxuankuang:before {
+  content: "\e6c0";
+}
+
+.icon-gouxuankuang-xuanzhong:before {
+  content: "\e6c1";
+}
+
+.icon-zhuanhuan:before {
+  content: "\e6bf";
+}
+
+.icon-jinchu:before {
+  content: "\e6be";
+}
+
+.icon-qiye:before {
+  content: "\e6b5";
+}
+
+.icon-yinshipin-mian:before {
+  content: "\e6b6";
+}
+
+.icon-zhengfu:before {
+  content: "\e6b7";
+}
+
+.icon-yinpin-mian:before {
+  content: "\e6b8";
+}
+
+.icon-zhengshu:before {
+  content: "\e6b9";
+}
+
+.icon-shipin-mian:before {
+  content: "\e6ba";
+}
+
+.icon-tingtong:before {
+  content: "\e6bb";
+}
+
+.icon-yinbo:before {
+  content: "\e6bc";
+}
+
+.icon-guangbo:before {
+  content: "\e6bd";
+}
+
+.icon-laba:before {
+  content: "\e6b4";
+}
+
+.icon-shoufukuan:before {
+  content: "\e6b3";
+}
+
+.icon-xiangzuoshuangjiantou:before {
+  content: "\e6b1";
+}
+
+.icon-xiangyoushuangjiantou:before {
+  content: "\e6b2";
+}
+
+.icon-gouwuche:before {
+  content: "\e6b0";
+}
+
+.icon-qingjia:before {
+  content: "\e6af";
+}
+
+.icon-diqiu2:before {
+  content: "\e6ae";
+}
+
+.icon-lieshu:before {
+  content: "\e6ad";
+}
+
+.icon-gonggao:before {
+  content: "\e6ac";
+}
+
+.icon-shouxie:before {
+  content: "\e6ab";
+}
+
+.icon-renyuan2:before {
+  content: "\e6aa";
+}
+
+.icon-lifangti:before {
+  content: "\e6a9";
+}
+
+.icon-i:before {
+  content: "\e6a8";
+}
+
+.icon-suoping-mian:before {
+  content: "\e69f";
+}
+
+.icon-kapian:before {
+  content: "\e6a0";
+}
+
+.icon-tuichu-mian:before {
+  content: "\e6a1";
+}
+
+.icon-jianhao:before {
+  content: "\e6a2";
+}
+
+.icon-saoyisao:before {
+  content: "\e6a3";
+}
+
+.icon-saoyisao2:before {
+  content: "\e6a4";
+}
+
+.icon-liebiao:before {
+  content: "\e6a5";
+}
+
+.icon-shandian:before {
+  content: "\e6a6";
+}
+
+.icon-wenhao:before {
+  content: "\e6a7";
+}
+
+.icon-fukuan:before {
+  content: "\e68e";
+}
+
+.icon-queding:before {
+  content: "\e68f";
+}
+
+.icon-shouye:before {
+  content: "\e690";
+}
+
+.icon-xiazai:before {
+  content: "\e691";
+}
+
+.icon-huiyi:before {
+  content: "\e692";
+}
+
+.icon-X-xi:before {
+  content: "\e693";
+}
+
+.icon-kaoqin:before {
+  content: "\e694";
+}
+
+.icon-suo-kai:before {
+  content: "\e695";
+}
+
+.icon-quanxiangzuo:before {
+  content: "\e696";
+}
+
+.icon-suo-guan:before {
+  content: "\e697";
+}
+
+.icon-shoucang:before {
+  content: "\e698";
+}
+
+.icon-bofangqi:before {
+  content: "\e699";
+}
+
+.icon-shubiao-bianji:before {
+  content: "\e69a";
+}
+
+.icon-shubiaodianji:before {
+  content: "\e69b";
+}
+
+.icon-fenleiqi:before {
+  content: "\e69c";
+}
+
+.icon-changyongyu:before {
+  content: "\e69d";
+}
+
+.icon-a-wenben1:before {
+  content: "\e69e";
+}
+
+.icon-zihao:before {
+  content: "\e67d";
+}
+
+.icon-zuoduanduiqi:before {
+  content: "\e67e";
+}
+
+.icon-youduanduiqi:before {
+  content: "\e67f";
+}
+
+.icon-ziti:before {
+  content: "\e680";
+}
+
+.icon-zitiyanse:before {
+  content: "\e681";
+}
+
+.icon-zitiyangshi:before {
+  content: "\e682";
+}
+
+.icon-tupian:before {
+  content: "\e683";
+}
+
+.icon-pinxing:before {
+  content: "\e684";
+}
+
+.icon-xiahuaxian:before {
+  content: "\e685";
+}
+
+.icon-a-zhongzuo1:before {
+  content: "\e686";
+}
+
+.icon-leida:before {
+  content: "\e687";
+}
+
+.icon-fanhuishangyibu:before {
+  content: "\e688";
+}
+
+.icon-yifu-mian:before {
+  content: "\e689";
+}
+
+.icon-canshu:before {
+  content: "\e68a";
+}
+
+.icon-jiacu:before {
+  content: "\e68b";
+}
+
+.icon-canshushezhi:before {
+  content: "\e68c";
+}
+
+.icon-baocun:before {
+  content: "\e68d";
+}
+
+.icon-xieti:before {
+  content: "\e66b";
+}
+
+.icon-wenzishangjiaobiao:before {
+  content: "\e66c";
+}
+
+.icon-wenziyouhuanrao:before {
+  content: "\e66d";
+}
+
+.icon-huaxianzi:before {
+  content: "\e66e";
+}
+
+.icon-dayinji:before {
+  content: "\e66f";
+}
+
+.icon-wenzishunpai:before {
+  content: "\e670";
+}
+
+.icon-wenzizuohuanrao:before {
+  content: "\e671";
+}
+
+.icon-chaifen:before {
+  content: "\e672";
+}
+
+.icon-hebing:before {
+  content: "\e673";
+}
+
+.icon-xiangyouhebing:before {
+  content: "\e674";
+}
+
+.icon-xiangxiahebing:before {
+  content: "\e675";
+}
+
+.icon-a-lianjie1:before {
+  content: "\e676";
+}
+
+.icon-biaoge:before {
+  content: "\e677";
+}
+
+.icon-quanwenjiansuo:before {
+  content: "\e678";
+}
+
+.icon-shanchubiaoge:before {
+  content: "\e679";
+}
+
+.icon-duixiang:before {
+  content: "\e67a";
+}
+
+.icon-zhongjianduiqi:before {
+  content: "\e67b";
+}
+
+.icon-liangduanduiqi:before {
+  content: "\e67c";
+}
+
+.icon-bingtu:before {
+  content: "\e658";
+}
+
+.icon-gou-mian:before {
+  content: "\e659";
+}
+
+.icon-huizhang:before {
+  content: "\e65a";
+}
+
+.icon-duihuapaopao2:before {
+  content: "\e65b";
+}
+
+.icon-daiban:before {
+  content: "\e65c";
+}
+
+.icon-zuoshangjiaobiao:before {
+  content: "\e65d";
+}
+
+.icon-wenben:before {
+  content: "\e65e";
+}
+
+.icon-ifuhao:before {
+  content: "\e65f";
+}
+
+.icon-caogaoxiang:before {
+  content: "\e660";
+}
+
+.icon-gongwen:before {
+  content: "\e661";
+}
+
+.icon-dasha:before {
+  content: "\e662";
+}
+
+.icon-yiban:before {
+  content: "\e663";
+}
+
+.icon-naoling:before {
+  content: "\e664";
+}
+
+.icon-wenzidise:before {
+  content: "\e665";
+}
+
+.icon-diqiu:before {
+  content: "\e666";
+}
+
+.icon-X-cu:before {
+  content: "\e667";
+}
+
+.icon-wendang:before {
+  content: "\e668";
+}
+
+.icon-wenzibuhuanrao:before {
+  content: "\e669";
+}
+
+.icon-wenzixiajiaobiao:before {
+  content: "\e66a";
+}
+
+.icon-zhifangtu:before {
+  content: "\e647";
+}
+
+.icon-piliangdaoru:before {
+  content: "\e648";
+}
+
+.icon-leidasaomiao:before {
+  content: "\e649";
+}
+
+.icon-xietong:before {
+  content: "\e64a";
+}
+
+.icon-zuanshi:before {
+  content: "\e64b";
+}
+
+.icon-taishidiannao:before {
+  content: "\e64c";
+}
+
+.icon-guanlian:before {
+  content: "\e64d";
+}
+
+.icon-fanghe:before {
+  content: "\e64e";
+}
+
+.icon-fuzhi:before {
+  content: "\e64f";
+}
+
+.icon-deng:before {
+  content: "\e650";
+}
+
+.icon-daochu:before {
+  content: "\e651";
+}
+
+.icon-sifangge:before {
+  content: "\e652";
+}
+
+.icon-jiantou:before {
+  content: "\e653";
+}
+
+.icon-lajitong:before {
+  content: "\e654";
+}
+
+.icon-duihuapaopao:before {
+  content: "\e655";
+}
+
+.icon-liucheng:before {
+  content: "\e656";
+}
+
+.icon-xiangzuojiantou2:before {
+  content: "\e657";
+}
+
+.icon-fangdajing:before {
+  content: "\e633";
+}
+
+.icon-suoxiao:before {
+  content: "\e634";
+}
+
+.icon-xunhuan:before {
+  content: "\e635";
+}
+
+.icon-wentiliebiao:before {
+  content: "\e636";
+}
+
+.icon-mubiao:before {
+  content: "\e637";
+}
+
+.icon-tuihui:before {
+  content: "\e638";
+}
+
+.icon-tingyong:before {
+  content: "\e639";
+}
+
+.icon-zhongzuo:before {
+  content: "\e63a";
+}
+
+.icon-zhifeiji:before {
+  content: "\e63b";
+}
+
+.icon-qiandaizi-mian:before {
+  content: "\e63c";
+}
+
+.icon-qianfuhao:before {
+  content: "\e63d";
+}
+
+.icon-xiangxiajiantou:before {
+  content: "\e63e";
+}
+
+.icon-xiangshangjiantou:before {
+  content: "\e63f";
+}
+
+.icon-xiangyoujiantou:before {
+  content: "\e640";
+}
+
+.icon-xiangzuojiantou:before {
+  content: "\e641";
+}
+
+.icon-yanjing-kai:before {
+  content: "\e642";
+}
+
+.icon-dianzan:before {
+  content: "\e643";
+}
+
+.icon-qizhi:before {
+  content: "\e644";
+}
+
+.icon-dangan:before {
+  content: "\e645";
+}
+
+.icon-dingwei:before {
+  content: "\e646";
+}
+
+.icon-charuhang:before {
+  content: "\e621";
+}
+
+.icon-tuozhuai:before {
+  content: "\e622";
+}
+
+.icon-xueyuan:before {
+  content: "\e623";
+}
+
+.icon-jinyong:before {
+  content: "\e624";
+}
+
+.icon-excel:before {
+  content: "\e625";
+}
+
+.icon-xiangshangshengjiantou:before {
+  content: "\e626";
+}
+
+.icon-word:before {
+  content: "\e627";
+}
+
+.icon-xiuxue:before {
+  content: "\e628";
+}
+
+.icon-lianjie:before {
+  content: "\e629";
+}
+
+.icon-X-mian:before {
+  content: "\e62a";
+}
+
+.icon-bugouxuan:before {
+  content: "\e62b";
+}
+
+.icon-neirongku:before {
+  content: "\e62c";
+}
+
+.icon-gouxuan:before {
+  content: "\e62d";
+}
+
+.icon-fenlei1:before {
+  content: "\e62e";
+}
+
+.icon-chazhao:before {
+  content: "\e62f";
+}
+
+.icon-chilun:before {
+  content: "\e630";
+}
+
+.icon-yanjing-bi:before {
+  content: "\e631";
+}
+
+.icon-shuben:before {
+  content: "\e632";
+}
+
+.icon-jiahao:before {
+  content: "\e60d";
+}
+
+.icon-shezhi-mian:before {
+  content: "\e60e";
+}
+
+.icon-yaoyiyaoshouji-kongxin:before {
+  content: "\e60f";
+}
+
+.icon-bianji:before {
+  content: "\e610";
+}
+
+.icon-biaoqian:before {
+  content: "\e611";
+}
+
+.icon-fanhuijiantou:before {
+  content: "\e612";
+}
+
+.icon-quanxuan:before {
+  content: "\e613";
+}
+
+.icon-gongcheng:before {
+  content: "\e614";
+}
+
+.icon-kuangxuanzhong:before {
+  content: "\e615";
+}
+
+.icon-biaoqing:before {
+  content: "\e616";
+}
+
+.icon-xiangji:before {
+  content: "\e617";
+}
+
+.icon-a-kuangrujiantou:before {
+  content: "\e618";
+}
+
+.icon-kuang-kaikou:before {
+  content: "\e619";
+}
+
+.icon-huihe:before {
+  content: "\e61a";
+}
+
+.icon-jieshu:before {
+  content: "\e61b";
+}
+
+.icon-dengpao:before {
+  content: "\e61c";
+}
+
+.icon-kaishi:before {
+  content: "\e61d";
+}
+
+.icon-shanchulie:before {
+  content: "\e61e";
+}
+
+.icon-shanchuhang:before {
+  content: "\e61f";
+}
+
+.icon-charulie:before {
+  content: "\e620";
+}
+
+.icon-jinru:before {
+  content: "\e600";
+}
+
+.icon-a-yuanxingren:before {
+  content: "\e601";
+}
+
+.icon-chenggong:before {
+  content: "\e602";
+}
+
+.icon-yaoyiyao-mian:before {
+  content: "\e603";
+}
+
+.icon-tuichu:before {
+  content: "\e604";
+}
+
+.icon-renyuan:before {
+  content: "\e605";
+}
+
+.icon-shijian:before {
+  content: "\e606";
+}
+
+.icon-kaisuo:before {
+  content: "\e607";
+}
+
+.icon-anquandunpai:before {
+  content: "\e608";
+}
+
+.icon-qiehuanyonghu:before {
+  content: "\e609";
+}
+
+.icon-a-wenjianjia:before {
+  content: "\e60a";
+}
+
+.icon-wenjianjia-he:before {
+  content: "\e60b";
+}
+
+.icon-wenjianjia-kai:before {
+  content: "\e60c";
+}
+

File diff suppressed because it is too large
+ 1 - 0
skin/easy/css/jodit.css


+ 36 - 0
skin/easy/css/var.css

@@ -0,0 +1,36 @@
+html{
+    --danger: #cc3600;
+    --primary: #6b9eee;
+    --warn: #dc6a12;
+    --success: #7ebf50;
+    --words: #373737;
+    --lightWords: #999;
+    --lightgray: #edf1f5;
+    --gray: #b4b8bc;
+    --lightdark: #3a3e51;
+    --lightdark-sub: #555a6b;
+    --dark: #252835;
+    --white: #fff;
+    --space-size: 20px;
+    --el-color-primary: #393d51 !important;
+}
+.left-side-container[size="max"] {
+--menu-item-label-visible: visible;
+--menu-item-label-opacity: 1;
+}
+.layout-container[sys-mode="edit"] {
+--menu-add-btn-display: flex;
+--menu-edit-mark-display: block;
+}
+.imgUnHandle {
+    user-drag: none;
+    -webkit-user-drag: none;
+    -khtml-user-drag: none;
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
+  }
+  

+ 1051 - 0
skin/easy/dlg.css

@@ -0,0 +1,1051 @@
+
+/* 新添加样式    设置dhtmlx_wins_body_inner顶部边框*/
+.dhtmlx_wins_body_inner{
+	/* border-top:2px solid #edf1f5; */
+}
+
+/*  修改样式		设置弹窗圆角*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_body_outer
+, .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_body_outer {
+	position: absolute;
+	overflow: hidden;
+	left: 0px;
+	top: 0px;
+	background-color: #fff;
+	/* border: #bcbdbf 1px solid; */
+	border-radius: 4px;
+}
+
+/*   修改样式   设置弹窗标题位置*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_title
+, .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_title{
+	position: absolute;
+	top: 0px;
+	height: 37px;
+	line-height:27px;
+	vertical-align: middle;
+	padding-left: 5px;
+	left: 0px;
+	cursor: default;
+	white-space: nowrap;
+	overflow: hidden;
+	-moz-user-select: none;
+	font-size: 16px;
+	color: #333;
+	font-family: 'SimHei','Microsoft YaHei', 'Helvetica Neue', Helvetica!important;
+	text-indent: 6px;
+	font-weight: normal;
+}
+
+/*  修改样式 设置弹窗按钮区域距离顶部的距离*/
+/* buttons */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_wins_btns {
+	position: absolute;
+	right: 0px;
+	top: 0px;
+	display: flex;
+	align-items: center;
+	/* height: 60px; */
+}
+
+/* active window */
+/*20180827  修改样式  活动状态弹窗样式*/
+div.dhtmlx_window_active{
+	box-shadow: 7px 7px 5px rgba(0, 0, 0, 0.25);
+	border-radius: 4px;
+}
+
+/* .dhtmlx_button_editWdHelp_save_default,.dhtmlx_button_editWdHelp_save_over_default{
+	background: url("image/dialog-wdRecord-save.png") center center no-repeat;
+} */
+
+/*    修改样式   设置弹窗按钮大小 */
+div.dhtmlx_wins_btns_button {
+	margin-left: 5px;
+	float: left;
+	width: 36px;
+	height: 36px;
+}
+
+
+/*--------------------------------------------------------上方为需要改动的样式------------------------------------------*/
+
+
+
+
+
+
+/*原样式*/
+
+
+.test--------{
+	position: absolute;
+	overflow: hidden;
+	left: 0px;
+	top: 0px;
+	background-color: #fff;
+	border: #a4bed4 1px solid;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_body_outer div.dhtmlx_wins_body_inner
+, .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_body_outer div.dhtmlx_wins_body_inner{
+	border-top: 1px solid #e2e4ec;
+	position: absolute;
+	overflow: hidden;
+	/*margin-top: 20px;*/
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active .dhtmlx_wins_no_header
+, .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive .dhtmlx_wins_no_header{ /* will added to div.dhtmlx_wins_body_inner in case of no header */
+	border-top: #c2d5dc 6px solid !important;
+}
+
+.animate{
+	animation: move .35s ease-in-out;
+	position:absolute; 
+}
+@keyframes move{
+	from{
+		left:-300px;
+	}
+	to {
+		left:0px;
+	}
+}
+
+@-webkit-keyframes move{
+	from {
+		left:-300px;
+	}
+	to {
+		left:0px;
+	}
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_title a
+, .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_title a{
+    color: #666;
+    font-size: 18px;
+	font-family: "SimHei",Georgia,Serif !important;
+    text-decoration: none;
+}
+/* active progress */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_progress 
+ {
+	background-image: url("image/active/progress.gif");
+}
+
+
+/* active buttons */
+/* close */
+
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_close_disabled 
+,.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_close_disabled 
+ {
+	background-position: -96px -48px;
+}
+
+/* minmax1 */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax1_default {
+	background-position: -64px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax1_disabled {
+	background-position: -64px -48px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax1_over_default {
+	background-position: -64px -16px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax1_over_pressed {
+	background-position: -64px -32px;
+}
+/* minmax2 */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax2_default {
+	background-position: -80px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax2_disabled {
+	background-position: -80px -48px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax2_over_default {
+	background-position: -80px -16px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_minmax2_over_pressed {
+	background-position: -80px -32px;
+}
+/* park */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_park_default {
+	background-position: -48px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_park_disabled {
+	background-position: -48px -48px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_park_over_default {
+	background-position: -48px -16px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_park_over_pressed {
+	background-position: -48px -32px;
+}
+/* stick */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_stick_default {
+	background-position: 0px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_stick_disabled {
+	background-position: 0px -48px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_stick_over_default {
+	background-position: 0px -16px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_stick_over_pressed {
+	background-position: 0px -32px;
+}
+/* sticked */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_sticked_default {
+	background-position: -16px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_sticked_disabled {
+	background-position: -16px -48px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_sticked_over_default {
+	background-position: -16px -16px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_sticked_over_pressed {
+	background-position: -16px -32px;
+}
+/* help */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_help_default {
+	background-position: -32px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_help_disabled {
+	background-position: -32px -48px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_help_over_default {
+	background-position: -32px -16px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_help_over_pressed {
+	background-position: -32px -32px;
+}
+/* dock */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_dock_default {
+	background-position: -112px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_dock_disabled {
+	background-position: -112px -48px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_dock_over_default {
+	background-position: -112px -16px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_dock_over_pressed {
+	background-position: -112px -32px;
+}
+
+
+/* inactive window */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_body_outer {
+	position: absolute;
+	overflow: hidden;
+	left: 0px;
+	top: 0px;
+	background-color: #fff;
+	border: #a4bed4 1px solid;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_body_outer div.dhtmlx_wins_body_inner {
+	position: absolute;
+	overflow: hidden;
+	border-top: none;
+	/*申请人弹出窗背景灰色问题*/
+	/*background-color: #ebebeb;*/
+	/*border: #ebebeb 2px solid !important;*/
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive .dhtmlx_wins_no_header { /* will added to div.dhtmlx_wins_body_inner in case of no header */
+	border-top: #c2d5dc 6px solid;
+}
+/* .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_title {
+	position: absolute;
+	top: 0px;
+	height: 29px;
+	line-height: 29px;
+	vertical-align: middle;
+	padding-left: 28px;
+	left: 0px;
+	color: #686868;
+	font-family: Tahoma;
+	font-size: 11px;
+	font-weight: bold;
+	cursor: default;
+	white-space: nowrap;
+	overflow: hidden;
+	-moz-user-select: none;
+} */
+/*inactive progress*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_progress {
+	background-image: url("image/inactive/progress.gif");
+}
+/*inactive butons
+close*/
+
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_close_disabled {
+	background-position: -96px -112px;
+}
+
+
+/* minmax1 */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax1_default {
+	background-position: -64px -64px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax1_disabled {
+	background-position: 0px 0px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax1_over_default {
+	background-position: -64px -80px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax1_over_pressed {
+	background-position: -64px -96px;
+}
+/* minmax2 */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax2_default {
+	background-position: -80px -64px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax2_disabled {
+	background-position: -80px -112px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax2_over_default {
+	background-position: -80px -80px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_minmax2_over_pressed {
+	background-position: -80px -96px;
+}
+/*park*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_park_default {
+	background-position: -48px -64px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_park_disabled {
+	background-position: -48px -112px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_park_over_default {
+	background-position: -48px -80px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_park_over_pressed {
+	background-position: -48px -96px;
+}
+/*stick*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_stick_default {
+	background-position: 0px -64px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_stick_disabled {
+	background-position: 0px -112px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_stick_over_default {
+	background-position: 0px -80px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_stick_over_pressed {
+	background-position: 0px -96px;
+}
+/*sticked*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_sticked_default {
+	background-position: -16px -64px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_sticked_disabled {
+	background-position: -16px -112px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_sticked_over_default {
+	background-position: -16px -80px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_sticked_over_pressed {
+	background-position: -16px -96px;
+}
+/*help*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_help_default {
+	background-position: -32px -64px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_help_disabled {
+	background-position: -32px -112px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_help_over_default {
+	background-position: -32px -80px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_help_over_pressed {
+	background-position: -32px -96px;
+}
+/*dock*/
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_dock_default {
+	background-position: -112px -64px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_dock_disabled {
+	background-position: -112px -112px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_dock_over_default {
+	background-position: -112px -80px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_dock_over_pressed {
+	background-position: -112px -96px;
+}
+
+/* common */
+/* content blocker */
+/*
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_main_content_blocker {
+	position: absolute;
+	left: 0px;
+	top: 0px;
+	width: 101%;
+	height: 101%;
+	filter: alpha(opacity=0);
+	-moz-opacity: 0;
+	opacity: 0;
+	background: #FFFFFF;
+	z-index: 1;
+	-moz-user-select: none;
+}
+*/
+/* window icon */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_wins_icon {
+	position: absolute;
+	top: 15px;
+	left: 5px;
+	width: 16px;
+	height: 16px;
+	border: none;
+	z-index: 1;
+	-moz-user-select: none;
+	background-repeat: no-repeat;
+	font-size: 1px;
+}
+
+
+/*关闭*/
+
+/*
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_main_content {
+	position: relative;
+	left: 0px;
+	top: 0px;
+	overflow: hidden;
+}
+*/
+/* resizers */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_wins_resizer_t {
+	display: none;
+	position: absolute;
+	left: 0px;
+	top: 0px;
+	width: 100%;
+	height: 0; /* should be generated by script */
+	font-size: 1px;
+	background: #FFFFFF;
+	z-index: 1;
+	filter: alpha(opacity=0);
+	-moz-opacity: 0;
+	opacity: 0;
+	-moz-user-select: none;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_wins_resizer_l {
+	display: none;
+	position: absolute;
+	left: 0px;
+	top: 0px;
+	width: 0; /* should be generated by script */
+	height: 100%;
+	font-size: 1px;
+	background: #FFFFFF;
+	z-index: 1;
+	filter: alpha(opacity=0);
+	-moz-opacity: 0;
+	opacity: 0;
+	-moz-user-select: none;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_wins_resizer_r {
+	display: none;
+	position: absolute;
+	right: 0px;
+	top: 0px;
+	width: 0; /* should be generated by script */
+	height: 100%;
+	font-size: 1px;
+	background: #FFFFFF;
+	z-index: 1;
+	filter: alpha(opacity=0);
+	-moz-opacity: 0;
+	opacity: 0;
+	-moz-user-select: none;
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_wins_resizer_b {
+	display: none;
+	position: absolute;
+	left: 0px;
+	bottom: 0px;
+	width: 100%;
+	height: 0; /* should be generated by script */
+	font-size: 1px;
+	background: #FFFFFF;
+	z-index: 1;
+	filter: alpha(opacity=0);
+	-moz-opacity: 0;
+	opacity: 0;
+	-moz-user-select: none;
+}
+/* progress */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_wins_progress {
+	position: absolute;
+	top: 5px;
+	left: 5px;
+	width: 16px;
+	height: 16px;
+	background-repeat: no-repeat;
+}
+/* statusbar */
+.dhtmlx_skin_dhx_skyblue div.dhxcont_sb_container {
+	position: relative;
+	height: 24px;
+}
+.dhtmlx_skin_dhx_skyblue div.dhxcont_sb_container div.dhxcont_statusbar {
+	position: relative;
+	top: 2px;
+	height: 22px;
+	line-height: 22px;
+	background-image: url("image/statusbar_bg.gif");
+	background-repeat: repeat-x;
+	width: auto;
+	padding: 0px 4px;
+	overflow: hidden;
+	white-space: nowrap;
+	border-top: none;
+	border-bottom: none;
+	border-left: #a4bed4 1px solid;
+	border-right: #a4bed4 1px solid;
+	font-family: Tahoma;
+	font-size: 11px;
+	vertical-align: middle;
+	color: #000000;
+}
+/*
+.dhtmlx_skin_dhx_skyblue div.dhxcont_statusbar {
+	position: absolute;
+	width: 100%;
+	bottom: 0px;
+	_bottom: -1px;
+	border-top: #c2d5dc 1px solid;
+	background-image: url("image/statusbar_bg.gif");
+	background-repeat: repeat-x;
+	width: 100%;
+	overflow: hidden;
+	font-family: Tahoma;
+	font-size: 11px;
+	vertical-align: middle;
+	line-height: 17px;
+	-moz-user-select: none;
+	cursor: default;
+	padding-left: 4px;
+	color: #7393ae;
+}
+*/
+
+
+.dhtmlx_skin_dhx_skyblue div.white_line {
+	/*注释原因:ry_input边框会增加白边*/
+	/*border-left: #FFFFFF 1px solid;
+	border-right: #FFFFFF 1px solid;
+	border-top: #FFFFFF 1px solid;*/
+	height: 100%;
+}
+.dhtmlx_skin_dhx_skyblue div.white_line2 {
+	position: absolute;
+	bottom: 0px;
+z-index:-1 !important;
+	/* height: 10px; */
+	width: 100%;
+	/*注释原因:ry_input边框会增加白边*/
+	/*border-bottom: #FFFFFF 1px solid;*/
+	/* font-size: 1px; */
+	/*background:#e8e9eb;*/
+}
+
+.dhtmlx_skin_dhx_skyblue .dhtmlxMenu_in_Window {
+	aborder-bottom: #cedce8 1px solid;
+	border-bottom: #a4bed4 1px solid;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+/*wdShare*/
+
+
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdShare_disabled 
+ {	
+ 	background-position: -2px -1px;
+	background-image: url("image/wdShareBIG.png");
+}
+
+
+
+
+
+
+
+
+
+/**
+ * wdHelp_copyhashcode
+ */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_default,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_default{
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	/* background-position: 59px 0px; */
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-ID.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_disabled 
+,.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_disabled 
+ {	
+ 	background-position: -371px -22px;
+	background-image: url("image/wdHelp_copyhashcode.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_over_default,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_over_default {
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	/*background-position: -59px 0px;*/
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-ID-hover.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_over_pressed,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_copyhashcode_over_pressed {
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	/*background-position: -59px 0px;*/
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-ID-hover.png");
+}
+
+/**
+ * wdHelp_pin
+ */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_default,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_default{
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	 /*background-position: -59px 0px;*/ 
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-lock.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_disabled 
+,.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_disabled 
+ {	
+ 	/*background-position: -371px -22px;*/
+	background-image: url("image/popWindow-lock-hover.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_over_default,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_over_default {
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	/*background-position: -59px 0px;*/
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-lock-hover.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_over_pressed ,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_pin_over_pressed {
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	/*background-position: -59px 0px;*/
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-lock-hover.png");
+}
+
+/**
+ * wdHelp_unpin
+ */
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_default,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_default{
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	 /*background-position: -59px 0px;*/ 
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-lock-hover.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_disabled 
+,.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_disabled 
+ {	
+ 	/*background-position: -371px -22px;*/
+	background-image: url("image/popWindow-lock-hover.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_over_default,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_over_default {
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	/*background-position: -59px 0px;*/
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-lock-hover.png");
+}
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_over_pressed,
+.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_wdHelp_unpin_over_pressed {
+	cursor:pointer;
+	width:36px;
+	height:36px;
+	/*background-position: -59px 0px;*/
+	background-repeat:no-repeat;
+	background-image: url("image/popWindow-lock-hover.png");
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+/**
+ * change_on
+ */
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_on_default,
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_on_default{
+	 cursor:pointer;
+	 width:36px;
+	 height:36px;
+	 /* background-position: 59px 0px; */
+	 background-repeat:no-repeat;
+	 background-image: url("image/popWindow-fullscreen.png");
+ }
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_on_disabled 
+ ,.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_on_disabled 
+  {	
+	  background-position: -371px -22px;
+	 background-image: url("image/popWindow-fullscreen.png");
+ }
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_on_over_default,
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_on_over_default {
+	 cursor:pointer;
+	 width:36px;
+	 height:36px;
+	 /*background-position: -59px 0px;*/
+	 background-repeat:no-repeat;
+	 background-image: url("image/popWindow-fullscreen-hover.png");
+ }
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_on_over_pressed ,
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_on_over_pressed {
+	 cursor:pointer;
+	 width:36px;
+	 height:36px;
+	 /*background-position: -59px 0px;*/
+	 background-repeat:no-repeat;
+	 background-image: url("image/popWindow-fullscreen-hover.png");
+ }
+ 
+/**
+ * change_off
+ */
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_off_default,
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_off_default{
+	 cursor:pointer;
+	 width:36px;
+	 height:36px;
+	 background-repeat:no-repeat;
+	 background-image: url("image/popWindow-fullscreen.png");
+ }
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_off_disabled 
+ ,.dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_off_disabled 
+  {	
+	  background-position: -371px -22px;
+	 background-image: url("image/popWindow-fullscreen.png");
+ }
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_off_over_default,
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_off_over_default {
+	 cursor:pointer;
+	 width:36px;
+	 height:36px;
+	 /*background-position: -59px 0px;*/
+	 background-repeat:no-repeat;
+	 background-image: url("image/popWindow-fullscreen-hover.png");
+ }
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_active div.dhtmlx_wins_btns .dhtmlx_button_change_off_over_pressed ,
+ .dhtmlx_skin_dhx_skyblue div.dhtmlx_window_inactive div.dhtmlx_wins_btns .dhtmlx_button_change_off_over_pressed {
+	 cursor:pointer;
+	 width:36px;
+	 height:36px;
+	 /*background-position: -59px 0px;*/
+	 background-repeat:no-repeat;
+	 background-image: url("image/popWindow-fullscreen-active.png");
+ }
+ /*编辑选项卡按钮*/
+.icon-set{
+    display: inline-block;
+    width: 36px;
+    height: 36px;
+    background: url(image/svg_icon/xuanxiangka.svg) center center no-repeat;
+	background-size: 22px;
+}
+.icon-set:hover{
+    background: #383d50 url(image/svg_icon/xuanxiangka_hover.svg) center center no-repeat;
+	background-size: 22px;
+	border-radius: 4px;
+}
+.icon-set:active{
+    background: #000 url(image/svg_icon/xuanxiangka_hover.svg) center center no-repeat;
+	background-size: 22px;
+	border-radius: 4px;
+}
+/* 打钩按钮 */
+.icon-ok{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-ok.png) center center no-repeat;
+}
+.icon-ok:hover{
+    background: url(image/icon-ok-hover.png) center center no-repeat;
+}
+.icon-ok:active{
+    background: url(image/icon-ok-active.png) center center no-repeat;
+}
+/* 收藏按钮 */
+.icon-favorite{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/svg_icon/shoucang.svg) center center no-repeat;
+	border-radius: 4px;
+	background-size: 24px;
+}
+.icon-favorite:hover{
+    background: #383d50 url(image/svg_icon/shoucang.svg) center center no-repeat;
+	border-radius: 4px;
+	background-size: 24px;
+}
+.icon-favorite:active{
+    background: #000 url(image/svg_icon/shoucang.svg) center center no-repeat;
+	border-radius: 4px;
+	background-size: 24px;
+}
+/* 帮助按钮 */
+.icon-help{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-help.png) center center no-repeat;
+}
+.icon-help:hover{
+    background: url(image/icon-help-hover.png) center center no-repeat;
+}
+.icon-help:active{
+    background: url(image/icon-help-active.png) center center no-repeat;
+}
+/* 下载按钮 */
+.icon-download{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-download.png) center center no-repeat;
+}
+.icon-download:hover{
+    background: url(image/icon-download-hover.png) center center no-repeat;
+}
+.icon-download:active{
+    background: url(image/icon-download-active.png) center center no-repeat;
+}
+/* 锁定按钮 */
+.icon-lock{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/svg_icon/lock.svg) center center no-repeat;
+	background-size: 18px;
+}
+.icon-lock:hover{
+    background:#383d50 url(image/svg_icon/lock_hover.svg) center center no-repeat;
+	background-size: 18px;
+	border-radius: 4px;
+}
+.icon-lock:active{
+    background:#000 url(image/svg_icon/lock_hover.svg) center center no-repeat;
+	background-size: 18px;
+	border-radius: 4px;
+}
+/* 编辑个人选值按钮 */
+.icon-setValue{
+    display: inline-block;
+    width: 36px;
+    height: 36px;
+    background: url(image/svg_icon/gerenxuanzhi.svg) center center no-repeat;
+	background-size: 22px;
+}
+.icon-setValue:hover{
+    background:#383d50 url(image/svg_icon/gerenxuanzhi_hover.svg) center center no-repeat;
+	background-size: 22px;
+	border-radius: 4px;
+}
+.icon-setValue:active{
+    background:#000 url(image/svg_icon/gerenxuanzhi_hover.svg) center center no-repeat;
+	background-size: 22px;
+	border-radius: 4px;
+}
+/* 编辑帮助按钮 */
+.icon-setHelp{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-setHelp.png) center center no-repeat;
+}
+.icon-setHelp:hover{
+    background: url(image/icon-setHelp-hover.png) center center no-repeat;
+}
+.icon-setHelp:active{
+    background: url(image/icon-setHelp-active.png) center center no-repeat;
+}
+/* (内容)属性按钮 */
+.icon-property{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-property.png) center center no-repeat;
+}
+.icon-property:hover{
+    background: url(image/icon-property-hover.png) center center no-repeat;
+}
+.icon-property:active{
+    background: url(image/icon-property-active.png) center center no-repeat;
+}
+/* 变动查看-关按钮 */
+.icon-history{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-history.png) center center no-repeat;
+}
+.icon-history:hover{
+    background: url(image/icon-history-hover.png) center center no-repeat;
+}
+.icon-history:active{
+    background: url(image/icon-history-active.png) center center no-repeat;
+}
+/* 变动查看-开按钮 */
+.icon-historyOn{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-historyOn.png) center center no-repeat;
+}
+.icon-historyOn:hover{
+    background: url(image/icon-historyOn-hover.png) center center no-repeat;
+}
+.icon-historyOn:active{
+    background: url(image/icon-historyOn-active.png) center center no-repeat;
+}
+/* 已收藏按钮 */
+.icon-favoriteOn{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-favoriteOn.png) center center no-repeat;
+}
+.icon-favoriteOn:hover{
+    background: url(image/icon-favoriteOn-hover.png) center center no-repeat;
+}
+.icon-favoriteOn:active{
+    background: url(image/icon-favoriteOn-active.png) center center no-repeat;
+}
+/* 关闭按钮 */
+.largeIcon-close{
+    display: inline-block;
+    width: 48px !important;
+    height: 48px !important;
+	position: relative;
+    background: url(image/svg_icon/guanbi.svg) center center no-repeat;
+	background-size: 28px;
+}
+
+.largeIcon-close:hover{
+    background:#383d50 url(image/svg_icon/guanbi_hover.svg) center center no-repeat;
+	background-size: 28px;
+}
+.largeIcon-close:active{
+    background:#000 url(image/svg_icon/guanbi_hover.svg) center center no-repeat;
+	background-size: 28px;
+}
+.largeIcon-close::before{
+	width: 0;
+    height: 24px;
+    /* border-left: 1px solid  #383d50; */
+    content: "";
+    position: absolute;
+    top: 50%;
+    left: 0;
+    transform: translateY(-50%);
+}
+/* 全屏按钮 */
+.icon-fullscreen{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/svg_icon/quanping.svg) center center no-repeat;
+	background-size: 28px;
+}
+.icon-fullscreen:hover{
+    background: #383d50 url(image/svg_icon/quanping.svg) center center no-repeat;
+	background-size: 28px;
+	border-radius: 4px;
+}
+.icon-fullscreen:active{
+    background: #000 url(image/svg_icon/quanping.svg) center center no-repeat;
+	background-size: 28px;
+	border-radius: 4px;
+}
+/* 退出全屏按钮 */
+.icon-restore{
+    display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-restore.png) center center no-repeat;
+}
+.icon-restore:hover{
+    background: url(image/icon-restore-hover.png) center center no-repeat;
+}
+.icon-restore:active{
+    background: url(image/icon-restore-active.png) center center no-repeat;
+}
+
+/*quan_pin_button to bofang*/
+.dialog-restoreButton{
+	display: inline-block;
+    width: 22px;
+    height: 22px;
+    background: url(image/icon-fullscreen.png) center center no-repeat;
+}
+.dialog-restoreButton:hover{
+    background: url(image/icon-fullscreen-hover.png) center center no-repeat;
+}
+.dialog-restoreButton:active{
+    background: url(image/icon-fullscreen-active.png) center center no-repeat;
+}

BIN
skin/easy/fonts/.smbdeleteAAAa00000012eea9


BIN
skin/easy/fonts/iconfont.ttf


BIN
skin/easy/fonts/iconfont.woff


BIN
skin/easy/fonts/iconfont.woff2


BIN
skin/easy/fonts/iconfont_4273728.woff2


BIN
skin/easy/fonts/iconfont_4279221.woff2


+ 508 - 0
skin/easy/gate.css

@@ -0,0 +1,508 @@
+/*-----------------gate.css--------------*/
+.xx {
+	text-align: center;
+	color: #FFF;
+	font-family: 'MicrosoftYaHei [Regular]';
+	font-size: 14px;
+	margin-top: -6px;
+	line-height: 20px;
+}
+
+.xm {
+	/* line-height: 50px; */
+	margin-top: 12px;
+	/*margin-left: 57px;*/
+}
+
+#lfxx th {
+	text-align: right !important;
+}
+
+#yylf th {
+	text-align: right !important;
+}
+
+.bottomright {
+	position: absolute;
+	right: 29px;
+	border: 0px;
+	bottom: 29px;
+	/* padding: 8px; */
+}
+
+.qrcode-text {
+	font-family: "SIL Hei";
+	font-size: 14px;
+	line-height: 24px;
+	color: #FFF;
+	display: block;
+	text-align: center;
+	letter-spacing: 1px;
+}
+
+.hugeInvertLogo {
+	width: 569px;
+	height: 569px;
+}
+
+#sfzh::-webkit-input-placeholder {
+	font-size: 20px;
+	color: #6c9ed7;
+}
+
+#sfzh::-moz-placeholder {
+	/* Mozilla Firefox 19+ */
+	font-size: 20px;
+	color: #6c9ed7;
+}
+
+#sfzh:-moz-placeholder {
+	/* Mozilla Firefox 4 to 18 */
+	font-size: 20px;
+	color: #6c9ed7;
+}
+
+#sfzh:-ms-input-placeholder {
+	/* Internet Explorer 10-11 */
+	font-size: 20px;
+	color: #6c9ed7;
+}
+
+#leave,
+#back,
+#cancle {
+	width: 150px;
+	height: 50px;
+	font-size: 30px;
+	margin: 0;
+}
+
+#leave,
+#back {
+	margin-right: 100px;
+}
+
+.content-largeText input {
+	width: 388px;
+	height: 50px;
+	padding-left: 5px !important;
+	font-family: Microsoft YaHei;
+	font-size: 36px;
+	color: #000000;
+	line-height: 30px;
+	/* padding:5px 0 11px 19px;*/
+	margin-left: 1px;
+	border: 2px solid #cccccc;
+	border-radius: 2px;
+	outline-color: #6b9ed6;
+}
+
+.content-largeText input:hover {
+	border-color: #6b9ed6;
+}
+
+
+.content-largeText textarea {
+	width: 388px !important;
+	/*height: 50px !important;*/
+	font-family: Microsoft YaHei;
+	color: #000000;
+	line-height: 30px;
+	font-size: 36px;
+	padding-top: 6px !important;
+	/* padding: 5px 0 11px 5px;*/
+	/*margin-left: 18px;*/
+	border: 2px solid #cccccc;
+	border-radius: 2px;
+	outline-color: #6b9ed6;
+	margin-top: 1px;
+}
+
+.content-largeText td {
+	vertical-align: bottom;
+}
+
+
+.objectPickerNameDiv-jfbmid>.icon-down {
+	right: 8px !important;
+	top: 6px;
+}
+
+.objectPickerNameDiv-jfbjid>.icon-down {
+	right: 8px !important;
+	top: 6px;
+}
+
+.objectPickerNameDiv-xbm>.icon-down {
+	right: 8px !important;
+	top: 6px;
+}
+
+.objectPickerNameDiv-jfryid>.icon-down {
+	right: 8px !important;
+	top: 6px;
+}
+
+.objectPickerNameDiv-syid>.icon-down {
+	right: 8px !important;
+	top: 6px;
+
+}
+
+.getDateInfo {
+	opacity: 0.65;
+	position: absolute;
+	bottom: 30px;
+	left: 25px;
+	z-index: 100;
+	font-family: 'SIL Hei';
+	font-size: 24px;
+	color: #ffffff;
+	line-height: 21.5px;
+	border: 1px solid #dae6eb;
+	border-radius: 3px;
+	width: 206px;
+	height: 18px;
+	padding: 11px 30px 12px 21px;
+}
+
+.getDateInfo span {
+	margin-right: 35px;
+}
+
+.userInfo {
+	opacity: 0.65;
+
+	z-index: 100;
+	font-family: 'SIL Hei';
+	font-size: 24px;
+	color: #ffffff;
+	line-height: 21.5px;
+	border: 1px solid #dae6eb;
+	border-radius: 3px;
+	/* width: 206px; */
+	height: 18px;
+	padding: 11px 21px 12px 21px;
+
+	gap: 20px;
+	display: flex;
+	justify-content: center;
+}
+
+.popup-div {
+	width: 388px !important;
+	min-width: 388px !important;
+	/*margin-left: 18px;*/
+}
+
+.popup-div>.scrollbar {
+	padding-right: 0px !important;
+	min-width: 385px !important;
+
+}
+
+.popupList {
+	height: 40px !important;
+	line-height: 36px !important;
+	font-size: 30px !important;
+	white-space: nowrap;
+	overflow: hidden;
+}
+
+.smallButton.icon-down {
+	box-shadow: none !important;
+	background-image: url(image/dialog-select-icon.png) !important;
+	background-position: center center !important;
+	z-index: 9999;
+	right: 0px !important;
+	width: 40px !important;
+	height: 40px !important;
+}
+
+#jfsy {
+	line-height: 40px !important;
+	padding-top: 0px !important;
+	margin-top: 0px !important;
+}
+
+#objectPickergrowheightjfbmid {
+	height: 50px !important;
+}
+
+#objectPickergrowheightjfbjid {
+	height: 50px !important;
+}
+
+#objectPickergrowheightxbm {
+	height: 50px !important;
+}
+
+#objectPickergrowheightjfryid {
+	height: 50px !important;
+}
+
+#objectPickergrowheightsyid {
+	width: 389px !important;
+	height: 50px !important;
+}
+
+#ascrail2006 {
+	display: none !important;
+}
+
+.objectPickerNameDiv-syid {
+	margin-top: 1px;
+}
+
+.input-div-selected {
+	border: 0px !important;
+	box-shadow: 0px 0px 0px #FFF !important;
+	top: 1px !important;
+	left: 61px !important;
+	outline-color: #6b9ed6 !important;
+}
+
+#objectPickergrowheightxbm:hover {
+	border-color: #6b9ed6 !important;
+}
+
+#objectPickergrowheightxbm:active {
+	border-color: #6b9ed6;
+}
+
+#objectPickergrowheightjfbmid:hover {
+	border-color: #6b9ed6 !important;
+}
+
+#objectPickergrowheightjfbmid:active {
+	border-color: #6b9ed6;
+}
+
+#objectPickergrowheightjfbjid:hover {
+	border-color: #6b9ed6 !important;
+}
+
+#objectPickergrowheightjfbjid:active {
+	border-color: #6b9ed6 !important;
+}
+
+#jfsy:hover {
+	border-color: #6b9ed6 !important;
+}
+
+#jfsy:active {
+	border-color: #6b9ed6 !important;
+}
+
+
+/* 许汝桦202106022 边框 未经过叶老大确定 */
+.mainState-inportantrRed {
+	border: 15px solid #ff5a4b !important;
+	box-sizing: border-box;
+	background: #f9dcdb !important;
+}
+
+.mainState-inportantrRed>div {
+	margin-left: 46px !important;
+	margin-top: 47px !important;
+}
+
+div#ryxxlb::-webkit-scrollbar {
+	display: none;
+}
+
+#jcjllb::-webkit-scrollbar {
+	width: 0 !important;
+}
+
+.ryxxlb-child {
+	z-index: -3 !important;
+}
+
+.jcjllb-child {
+	z-index: -3 !important;
+}
+
+/* 绿色 */
+.mainState-green {
+	background-color: #c7ffe0 !important;
+}
+
+/*红色*/
+.mainState-orange {
+	background-color: #f9dcdb !important;
+}
+
+/*蓝色*/
+.mainState-blue {
+	background-color: #c7e0ff !important;
+}
+
+/* 三区域布局容器 - 固定三个位置 */
+#ryxxlb {
+	position: absolute;
+	width: 100%;
+	height: calc(100% - 60px);
+	display: grid;
+	grid-template-columns: 475px 535px 475px;
+	grid-template-areas: "left center right";
+	justify-content: center;
+	align-items: center;
+	gap: 30px;
+	padding: 0 50px;
+}
+
+/* 所有卡片的基础样式 */
+.ryxxlb-child {
+	transition: all 0.8s cubic-bezier(0.4, 0, 0.2, 1);
+	position: relative;
+	border: 1px white solid;
+	border-radius: 5px;
+	display: flex;
+	flex-direction: column;
+	justify-content: center;
+	align-items: center;
+	padding: 40px 30px;
+}
+
+/* 左边位置 - 小卡片 */
+.ryxxlb-child-left {
+	grid-area: left;
+	width: 475px;
+	height: 750px;
+	opacity: 0.9;
+	box-sizing: border-box;
+}
+
+/* 中间位置 - 大卡片 */
+.ryxxlb-child-center {
+	grid-area: center;
+	width: 535px;
+	height: 845px;
+	z-index: 10;
+	opacity: 1;
+	box-sizing: border-box;
+}
+
+/* 右边位置 - 小卡片 */
+.ryxxlb-child-right {
+	grid-area: right;
+	width: 475px;
+	height: 750px;
+	opacity: 0.9;
+	box-sizing: border-box;
+}
+
+/* 从中间移动到左边的动画 */
+@keyframes moveToLeft {
+	from {
+		grid-area: center;
+		width: 535px;
+		height: 845px;
+		opacity: 1;
+	}
+
+	to {
+		grid-area: left;
+		width: 475px;
+		height: 750px;
+		opacity: 0.9;
+	}
+}
+
+/* 从中间移动到右边的动画 */
+@keyframes moveToRight {
+	from {
+		grid-area: center;
+		width: 535px;
+		height: 845px;
+		opacity: 1;
+	}
+
+	to {
+		grid-area: right;
+		width: 475px;
+		height: 750px;
+		opacity: 0.9;
+	}
+}
+
+/* 淡入动画 */
+@keyframes fadeIn {
+	from {
+		opacity: 0;
+		transform: scale(0.9);
+	}
+
+	to {
+		opacity: 1;
+		transform: scale(1);
+	}
+}
+
+/* 淡出动画 */
+@keyframes fadeOut {
+	from {
+		opacity: 1;
+	}
+
+	to {
+		opacity: 0;
+		transform: scale(0.8);
+	}
+}
+
+/* 左右两边卡片的照片尺寸 */
+.ryxxlb-child-left .card-photo,
+.ryxxlb-child-right .card-photo {
+	width: 405px !important;
+	height: 548px !important;
+}
+
+/* 中间卡片的照片尺寸 */
+.ryxxlb-child-center .card-photo {
+	width: 455px !important;
+	height: 616px !important;
+}
+
+/* 左右两边卡片的姓名班级容器 */
+.ryxxlb-child-left .name-class-container,
+.ryxxlb-child-right .name-class-container {
+	font-size: 46px !important;
+}
+
+/* 中间卡片的姓名班级容器 */
+.ryxxlb-child-center .name-class-container {
+	font-size: 52px !important;
+}
+
+/* 左右两边卡片的状态文字 */
+.ryxxlb-child-left .status-text,
+.ryxxlb-child-right .status-text {
+	font-size: 46px !important;
+}
+
+/* 中间卡片的状态文字 */
+.ryxxlb-child-center .status-text {
+	font-size: 52px !important;
+}
+
+/* 进校状态文字颜色 */
+.status-enter {
+	color: #0006ff !important;
+}
+
+/* 离校状态文字颜色 */
+.status-leave {
+	color: #007339 !important;
+}
+
+/* 请假状态文字颜色 */
+.status-vacation {
+	color: #ff1212 !important;
+}
+
+/* 淡出动画类 */
+.fade-out {
+	animation: fadeOut 0.4s ease-out forwards;
+}

BIN
skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/)]BDJ_U}%HB2`9(~5X[_3H6.gif


BIN
skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/0iocntuq0mw77.gif


BIN
skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/1~CK74Y6~VGAWKZ0]3$9S@I.jpg


BIN
skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/2c06a90652cac5f927a377a7ee1fe333_5bad6ca91bd0b.gif


BIN
skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/3c7e711ca2adea83e957467144ce9d2e_5a557e794f261.gif


BIN
skin/easy/help/PC/04527db3/e38af5bd/8d7fbee0/342213b1/3f27e969531aab06b0dbc7d1521aec99_b158b81479e745b4ab28925d38bd54bc.gif


BIN
skin/easy/help/PC/29d/c3c/dc6/da1/398/eb3/42c/c18/c11/ca3/01/9.png


BIN
skin/easy/help/PC/7f40ef61/565a6831/efb01283/7adbb1db/9ce6efb31aa5c42b3329ce03aaf5aae9_5b8b5b7301a57.gif


BIN
skin/easy/help/PC/ry/0fjcklyqviz95.gif


BIN
skin/easy/help/PC/ry/25def9e1846aab73ceca6d2d959ea08e_5b513d51c5b39.gif


BIN
skin/easy/help/PC/ry/XQADGBVNXRFCUZNQ`25UBC.jpg


BIN
skin/easy/help/PC/ry/mmexport1552022002827.jpg


BIN
skin/easy/help/PC/ry/喵喵2.gif


BIN
skin/easy/help/PC/ry/微信图片_20171012135344.png


BIN
skin/easy/help/PC/ws/20150816193536_AkCru.jpeg


Some files were not shown because too many files changed in this diff