diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c987e15..a63db01 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,11 +43,30 @@ jobs: - run: npm ci --prefix tests/prompts - run: npm test --prefix tests/prompts - run: python3 tests/release/test_release.py + - run: python3 tests/release/test_web_minify.py - name: Install Chrome for actual-page regressions working-directory: tests/prompts run: npx playwright install --with-deps chrome - run: npm run test:browser --prefix tests/prompts + runtime-tests: + name: Runtime and fan regression tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Fetch matching ESP-IDF cJSON sources for host tests + run: | + export IDF_PATH="$RUNNER_TEMP/runtime-idf" + echo "IDF_PATH=$IDF_PATH" >> "$GITHUB_ENV" + git clone --depth 1 --branch "$IDF_VERSION" --filter=blob:none --sparse https://github.com/espressif/esp-idf.git "$IDF_PATH" + git -C "$IDF_PATH" sparse-checkout set components/json + git -C "$IDF_PATH" submodule update --init --depth 1 components/json/cJSON + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: bash tests/runtime/run_all.sh + - run: bash tests/fan_monitor/run_host.sh + build: name: Build ESP32-S3 Firmware runs-on: ubuntu-latest @@ -154,7 +173,7 @@ jobs: # 编译成功后自动创建或更新 Release:tag 推送、main 推送或手动发布 Release release: name: Create or Update Release - needs: [build, web-tests] + needs: [build, web-tests, runtime-tests] if: github.repository == 'RMinte-AI/TianshanOS' && (github.event_name == 'release' || startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main' && github.event_name == 'push')) runs-on: ubuntu-latest permissions: diff --git a/build/TianShanOS.bin b/build/TianShanOS.bin index 86c142e..7021387 100644 Binary files a/build/TianShanOS.bin and b/build/TianShanOS.bin differ diff --git a/build/bootloader/bootloader.bin b/build/bootloader/bootloader.bin index b200e69..3b6b374 100644 Binary files a/build/bootloader/bootloader.bin and b/build/bootloader/bootloader.bin differ diff --git a/build/www.bin b/build/www.bin index 14be972..418c598 100644 Binary files a/build/www.bin and b/build/www.bin differ diff --git a/components/ts_api/src/ts_api_ssh.c b/components/ts_api/src/ts_api_ssh.c index 5d6ef53..e72a9c0 100644 --- a/components/ts_api/src/ts_api_ssh.c +++ b/components/ts_api/src/ts_api_ssh.c @@ -678,6 +678,9 @@ static esp_err_t api_ssh_test(const cJSON *params, ts_api_result_t *result) * 2. 验证主机指纹(Known Hosts) * 3. 部署公钥到 authorized_keys * 4. 使用公钥认证验证部署是否成功 + * 5. 登记主机到 NVS;登记失败保留 deployed/verified 并返回业务错误 + * + * Result data adds host_id, registered (NVS committed), registration_error (on failure). * * Params: { * "host": "192.168.1.100", @@ -846,10 +849,11 @@ static esp_err_t api_ssh_copyid(const cJSON *params, ts_api_result_t *result) free(pubkey_data); - /* 部署成功后,自动注册到 SSH 主机配置(无论验证是否成功) */ - if (deploy_ok) { + /* Registration is independent of remote deployment and login verification. */ + esp_err_t add_ret; + char auto_id[TS_SSH_HOST_ID_MAX]; + { /* 生成主机 ID:user@host:port */ - char auto_id[TS_SSH_HOST_ID_MAX]; if (ssh_port == 22) { snprintf(auto_id, sizeof(auto_id), "%s@%s", user->valuestring, host->valuestring); @@ -868,9 +872,12 @@ static esp_err_t api_ssh_copyid(const cJSON *params, ts_api_result_t *result) strncpy(host_config.username, user->valuestring, sizeof(host_config.username) - 1); strncpy(host_config.keyid, keyid->valuestring, sizeof(host_config.keyid) - 1); - esp_err_t add_ret = ts_ssh_hosts_config_add(&host_config); + add_ret = ts_ssh_hosts_config_add(&host_config); if (add_ret == ESP_OK) { TS_LOGI(TAG, "Auto-registered SSH host: %s", auto_id); + } else { + TS_LOGW(TAG, "Public key deployed but host %s registration failed: %s", + auto_id, esp_err_to_name(add_ret)); } } @@ -882,8 +889,20 @@ static esp_err_t api_ssh_copyid(const cJSON *params, ts_api_result_t *result) cJSON_AddNumberToObject(data, "port", ssh_port); cJSON_AddStringToObject(data, "user", user->valuestring); cJSON_AddStringToObject(data, "keyid", keyid->valuestring); + cJSON_AddStringToObject(data, "host_id", auto_id); + cJSON_AddBoolToObject(data, "registered", add_ret == ESP_OK); - ts_api_result_ok(result, data); + if (add_ret == ESP_OK) { + ts_api_result_ok(result, data); + } else { + cJSON_AddStringToObject(data, "registration_error", esp_err_to_name(add_ret)); + ts_api_result_code_t code = add_ret == ESP_ERR_INVALID_STATE ? TS_API_ERR_BUSY + : add_ret == ESP_ERR_NO_MEM ? TS_API_ERR_NO_MEM + : TS_API_ERR_INTERNAL; + ts_api_result_error(result, code, "host_registration_failed"); + result->data = data; + } + /* Keep partial-result data in the existing HTTP response serializer. */ return ESP_OK; } diff --git a/components/ts_drivers/src/ts_fan.c b/components/ts_drivers/src/ts_fan.c index 8c3cf39..64c954f 100644 --- a/components/ts_drivers/src/ts_fan.c +++ b/components/ts_drivers/src/ts_fan.c @@ -620,19 +620,9 @@ static void fan_update_callback(void *arg) ts_temp_data_t temp_data = {0}; bool have_temp_data = false; bool basic_temp_data_valid = false; - bool needs_temp_data = false; - - for (int i = 0; i < TS_FAN_MAX; i++) { - if (s_fans[i].initialized && - (s_fans[i].mode == TS_FAN_MODE_CURVE || - s_fans[i].mode == TS_FAN_MODE_AUTO)) { - needs_temp_data = true; - break; - } - } - /* 主动获取最新温度(确保曲线模式能及时响应温度变化) */ - if (s_auto_temp_enabled && needs_temp_data) { + /* 温度监控独立于风扇模式,手动/关闭模式下也保持有效温度快照更新。 */ + if (s_auto_temp_enabled) { int16_t current_temp = ts_temp_get_effective_nonblocking(&temp_data); have_temp_data = true; basic_temp_data_valid = is_temp_data_basic_valid(&temp_data); @@ -647,9 +637,7 @@ static void fan_update_callback(void *arg) if (basic_temp_data_valid) { for (int i = 0; i < TS_FAN_MAX; i++) { - if (s_fans[i].initialized && - (s_fans[i].mode == TS_FAN_MODE_CURVE || - s_fans[i].mode == TS_FAN_MODE_AUTO)) { + if (s_fans[i].initialized) { s_fans[i].temperature = current_temp; } } diff --git a/components/ts_security/include/ts_ssh_service.h b/components/ts_security/include/ts_ssh_service.h index 84ef13a..0783c9d 100644 --- a/components/ts_security/include/ts_ssh_service.h +++ b/components/ts_security/include/ts_ssh_service.h @@ -36,6 +36,8 @@ bool ts_ssh_service_start_admissible(const char *id); bool ts_ssh_service_command_protected(const char *id); bool ts_ssh_service_host_protected(const char *host_id); +/* Missing-host recovery: inspect runtime ownership, not historical command references. */ +bool ts_ssh_service_host_runtime_protected(const char *host_id); void ts_ssh_service_cancel_observation(const char *id, uint32_t generation); bool ts_ssh_service_any_in_use(void); diff --git a/components/ts_security/src/ts_ssh_hosts_config.c b/components/ts_security/src/ts_ssh_hosts_config.c index 40b399f..26a223f 100644 --- a/components/ts_security/src/ts_ssh_hosts_config.c +++ b/components/ts_security/src/ts_ssh_hosts_config.c @@ -55,6 +55,7 @@ static struct { bool initialized; nvs_handle_t nvs; SemaphoreHandle_t mutex; + SemaphoreHandle_t sd_mutex; /* Serialize restore input with backup output. */ } s_state = {0}; /*===========================================================================*/ @@ -76,6 +77,7 @@ static uint32_t get_current_time(void) /* 前向声明 - SD 卡操作 */ static bool is_sdcard_mounted(void); static void delete_host_file(const char *id); +static esp_err_t host_add_guarded(const ts_ssh_host_config_t *cfg, bool sync_sdcard); /*===========================================================================*/ /* Initialization */ @@ -84,8 +86,6 @@ static void delete_host_file(const char *id); /** 延迟导出标志 */ static bool s_hosts_pending_export = false; -/** 正在从 SD 卡加载中(禁止触发同步) */ -static bool s_loading_from_sdcard = false; static TaskHandle_t initial_loader; /** @@ -110,7 +110,7 @@ static void hosts_deferred_export_task(void *arg) /* * 配置加载优先级:SD 卡 (.tscfg > .json) > NVS * - * 如果 SD 卡有配置文件,以 SD 卡为权威来源(清空 NVS 后导入) + * 如果 SD 卡有配置文件,逐条合并(保留其他 NVS 记录) * 如果 SD 卡没有配置文件,保留 NVS 数据并导出到 SD 卡 */ ESP_LOGI(TAG, "Deferred: checking SD card for config (NVS has %d hosts)...", (int)nvs_count); @@ -133,17 +133,8 @@ static void hosts_deferred_export_task(void *arg) } if (sdcard_has_config) { - /* SD 卡有配置,清空 NVS 后导入(SD 卡为权威来源) */ - ESP_LOGI(TAG, "SD card has config, clearing NVS and importing..."); - esp_err_t cleared = ts_ssh_hosts_config_clear(); - if (cleared != ESP_OK) { - s_loading_from_sdcard = false; - initial_loader = NULL; - vTaskDelete(NULL); - return; - } - - esp_err_t import_ret = ts_ssh_hosts_config_import_from_sdcard(false); + ESP_LOGI(TAG, "SD card has config, merging into NVS..."); + esp_err_t import_ret = ts_ssh_hosts_config_import_from_sdcard(true); size_t count = ts_ssh_hosts_config_count(); if (import_ret == ESP_OK) { @@ -179,10 +170,17 @@ esp_err_t ts_ssh_hosts_config_init(void) return ESP_ERR_NO_MEM; } + s_state.sd_mutex = xSemaphoreCreateMutex(); + if (!s_state.sd_mutex) { + vSemaphoreDelete(s_state.mutex); + return ESP_ERR_NO_MEM; + } + /* 打开 NVS */ esp_err_t ret = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &s_state.nvs); if (ret != ESP_OK) { ESP_LOGE(TAG, "Failed to open NVS: %s", esp_err_to_name(ret)); + vSemaphoreDelete(s_state.sd_mutex); vSemaphoreDelete(s_state.mutex); return ret; } @@ -215,6 +213,7 @@ void ts_ssh_hosts_config_deinit(void) } nvs_close(s_state.nvs); + vSemaphoreDelete(s_state.sd_mutex); vSemaphoreDelete(s_state.mutex); s_state.initialized = false; } @@ -228,7 +227,7 @@ bool ts_ssh_hosts_config_is_initialized(void) /* CRUD Operations */ /*===========================================================================*/ -static esp_err_t host_add_impl(const ts_ssh_host_config_t *config) { +static esp_err_t host_add_impl(const ts_ssh_host_config_t *config, bool sync_sdcard) { if (!s_state.initialized || !config || !config->id[0] || !config->host[0]) { return ESP_ERR_INVALID_ARG; } @@ -252,8 +251,11 @@ static esp_err_t host_add_impl(const ts_ssh_host_config_t *config) { existing_index = i; break; } - } else if (free_index < 0) { - free_index = i; + } else if (ret == ESP_ERR_NVS_NOT_FOUND) { + if (free_index < 0) free_index = i; + } else { + xSemaphoreGive(s_state.mutex); + return ret == ESP_OK ? ESP_ERR_INVALID_SIZE : ret; } } @@ -290,8 +292,8 @@ static esp_err_t host_add_impl(const ts_ssh_host_config_t *config) { existing_index >= 0 ? "Updated" : "Added", config->id, config->username, config->host, entry.port); - /* 同步到 SD 卡(加载期间不触发,避免文件描述符用尽) */ - if (!s_loading_from_sdcard) { + /* Only this import's writes skip backup; concurrent normal adds still sync. */ + if (sync_sdcard) { ts_ssh_hosts_config_sync_to_sdcard(); } } @@ -352,21 +354,25 @@ esp_err_t ts_ssh_hosts_config_get(const char *id, ts_ssh_host_config_t *config) for (int i = 0; i < TS_SSH_HOSTS_MAX; i++) { make_nvs_key(i, key, sizeof(key)); len = sizeof(entry); - if (nvs_get_blob(s_state.nvs, key, &entry, &len) == ESP_OK && len == sizeof(entry)) { - if (strcmp(entry.id, id) == 0) { - memset(config, 0, sizeof(*config)); - strncpy(config->id, entry.id, sizeof(config->id) - 1); - strncpy(config->host, entry.host, sizeof(config->host) - 1); - config->port = entry.port; - strncpy(config->username, entry.username, sizeof(config->username) - 1); - config->auth_type = (ts_ssh_host_auth_type_t)entry.auth_type; - strncpy(config->keyid, entry.keyid, sizeof(config->keyid) - 1); - config->created_time = entry.created_time; - config->last_used_time = entry.last_used_time; - config->enabled = entry.enabled != 0; - ret = ESP_OK; - break; - } + esp_err_t read_ret = nvs_get_blob(s_state.nvs, key, &entry, &len); + if (read_ret == ESP_ERR_NVS_NOT_FOUND) continue; + if (read_ret != ESP_OK || len != sizeof(entry)) { + ret = read_ret == ESP_OK ? ESP_ERR_INVALID_SIZE : read_ret; + break; + } + if (strcmp(entry.id, id) == 0) { + memset(config, 0, sizeof(*config)); + strncpy(config->id, entry.id, sizeof(config->id) - 1); + strncpy(config->host, entry.host, sizeof(config->host) - 1); + config->port = entry.port; + strncpy(config->username, entry.username, sizeof(config->username) - 1); + config->auth_type = (ts_ssh_host_auth_type_t)entry.auth_type; + strncpy(config->keyid, entry.keyid, sizeof(config->keyid) - 1); + config->created_time = entry.created_time; + config->last_used_time = entry.last_used_time; + config->enabled = entry.enabled != 0; + ret = ESP_OK; + break; } } @@ -567,9 +573,9 @@ esp_err_t ts_ssh_hosts_config_iterate(ts_ssh_host_iterator_cb_t callback, if (!s_state.initialized || !callback) { return ESP_ERR_INVALID_ARG; } - + xSemaphoreTake(s_state.mutex, portMAX_DELAY); - + char key[16]; nvs_host_entry_t entry; ts_ssh_host_config_t cfg; @@ -577,44 +583,49 @@ esp_err_t ts_ssh_hosts_config_iterate(ts_ssh_host_iterator_cb_t callback, size_t total = 0; /* 总有效条目数 */ size_t returned = 0; /* 实际返回给回调的数量 */ size_t skipped = 0; /* 跳过的条目数(offset) */ - + for (int i = 0; i < TS_SSH_HOSTS_MAX; i++) { make_nvs_key(i, key, sizeof(key)); len = sizeof(entry); - if (nvs_get_blob(s_state.nvs, key, &entry, &len) == ESP_OK && len == sizeof(entry)) { - total++; - - /* 处理分页偏移 */ - if (skipped < offset) { - skipped++; - continue; - } - - /* 处理分页限制 */ - if (limit > 0 && returned >= limit) { - continue; /* 继续计数但不回调 */ - } - - /* 转换并回调 */ - host_entry_to_config(&entry, &cfg); - bool cont = callback(&cfg, returned, user_data); - returned++; - - if (!cont) { - break; /* 回调请求停止 */ - } + esp_err_t ret = nvs_get_blob(s_state.nvs, key, &entry, &len); + if (ret == ESP_ERR_NVS_NOT_FOUND) continue; + if (ret != ESP_OK || len != sizeof(entry)) { + xSemaphoreGive(s_state.mutex); + return ret == ESP_OK ? ESP_ERR_INVALID_SIZE : ret; + } + total++; + + /* 处理分页偏移 */ + if (skipped < offset) { + skipped++; + continue; + } + + /* 处理分页限制 */ + if (limit > 0 && returned >= limit) { + continue; /* 继续计数但不回调 */ + } + + /* 转换并回调 */ + host_entry_to_config(&entry, &cfg); + bool cont = callback(&cfg, returned, user_data); + returned++; + + if (!cont) { + break; /* 回调请求停止 */ } } - + xSemaphoreGive(s_state.mutex); - + if (total_count) { *total_count = total; } - + return ESP_OK; } + /*===========================================================================*/ /* SD Card Export/Import (持久化备份) */ /*===========================================================================*/ @@ -896,6 +907,7 @@ static esp_err_t load_hosts_from_dir(void) int loaded = 0; int skipped = 0; + esp_err_t first_error = ESP_OK; struct dirent *entry; /* 使用堆分配避免栈溢出 */ @@ -931,6 +943,9 @@ static esp_err_t load_hosts_from_dir(void) /* 限制文件名长度避免缓冲区溢出 */ if (len > 60) { + skipped++; + if (first_error == ESP_OK) first_error = ESP_ERR_INVALID_SIZE; + ESP_LOGW(TAG, "Host filename too long: %s", entry->d_name); continue; } @@ -952,6 +967,9 @@ static esp_err_t load_hosts_from_dir(void) filepath, &content, &content_len, &used_tscfg); if (ret != ESP_OK) { + skipped++; + if (first_error == ESP_OK) first_error = ret; + ESP_LOGW(TAG, "Failed to load host file %s: %s", filepath, esp_err_to_name(ret)); continue; } @@ -962,22 +980,26 @@ static esp_err_t load_hosts_from_dir(void) if (!root) { ESP_LOGW(TAG, "Failed to parse JSON from %s", filepath); skipped++; + if (first_error == ESP_OK) first_error = ESP_ERR_INVALID_ARG; continue; } /* 解析并添加 */ memset(cfg, 0, sizeof(ts_ssh_host_config_t)); - if (json_to_host(root, cfg) == ESP_OK && cfg->host[0] && cfg->username[0]) { - esp_err_t add_ret = ts_ssh_hosts_config_add(cfg); - if (add_ret == ESP_OK) { - loaded++; - ESP_LOGD(TAG, "Loaded host from file: %s%s", cfg->id, - used_tscfg ? " (encrypted)" : ""); - } else { - skipped++; - } + ret = json_to_host(root, cfg); + if (ret == ESP_OK && (!cfg->id[0] || !cfg->host[0] || !cfg->username[0])) + ret = ESP_ERR_INVALID_ARG; + if (ret == ESP_OK) + ret = host_add_guarded(cfg, false); + if (ret == ESP_OK) { + loaded++; + ESP_LOGD(TAG, "Loaded host from file: %s%s", cfg->id, + used_tscfg ? " (encrypted)" : ""); } else { skipped++; + if (first_error == ESP_OK) first_error = ret; + ESP_LOGW(TAG, "Failed to import host %s from %s: %s", + cfg->id, filepath, esp_err_to_name(ret)); } cJSON_Delete(root); @@ -986,13 +1008,10 @@ static esp_err_t load_hosts_from_dir(void) free(cfg); closedir(dir); - if (loaded > 0) { - ESP_LOGI(TAG, "Loaded %d SSH hosts from directory: %s (skipped %d)", - loaded, TS_SSH_HOSTS_SDCARD_DIR, skipped); - return ESP_OK; - } - - return ESP_ERR_NOT_FOUND; + ESP_LOGI(TAG, "SSH host import: loaded %d, failed %d, first error %s", + loaded, skipped, esp_err_to_name(first_error)); + if (first_error != ESP_OK) return first_error; + return loaded > 0 ? ESP_OK : ESP_ERR_NOT_FOUND; } /** @@ -1022,15 +1041,20 @@ esp_err_t ts_ssh_hosts_config_export_to_sdcard(void) return ESP_ERR_NOT_FOUND; } + /* Lock order: SD -> NVS for backup, SD -> binding -> NVS for import. + * Normal registration only queues backup, never waits for this lock. */ + xSemaphoreTake(s_state.sd_mutex, portMAX_DELAY); /* 确保独立文件目录存在 */ esp_err_t ret = ensure_hosts_dir(); if (ret != ESP_OK) { + xSemaphoreGive(s_state.sd_mutex); return ret; } /* 只导出到独立文件(不再生成主配置文件) */ int count = 0; ret = ts_ssh_hosts_config_iterate(host_export_iterator_cb, &count, 0, 0, NULL); + xSemaphoreGive(s_state.sd_mutex); if (ret == ESP_OK) { ESP_LOGI(TAG, "Exported %d SSH hosts to %s/", count, TS_SSH_HOSTS_SDCARD_DIR); @@ -1049,24 +1073,19 @@ esp_err_t ts_ssh_hosts_config_import_from_sdcard(bool merge) return ESP_ERR_NOT_FOUND; } - /* 设置加载标志,禁止 add 函数触发同步 */ - s_loading_from_sdcard = true; - + xSemaphoreTake(s_state.sd_mutex, portMAX_DELAY); /* 如果不是合并模式,先清空现有配置 */ if (!merge) { esp_err_t cleared = ts_ssh_hosts_config_clear(); if (cleared != ESP_OK) { - s_loading_from_sdcard = false; + xSemaphoreGive(s_state.sd_mutex); return cleared; } } /* 只从目录加载独立文件(.tscfg 优先于 .json) */ esp_err_t ret = load_hosts_from_dir(); - - /* 清除加载标志 */ - s_loading_from_sdcard = false; - + xSemaphoreGive(s_state.sd_mutex); return ret; } @@ -1093,21 +1112,30 @@ void ts_ssh_hosts_config_sync_to_sdcard(void) xTaskCreate(hosts_async_sync_task, "ssh_host_sync", 8192, NULL, 2, NULL); } -esp_err_t ts_ssh_hosts_config_add(const ts_ssh_host_config_t *cfg) { +static esp_err_t host_add_guarded(const ts_ssh_host_config_t *cfg, bool sync_sdcard) { if (!cfg) return ESP_ERR_INVALID_ARG; ts_ssh_binding_lock(); ts_ssh_host_config_t old; esp_err_t got = ts_ssh_hosts_config_get(cfg->id, &old); + if (got != ESP_OK && got != ESP_ERR_NOT_FOUND) { + ts_ssh_binding_unlock(); + return got; + } bool changed = got == ESP_OK && (strcmp(old.host, cfg->host) || old.port != cfg->port || strcmp(old.username, cfg->username) || strcmp(old.keyid, cfg->keyid) || old.auth_type != cfg->auth_type); - esp_err_t ret = (changed || got == ESP_ERR_NOT_FOUND) && ts_ssh_service_host_protected(cfg->id) ? ESP_ERR_INVALID_STATE - : host_add_impl(cfg); + bool protected = got == ESP_ERR_NOT_FOUND + ? ts_ssh_service_host_runtime_protected(cfg->id) + : changed && ts_ssh_service_host_protected(cfg->id); + esp_err_t ret = protected ? ESP_ERR_INVALID_STATE : host_add_impl(cfg, sync_sdcard); ts_ssh_binding_unlock(); return ret; } +esp_err_t ts_ssh_hosts_config_add(const ts_ssh_host_config_t *cfg) { + return host_add_guarded(cfg, true); +} esp_err_t ts_ssh_hosts_config_remove(const char *id) { if (!id) return ESP_ERR_INVALID_ARG; diff --git a/components/ts_security/src/ts_ssh_service.c b/components/ts_security/src/ts_ssh_service.c index f1532ad..5d3dd2b 100644 --- a/components/ts_security/src/ts_ssh_service.c +++ b/components/ts_security/src/ts_ssh_service.c @@ -508,6 +508,17 @@ static bool protect_host(const ts_ssh_command_config_t *cmd, size_t index, void *busy = true; return !*busy; } +bool ts_ssh_service_host_runtime_protected(const char *host_id) { + if (!binding_gate || !mutex || !services || !host_id) + return true; + bool busy = false; + xSemaphoreTake(mutex, portMAX_DELAY); + for (unsigned i = 0; i < TS_SSH_COMMANDS_MAX; ++i) + if (services[i].id[0] && !strcmp(services[i].host_id, host_id) && occupied(&services[i])) + busy = true; + xSemaphoreGive(mutex); + return busy; +} bool ts_ssh_service_host_protected(const char *host_id) { bool busy = false; xSemaphoreTake(mutex, portMAX_DELAY); diff --git a/components/ts_webui/web/css/style.css b/components/ts_webui/web/css/style.css index 12c6fda..e25e5ea 100644 --- a/components/ts_webui/web/css/style.css +++ b/components/ts_webui/web/css/style.css @@ -1,9185 +1,562 @@ -:root { - /* ===== Tremor Design Tokens ===== */ - - /* -- Semantic Color Scales -- */ - --blue-50: rgba(59, 130, 246, 0.06); - --blue-100: rgba(59, 130, 246, 0.12); - --blue-500: #3b82f6; - --blue-600: #2563eb; - - --emerald-50: rgba(16, 185, 129, 0.06); - --emerald-100: rgba(16, 185, 129, 0.12); - --emerald-500: #10b981; - --emerald-600: #059669; - - --rose-50: rgba(244, 63, 94, 0.06); - --rose-100: rgba(244, 63, 94, 0.12); - --rose-500: #f43f5e; - --rose-600: #e11d48; - - --amber-50: rgba(245, 158, 11, 0.06); - --amber-100: rgba(245, 158, 11, 0.12); - --amber-500: #f59e0b; - --amber-600: #d97706; - - --violet-500: #8b5cf6; - --cyan-500: #06b6d4; - - /* -- Backgrounds -- */ - --bg-page: #f9fafb; - --bg-card: #ffffff; - --bg-muted: #f3f4f6; - --bg-subtle: #f9fafb; - - /* -- Text -- */ - --text-primary: #111827; - --text-secondary: #6b7280; - --text-muted: #9ca3af; - --text-faint: #d1d5db; - - /* -- Border -- */ - --border: #e5e7eb; - --border-hover: #d1d5db; - --ring: #3b82f6; - - /* -- Radius -- */ - --radius-sm: 0.375rem; - --radius: 0.5rem; - --radius-lg: 0.75rem; - --radius-full: 9999px; - - /* -- Shadows (Tailwind) -- */ - --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); - --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); - - /* -- Transitions -- */ - --ease: cubic-bezier(0.4, 0, 0.2, 1); - --t-fast: 0.15s var(--ease); - --t-normal: 0.2s var(--ease); - - /* -- Fonts -- */ - --font-mono: 'SF Mono', 'Fira Code', 'Menlo', monospace; - - /* ===== Backward-Compatible Aliases (referenced by JS) ===== */ - - /* Old primary names */ - --primary-color: #3b82f6; - --primary-dark: #2563eb; - --secondary-color: #10b981; - --danger-color: #f43f5e; - --warning-color: #f59e0b; - --success-color: #10b981; - - /* Shorthand aliases */ - --primary: #3b82f6; - --primary-rgb: 59, 130, 246; - --warning: #f59e0b; - --error: #f43f5e; - --success: #10b981; - --danger: #f43f5e; - - /* Text aliases */ - --text-color: #111827; - --text-light: #6b7280; - - /* Background aliases */ - --bg-color: #f9fafb; - --bg-primary: #ffffff; - --bg-secondary: #f9fafb; - --bg-tertiary: #f3f4f6; - --bg-hover: #f3f4f6; - --bg-elevated: #f9fafb; - --card-bg: #ffffff; - - /* Border alias */ - --border-color: #e5e7eb; - - /* Layout */ - --header-height: 60px; - --footer-height: 50px; -} - -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -body { - font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: var(--bg-page); - color: var(--text-primary); - line-height: 1.5; - -webkit-font-smoothing: antialiased; - font-size: 14px; -} - -#app { - min-height: 100vh; - display: flex; - flex-direction: column; -} - -/* Header */ -.header { - height: var(--header-height); - background: var(--bg-card); - border-bottom: 1px solid var(--border); - display: flex; - align-items: center; - padding: 0 20px; - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 100; - box-shadow: var(--shadow-sm); -} - -.logo { - display: flex; - align-items: center; - gap: 8px; - font-size: 1.5rem; - font-weight: bold; -} - -.logo-icon { - width: 32px; - height: 32px; - object-fit: contain; - display: block; -} - -.nav { - display: flex; - gap: 20px; - margin-left: 0; -} - -.nav-link { - color: var(--text-secondary); - text-decoration: none; - padding: 8px 12px; - border-radius: var(--radius); - transition: all var(--t-fast); - font-size: 0.875rem; - font-weight: 500; -} - -.nav-link:hover, .nav-link.active { - color: var(--blue-500); - background: var(--blue-50); -} - -.user-menu { - margin-left: auto; - display: flex; - align-items: center; - gap: 12px; -} - -/* Main */ -.main { - flex: 1; - margin-top: var(--header-height); - padding: 20px; -} - -/* Footer */ -.footer { - height: var(--footer-height); - line-height: var(--footer-height); - margin-top: -14px; - background: transparent; - border-top: none; - text-align: center; - color: var(--text-muted); - font-size: 0.8rem; -} - -.footer p { - margin: 0; - display: inline; - line-height: inherit; -} - -/* Cards */ -.cards { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 20px; - margin-top: 0; -} - -.card { - background: var(--bg-card); - border-radius: var(--radius-lg); - padding: 20px; - box-shadow: var(--shadow); - border: 1px solid var(--border); - transition: box-shadow var(--t-normal); -} - -.card:hover { - box-shadow: var(--shadow-md); -} - -.card h3 { - margin-bottom: 16px; - color: var(--text-primary); - border-bottom: 1px solid var(--border); - padding-bottom: 8px; - font-size: 0.875rem; - font-weight: 600; -} - -.card-content p { - margin: 8px 0; -} - -/* Progress Bar */ -.progress-bar { - height: 8px; - background: var(--bg-muted); - border-radius: var(--radius-full); - overflow: hidden; - margin: 6px 0; -} - -.progress { - height: 100%; - background: var(--blue-500); - border-radius: var(--radius-full); - transition: width 0.3s ease; -} - -/* DRAM/PSRAM */ -#heap-progress, #psram-progress { - background: var(--emerald-500); -} - -/* Buttons (Tremor style) */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - padding: 7px 14px; - font-size: 0.8rem; - font-weight: 500; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-card); - color: var(--text-primary); - cursor: pointer; - transition: all var(--t-fast); - white-space: nowrap; - font-family: inherit; - line-height: 1.4; - box-shadow: var(--shadow-sm); -} - -.btn:hover { - background: var(--bg-muted); - border-color: var(--border-hover); -} - -.btn i { font-size: 0.85em; } - -.btn-primary { - background: var(--blue-500); - border-color: var(--blue-500); - color: #fff; - font-weight: 600; - box-shadow: var(--shadow-sm); -} -.btn-primary:hover { - background: var(--blue-600); - border-color: var(--blue-600); -} - -.btn-danger { - background: var(--rose-50); - border-color: transparent; - color: var(--rose-600); - box-shadow: none; -} -.btn-danger:hover { - background: var(--rose-100); -} - -/* Gray button */ -button.btn-gray, -.btn-gray { - background: var(--bg-card) !important; - color: var(--text-secondary) !important; - border: 1px solid var(--border) !important; - box-shadow: var(--shadow-sm) !important; -} -button.btn-gray:hover, -.btn-gray:hover { - background: var(--bg-muted) !important; - border-color: var(--border-hover) !important; -} - -/* Success button */ -.btn-success { - background: var(--emerald-50); - color: var(--emerald-600); - border: 1px solid transparent; - box-shadow: none; -} -.btn-success:hover { - background: var(--emerald-100); -} - -/* Modal */ -.modal { - 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: 1000; - opacity: 1; - visibility: visible; - transition: opacity 0.3s ease, visibility 0.3s ease; -} - -.modal.hidden { - opacity: 0; - visibility: hidden; - pointer-events: none; -} - -.modal-content { - background: var(--bg-card); - padding: 24px; - border-radius: var(--radius-lg); - min-width: 360px; - max-width: 90%; - transform: translateY(0); - transition: transform 0.3s ease; - box-shadow: var(--shadow-lg); - border: 1px solid var(--border); -} - -.modal.hidden .modal-content { - transform: translateY(-20px); -} - -/* Modal header */ -.modal-header { - display: flex; - align-items: center; - justify-content: flex-start; - margin-bottom: 12px; - padding-bottom: 10px; - border-bottom: 1px solid var(--border); - gap: 8px; -} - -.modal-header h2 { - margin: 0; - font-size: 0.9375rem; - font-weight: 600; - line-height: 28px; - height: 28px; - display: flex; - align-items: center; - gap: 8px; -} - -.modal-header h2 i { - color: var(--blue-500); -} - -#led-modal-header-actions { - display: flex; - gap: 8px; - margin-left: auto; - margin-right: 8px; - align-items: center; -} - -#led-modal-header-actions .btn { - height: 28px; - line-height: 28px; - padding: 0 10px; -} - -.modal-close { - width: 32px; - height: 32px; - min-width: 32px; - margin-left: auto; - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - padding: 0; - font-size: 1.1rem; - color: var(--text-muted); - background: transparent; - border: none; - cursor: pointer; - border-radius: 50%; - transition: all var(--t-fast); -} - -.modal-close:hover { - background: var(--rose-50); - color: var(--rose-500); -} - -.modal-body { - max-height: 70vh; - overflow-y: auto; -} - -.modal-footer { - display: flex; - justify-content: flex-end; - gap: 12px; - margin-top: 20px; - padding-top: 16px; - border-top: 1px solid var(--border); -} - -/* LED 模态框 */ -.led-modal { - min-width: 420px; - max-width: 560px; -} - -/* Color correction modal - compact */ -.cc-compact { - padding: 0 !important; - background: var(--bg-subtle) !important; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.cc-compact .modal-header { - padding: 12px 20px !important; - background: var(--bg-card); - border-bottom: 1px solid var(--border); - margin-bottom: 0; - display: flex; - align-items: flex-start; - justify-content: space-between; - position: relative; -} - -.cc-compact .modal-header h2 { - margin: 0 !important; - padding: 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - text-align: left; - line-height: 1.2; -} - -.cc-compact #led-modal-header-actions, -.cc-compact #command-modal-header-actions { - display: flex; - gap: 8px; - margin-right: 35px; /* 为关闭按钮留位 */ -} - -.cc-compact .modal-close { - position: absolute; - top: 12px; - right: 12px; - font-size: 1.1rem; - background: transparent; - border: none; - cursor: pointer; - color: var(--text-muted); - line-height: 1; -} - -.cc-compact .modal-header .btn-service-style { - background: var(--blue-50); - color: var(--blue-600); - border: 1px solid transparent; - padding: 4px 12px; - border-radius: var(--radius-sm); - font-weight: 500; - font-size: 0.8rem; - transition: all var(--t-fast); - line-height: 1.2; -} - -.cc-compact .modal-header .btn-service-style:hover { - background: var(--blue-100); - color: var(--blue-600); -} - -.cc-compact .modal-body { - padding: 20px; - background: var(--bg-subtle); - flex: 1; - overflow-y: auto; - max-height: 75vh; -} - -.cc-compact .modal-footer { - padding: 12px 20px; - background: var(--bg-subtle); - border-top: 1px solid var(--border); - display: flex; - justify-content: flex-end; - gap: 12px; -} - -.cc-compact .modal-tabs, -.cc-compact .automation-modal-tabs { - display: flex; - border-bottom: 1px solid var(--border); - margin-bottom: 16px; -} - -.cc-compact .modal-tabs .modal-tab, -.cc-compact .automation-modal-tabs .modal-tab { - flex: 1; - padding: 10px 16px; - border: none; - background: transparent; - cursor: pointer; - font-size: 0.85rem; - font-weight: 500; - color: var(--text-secondary); - border-bottom: 2px solid transparent; - margin-bottom: -1px; - transition: all var(--t-fast); - font-family: inherit; -} - -.cc-compact .modal-tabs .modal-tab:hover, -.cc-compact .automation-modal-tabs .modal-tab:hover { - color: var(--text-primary); - background: var(--bg-muted); -} - -.cc-compact .modal-tabs .modal-tab.active, -.cc-compact .automation-modal-tabs .modal-tab.active { - background: var(--blue-50); - color: var(--blue-600); - border-bottom-color: var(--blue-500); - font-weight: 600; -} - -.led-modal .modal-tabs { - display: flex; - border-bottom: 1px solid var(--border); - margin-bottom: 16px; -} - -.led-modal .modal-tab { - flex: 1; - padding: 10px 12px; - font-size: 0.85rem; - font-weight: 500; - background: transparent; - border: none; - border-bottom: 2px solid transparent; - margin-bottom: -1px; - cursor: pointer; - color: var(--text-secondary); - transition: all var(--t-fast); - font-family: inherit; -} - -.led-modal .modal-tab:hover { - color: var(--text-primary); - background: var(--bg-muted); -} - -.led-modal .modal-tab.active { - color: var(--blue-600); - border-bottom-color: var(--blue-500); -} - -.led-modal .modal-tab-content { - display: none; -} - -.led-modal .modal-tab-content.active { - display: block; -} - -.modal-section { - margin-bottom: 16px; -} - -.modal-section h3 { - font-size: 0.875rem; - font-weight: 600; - margin-bottom: 12px; - color: var(--text-primary); -} - -/* 特效网格 */ -.effects-grid { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 12px; -} - -.effects-grid .effect-btn { - padding: 8px 12px; - font-size: 0.85rem; -} - -/* 滤镜网格 */ -.filters-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; - margin-bottom: 12px; -} - -.filters-grid .filter-btn { - padding: 10px 8px; - font-size: 0.8rem; - text-align: center; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: all var(--t-fast); - font-family: inherit; - font-weight: 500; -} - -.filters-grid .filter-btn:hover { - background: var(--blue-50); - border-color: var(--blue-500); - color: var(--blue-600); -} - -.filters-grid .filter-btn.active { - background: var(--blue-500); - border-color: var(--blue-500); - color: white; -} - -/* Matrix 后处理滤镜模态框:滑块样式与程序动画 cc-section 完全一致 */ -#modal-filter-config input[type="range"] { - flex: 1; - height: 6px; - border-radius: var(--radius-full); - background: var(--bg-muted); - -webkit-appearance: none; - appearance: none; -} -#modal-filter-config input[type="range"]::-webkit-slider-runnable-track { - width: 100%; - height: 6px; - background: var(--bg-muted); - border-radius: var(--radius-full); - cursor: pointer; -} -#modal-filter-config input[type="range"]::-webkit-slider-thumb { - appearance: none; - -webkit-appearance: none; - width: 16px; - height: 16px; - margin-top: -5px; /* 垂直居中:16px 手柄对齐 6px 轨道 */ - border-radius: 50%; - background: var(--violet-500); - box-shadow: var(--shadow); - cursor: pointer; - border: 2.5px solid var(--bg-card); -} - -/* 特效/滤镜配置区 */ -.effect-config-modal, -.filter-config-modal { - display: flex; - flex-direction: column; - gap: 10px; - padding: 12px; - background: var(--bg-muted); - border-radius: var(--radius); - margin-bottom: 12px; -} - -.effect-config-modal .effect-name, -.filter-config-modal .filter-name { - font-weight: 600; - font-size: 0.8rem; - padding: 4px 10px; - background: var(--blue-500); - color: white; - border-radius: var(--radius-full); - display: inline-block; - align-self: flex-start; -} - -/* 后处理滤镜滑块:与程序动画 cc-section 一致,灰色轨道线用 var(--bg-muted) */ -.filter-config-modal .config-row input[type="range"] { - flex: 1; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: var(--bg-muted); - border-radius: var(--radius-full); -} -.filter-config-modal .config-row input[type="range"]::-webkit-slider-runnable-track { - width: 100%; - height: 6px; - background: var(--bg-muted); - border-radius: var(--radius-full); - cursor: pointer; -} -.filter-config-modal .config-row input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 16px; - height: 16px; - margin-top: -5px; - border-radius: 50%; - background: var(--violet-500); - box-shadow: var(--shadow); - cursor: pointer; - border: 2.5px solid var(--bg-card); -} - -.config-row { - display: flex; - align-items: center; - gap: 10px; -} - -.config-row label { - font-size: 0.85rem; - color: var(--text-secondary); -} - -.config-row input[type="range"] { - flex: 1; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: var(--bg-muted); - border-radius: var(--radius-full); -} - -.config-row input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 16px; - height: 16px; - background: var(--blue-500); - border-radius: 50%; - cursor: pointer; - border: 2.5px solid var(--bg-card); - box-shadow: var(--shadow); -} - -.config-row input[type="color"] { - width: 36px; - height: 36px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - padding: 2px; - box-shadow: var(--shadow-sm); -} -.config-row input[type="color"]::-webkit-color-swatch-wrapper { - padding: 0; - border-radius: calc(var(--radius-sm) - 2px); - overflow: hidden; -} -.config-row input[type="color"]::-webkit-color-swatch { - border: none; - border-radius: calc(var(--radius-sm) - 2px); -} - -.config-row input[type="text"], -.config-row input[type="number"] { - padding: 9px 12px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 0.875rem; - font-family: inherit; - box-shadow: var(--shadow-sm); -} - -.config-row input[type="text"]:focus, -.config-row input[type="number"]:focus { - outline: none; - border-color: var(--blue-500); - box-shadow: 0 0 0 3px var(--blue-50); -} - -.config-row input.input-flex { - flex: 1; -} - -.config-row select { - padding: 9px 12px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 0.875rem; - font-family: inherit; - box-shadow: var(--shadow-sm); -} - -/* Matrix 文本显示:字体下拉足够大、易点击 */ -#modal-text-font, -.led-modal .cc-section select#modal-text-font { - min-width: 160px; - min-height: 38px; - padding: 10px 12px; - font-size: 0.95rem; - cursor: pointer; - appearance: auto; - -webkit-appearance: menulist; -} - -.config-actions { - display: flex; - gap: 10px; - justify-content: flex-end; - margin-top: 12px; -} - -/* Color Correction Modal Cards */ -.cc-enable-row { - background: var(--bg-card); - padding: 16px 20px; - border-radius: var(--radius-lg); - margin-bottom: 16px; - box-shadow: var(--shadow-sm); - border: 1px solid var(--border); -} - -.cc-enable-row label { - display: flex; - align-items: center; - gap: 12px; - font-weight: 600; - cursor: pointer; - margin: 0; - width: 100%; - font-size: 0.875rem; -} - -.cc-enable-row input[type="checkbox"] { - width: 20px; - height: 20px; - accent-color: var(--blue-500); -} - -.cc-section { - background: var(--bg-card); - padding: 20px; - border-radius: var(--radius-lg); - margin-bottom: 16px; - box-shadow: var(--shadow-sm); - border: 1px solid var(--border); -} - -.cc-section h4 { - font-size: 0.875rem; - font-weight: 600; - margin: 0 0 8px 0; - color: var(--text-primary); - display: flex; - align-items: center; - gap: 7px; -} - -.cc-section h4 i { - color: var(--blue-500); -} - -.cc-help-text { - font-size: 0.8rem; - color: var(--text-muted); - margin: 0 0 20px 0; - line-height: 1.5; -} - -.cc-section .config-row { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 16px; -} - -.cc-section .config-row:last-child { - margin-bottom: 0; -} - -.cc-section input[type="range"] { - flex: 1; - height: 6px; - border-radius: var(--radius-full); - background: var(--bg-muted); - -webkit-appearance: none; - appearance: none; -} - -.cc-section input[type="range"]::-webkit-slider-thumb { - appearance: none; - -webkit-appearance: none; - width: 16px; - height: 16px; - margin-top: -5px; /* 垂直居中:16px 手柄对齐 6px 轨道 */ - border-radius: 50%; - background: var(--violet-500); - box-shadow: var(--shadow); - cursor: pointer; - border: 2.5px solid var(--bg-card); -} - -.cc-section span { - min-width: 36px; - text-align: right; - font-weight: 600; - color: var(--text-muted); - font-family: var(--font-mono); - font-size: 0.8125rem; - font-variant-numeric: tabular-nums; -} - -.cc-actions { - margin-top: 20px; - padding: 0; - background: transparent; - display: flex; - justify-content: flex-end; - gap: 12px; -} - -.cc-actions .btn { - padding: 7px 16px; - font-size: 0.8rem; - border-radius: var(--radius); -} - -/* 文件选择器模态框 - 需要比其他模态框更高的 z-index,因为它会从其他模态框内打开 */ -#file-picker-modal { - z-index: 1100; -} - -.file-picker-modal { - min-width: 480px; - max-width: 600px; -} - -.file-picker-header { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 16px; - padding-bottom: 12px; - border-bottom: 1px solid var(--border); -} - -.file-picker-header h3 { - margin: 0; - flex: 1; -} - -.file-picker-path { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 12px; - padding: 8px 12px; - background: var(--bg-muted); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - font-size: 0.8rem; -} - -.file-picker-path .path-text { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.file-picker-list { - border: 1px solid var(--border); - border-radius: var(--radius); - max-height: 300px; - overflow-y: auto; - margin-bottom: 12px; -} - -.file-picker-item { - display: flex; - align-items: center; - gap: 8px; - padding: 10px 12px; - cursor: pointer; - transition: all var(--t-fast); - border-bottom: 1px solid var(--border); -} - -.file-picker-item:last-child { - border-bottom: none; -} - -.file-picker-item:hover { - background: var(--bg-muted); -} - -.file-picker-item.selected { - background: var(--blue-50); - color: var(--blue-600); - border-color: var(--blue-100); -} - -.file-picker-item .icon { - font-size: 1.2rem; - width: 24px; - text-align: center; -} - -.file-picker-item .name { - flex: 1; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.file-picker-item .size { - font-size: 0.85rem; - color: var(--text-secondary); -} - -.file-picker-item.selected .size { - color: var(--blue-500); -} - -.file-picker-selected { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 16px; - font-size: 0.9rem; -} - -.file-picker-selected .label { - color: var(--text-secondary); -} - -.file-picker-selected .value { - font-weight: 500; -} - -.file-picker-actions { - display: flex; - justify-content: flex-end; - gap: 8px; -} - -.file-picker-list .loading, -.file-picker-list .empty-state, -.file-picker-list .error { - padding: 40px 20px; - text-align: center; - color: var(--text-secondary); -} - -.file-picker-list .error { - color: var(--rose-500); -} - -.modal-content h2 { - margin-bottom: 20px; -} - -/* Forms */ -.form-group { - margin-bottom: 16px; -} - -.form-group label { - display: block; - margin-bottom: 6px; - font-weight: 500; - font-size: 0.875rem; -} - -.form-group input:not([type="checkbox"]):not([type="radio"]), -.form-group select, -.form-group textarea { - display: block; - width: 100%; - padding: 9px 12px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - font-size: 0.875rem; - font-family: inherit; - background: var(--bg-card); - color: var(--text-primary); - box-shadow: var(--shadow-sm); -} - -.form-group small { - display: block; - margin-top: 4px; -} - -.form-group input[type="checkbox"] { - width: auto; - min-width: 18px; - height: 18px; - margin: 0 8px 0 0; - vertical-align: middle; - accent-color: var(--blue-500); -} - -.form-group-checkbox-label { - display: inline-flex; - align-items: center; - gap: 6px; - font-weight: 500; - cursor: pointer; -} -.form-group-checkbox-label input[type="checkbox"] { - flex-shrink: 0; -} - -.form-group-hint { - display: flex; - align-items: flex-start; - gap: 6px; - margin-top: 4px; - font-size: 0.8em; - color: var(--text-muted); -} -.form-group-hint i { - flex-shrink: 0; - margin-top: 2px; -} -.form-group-hint span { - flex: 1; - min-width: 0; -} -.form-group-hint-warning { - color: var(--amber-600); -} - -.form-group input:focus, -.form-group select:focus, -.form-group textarea:focus { - outline: none; - border-color: var(--blue-500); - box-shadow: 0 0 0 3px var(--blue-50); -} - -/* Base .input / .form-control class — consistent sizing for inputs outside .form-group,圆角与卡片匹配 */ -.input, -.form-control { - width: 100%; - padding: 9px 12px; - border: 1px solid var(--border); - border-radius: var(--radius); - font-size: 0.875rem; - font-family: inherit; - background: var(--bg-card); - color: var(--text-primary); - box-shadow: var(--shadow-sm); - box-sizing: border-box; -} - -.input:focus, -.form-control:focus { - outline: none; - border-color: var(--blue-500); - box-shadow: 0 0 0 3px var(--blue-50); -} - -.form-actions { - display: flex; - gap: 12px; - justify-content: flex-end; - margin-top: 20px; -} - -/* Status indicators (Tremor pill style) */ -.status { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 3px 10px; - border-radius: var(--radius-full); - font-size: 0.7rem; - font-weight: 600; -} - -.status-ok { - background: var(--emerald-50); - color: var(--emerald-600); -} - -.status-error { - background: var(--rose-50); - color: var(--rose-600); -} - -.status-warning { - background: var(--amber-50); - color: var(--amber-600); -} - -/* Responsive */ -@media (max-width: 768px) { - .nav { - display: none; - } - - .cards { - grid-template-columns: 1fr; - } -} - -/* ============================================================ */ -/* 新增样式 */ -/* ============================================================ */ - -/* WebSocket 状态指示器(位于用户菜单内、root 图标左侧,与 user-menu 的 gap 保持间隔) */ -.ws-status { - width: 10px; - height: 10px; - border-radius: 50%; - background: var(--rose-500); - flex-shrink: 0; - transition: background 0.3s; - box-shadow: 0 0 0 3px var(--rose-50); -} - -.ws-status.connected { - background: var(--emerald-500); - box-shadow: 0 0 0 3px var(--emerald-50); -} - -/* Header separator line */ -.header-separator { - width: 1px; - height: 24px; - background-color: var(--border); - margin: 0 16px; -} - -/* 语言切换按钮 */ -.lang-switch { - margin-left: 16px; - margin-right: 0; - position: relative; -} - -.lang-btn { - display: flex; - align-items: center; - gap: 4px; - padding: 4px 10px; - font-size: 0.8rem; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - transition: all var(--t-fast); - font-family: inherit; - box-shadow: var(--shadow-sm); -} - -.lang-btn:hover { - background: var(--blue-50); - color: var(--blue-600); - border-color: var(--blue-500); -} - -/* Language button: no background, no border */ -.lang-btn.btn-service-style { - background: transparent !important; - color: var(--blue-500) !important; - border: none !important; - box-shadow: none !important; -} -.lang-btn.btn-service-style:hover { - background: var(--blue-50) !important; - color: var(--blue-600) !important; - border: none !important; -} - -#lang-icon { - font-size: 1rem; -} - -#lang-name { - font-weight: 500; -} - -.lang-arrow { - font-size: 0.65rem; - margin-left: 2px; - opacity: 0.7; -} - -/* 语言下拉菜单 */ -.lang-menu { - position: absolute; - top: 100%; - right: 0; - margin-top: 4px; - min-width: 160px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - box-shadow: var(--shadow-lg); - z-index: 1000; - overflow: hidden; -} - -.lang-menu.hidden { - display: none; -} - -.lang-menu-item { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - cursor: pointer; - transition: background 0.15s; -} - -.lang-menu-item:hover { - background: var(--bg-muted); -} - -.lang-menu-item.active { - background: var(--blue-50); - color: var(--blue-600); -} - -.lang-menu-flag { - font-size: 1.2rem; -} - -.lang-menu-name { - flex: 1; - font-size: 0.9rem; -} - -.lang-menu-check { - color: var(--blue-500); - font-weight: bold; -} - -/* 加载动画 */ -.loading { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 60px; - gap: 20px; -} - -.loading .loading-icon { - font-size: 48px; - color: var(--blue-500); - animation: spin 1s linear infinite; -} - -.loading p { - margin: 0; -} - -.spinner { - width: 40px; - height: 40px; - border: 4px solid var(--border); - border-top-color: var(--blue-500); - border-radius: 50%; - animation: spin 1s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Toast notifications */ -.toast { - position: fixed; - bottom: 80px; - left: 50%; - transform: translateX(-50%) translateY(100px); - background: var(--text-primary); - color: white; - padding: 10px 20px; - border-radius: var(--radius); - opacity: 0; - transition: all 0.3s; - z-index: 9999; - font-size: 0.8rem; - font-weight: 500; - box-shadow: var(--shadow-lg); -} - -.toast.show { - opacity: 1; - transform: translateX(-50%) translateY(0); -} - -.toast-success { - background: var(--bg-card); - color: var(--emerald-600); - border: 1px solid var(--border); - box-shadow: var(--shadow-lg); -} - -.toast-error { - background: var(--bg-card); - color: var(--rose-600); - border: 1px solid var(--border); - box-shadow: var(--shadow-lg); -} - -.toast-warning { - background: var(--bg-card); - color: var(--amber-600); - border: 1px solid var(--border); - box-shadow: var(--shadow-lg); -} - -.toast-info { - background: var(--bg-card); - color: var(--blue-600); - border: 1px solid var(--border); - box-shadow: var(--shadow-lg); -} - -/* Data table (Tremor style) - 圆角与外层 section 卡片一致,用 separate + border-spacing 以支持 border-radius */ -.data-table { - width: 100%; - border-collapse: separate; - border-spacing: 0; -} - -.data-table th, -.data-table td { - padding: 11px 16px; - text-align: left; - border-bottom: 1px solid var(--border); -} - -.data-table th { - background: var(--bg-muted); - font-size: 0.75rem; - font-weight: 500; - color: var(--text-muted); - text-transform: none; -} - -/* 表头首行圆角(与 section 卡片匹配) */ -.data-table thead tr:first-child th:first-child { - border-top-left-radius: var(--radius); -} -.data-table thead tr:first-child th:last-child { - border-top-right-radius: var(--radius); -} - -/* 末行单元格圆角 */ -.data-table tbody tr:last-child td:first-child { - border-bottom-left-radius: var(--radius); -} -.data-table tbody tr:last-child td:last-child { - border-bottom-right-radius: var(--radius); -} - -.data-table td { - font-size: 0.8125rem; -} - -.data-table tbody tr:hover td { - background: var(--bg-page); -} - -.data-table tbody tr:last-child td { - border-bottom: none; -} - -.data-table code { - background: var(--bg-muted); - padding: 2px 6px; - border-radius: var(--radius); - font-size: 0.8em; - font-family: var(--font-mono); -} - -/* Status badges (Tremor pill) */ -.status-badge { - display: inline-flex; - align-items: center; - gap: 5px; - padding: 3px 10px; - border-radius: var(--radius-full); - font-size: 0.7rem; - font-weight: 600; -} - -.status-badge.status-ok { - background: var(--emerald-50); - color: var(--emerald-600); -} - -.status-badge.status-error { - background: var(--rose-50); - color: var(--rose-600); -} - -.status-badge.status-warn { - background: var(--amber-50); - color: var(--amber-600); -} - -/* 小按钮 */ -.btn-small { - padding: 4px 12px; - font-size: 0.85rem; -} - -.time-sync-btn { - border: 1px solid transparent; -} -.time-sync-btn:active { - background: var(--blue-50); - color: var(--blue-600); -} - -.btn-warning { - background: var(--amber-50); - border-color: transparent; - color: var(--amber-600); - box-shadow: none; -} -.btn-warning:hover { - background: var(--amber-100); -} - -/* 按钮组 */ -.button-group { - display: flex; - gap: 10px; - margin-top: 16px; - flex-wrap: wrap; -} - -/* Section */ -.section { - background: var(--bg-card); - border-radius: var(--radius-lg); - padding: 24px; - margin-top: 16px; - box-shadow: var(--shadow); - border: 1px solid var(--border); - transition: box-shadow var(--t-normal); -} - -.section:hover { - box-shadow: var(--shadow-md); -} - -.section h2 { - margin-bottom: 16px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border); - color: var(--text-primary); - font-size: 0.9375rem; - font-weight: 600; - display: flex; - align-items: center; - gap: 8px; -} - -.section h2 i { - color: var(--blue-500); - font-size: 1.1rem; -} - -/* 信息网格 */ -.info-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; -} - -.info-item { - display: flex; - flex-direction: column; -} - -.info-item label { - font-size: 0.75rem; - color: var(--text-secondary); - margin-bottom: 4px; - font-weight: 500; -} - -.info-item span { - font-weight: 600; - font-size: 0.875rem; - font-variant-numeric: tabular-nums; -} - -/* 内存条 */ -.memory-bars { - display: flex; - flex-direction: column; - gap: 16px; -} - -.memory-item { - display: flex; - align-items: center; - gap: 16px; -} - -.memory-item label { - min-width: 100px; - font-weight: 500; -} - -.memory-item .progress-bar { - flex: 1; - margin: 0; -} - -.memory-item span { - min-width: 180px; - text-align: right; - font-size: 0.8rem; - color: var(--text-secondary); - font-variant-numeric: tabular-nums; -} - -/* 表单行 */ -.form-row { - display: flex; - gap: 16px; - flex-wrap: wrap; - align-items: flex-end; -} - -.form-row .form-group { - flex: 1; - min-width: 150px; -} - -/* ========================================================================= - LED 控制页面 - 新版设备卡片布局 - ========================================================================= */ - -.page-led { - max-width: 1400px; - margin: 0 auto; -} - -.led-page-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; /* 与 Device Panel 等 section-header 一致 */ - flex-wrap: wrap; - gap: 12px; -} -/* 系统页 LED 区块:上下间距相等,使标题+按钮在灰色顶线与蓝色内容线间垂直居中 */ -.section .led-page-header { - margin-top: -12px; /* 24px padding → 12px,与 margin-bottom 一致 */ -} -/* 独立 LED 页:main 有 20px padding,同样实现上下对称 */ -.page-led .led-page-header { - margin-top: -8px; /* 20px → 12px,与 margin-bottom 一致 */ -} - -.led-page-header h1 { - margin: 0; - font-size: 1.5rem; -} - -.led-quick-actions { - display: flex; - gap: 8px; - position: relative; - z-index: 1; -} -.led-refresh-btn { - min-width: 32px; - min-height: 32px; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid transparent; -} -.led-refresh-btn:active { - background: var(--blue-50); - color: var(--blue-600); -} - -/* LED 设备网格 */ -.led-devices-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); - gap: 20px; -} - -/* LED device card (Tremor style) */ -.led-device-card { - background: var(--bg-card); - border-radius: var(--radius-lg); - border: 1px solid var(--border); - overflow: hidden; - transition: all var(--t-normal); - box-shadow: var(--shadow); -} - -.led-device-card:hover { - box-shadow: var(--shadow-md); -} - -.led-device-card.is-on { - border-color: var(--blue-500); - box-shadow: 0 0 0 1px var(--blue-50), var(--shadow-md); -} - -/* 卡片头部 */ -.led-card-header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 20px; - background: var(--bg-subtle); - border-bottom: 1px solid var(--border); -} - -.led-device-icon { - font-size: 1.5rem; - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-card); - border-radius: var(--radius); - border: 1px solid var(--border); - color: var(--blue-500); -} - -.led-device-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; -} - -.led-device-name { - font-size: 0.9375rem; - font-weight: 600; - text-transform: capitalize; -} - -.led-device-desc { - font-size: 0.75rem; - color: var(--text-secondary); -} - -.led-device-status { - font-size: 0.7rem; - padding: 3px 10px; - border-radius: var(--radius-full); - font-weight: 600; - white-space: nowrap; -} - -.led-device-status.off { - background: var(--bg-muted); - color: var(--text-muted); -} - -.led-device-status.on { - background: var(--emerald-50); - color: var(--emerald-600); -} - -.led-device-status.effect { - background: var(--blue-50); - color: var(--blue-600); -} - -/* 卡片控制区域 */ -.led-card-controls { - padding: 16px 20px; - display: flex; - flex-direction: column; - gap: 14px; - background: var(--bg-card); -} - -/* 亮度控制行 */ -.led-brightness-row { - display: flex; - align-items: center; - gap: 12px; - background: var(--bg-page); - padding: 10px 14px; - border-radius: var(--radius); - border: 1px solid var(--border); -} - -.led-brightness-row .brightness-label { - font-size: 1rem; -} - -.led-brightness-slider { - flex: 1; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: var(--bg-muted); - border-radius: var(--radius-full); - outline: none; -} - -.led-brightness-slider::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: 18px; - height: 18px; - background: var(--blue-500); - border-radius: 50%; - cursor: pointer; - border: 2.5px solid var(--bg-card); - box-shadow: var(--shadow); -} - -.led-brightness-row .brightness-value { - min-width: 32px; - text-align: center; - font-size: 0.85rem; - font-weight: 500; - color: var(--text-secondary); -} - -/* 颜色控制行 - Modern Redesign (缩小约 1/3) */ -.led-color-row { - display: flex; - align-items: center; - gap: 10px; - padding: 5px 0; -} - -/* Modern Picker */ -.modern-picker-wrapper { - position: relative; - width: 32px; - height: 32px; - flex-shrink: 0; -} - -.modern-picker-input { - width: 100%; - height: 100%; - opacity: 0; - position: absolute; - top: 0; - left: 0; - cursor: pointer; - z-index: 2; -} - -.modern-picker-visual { - width: 100%; - height: 100%; - border-radius: 50%; - background: conic-gradient(from 180deg at 50% 50%, #FF0000 0deg, #FFFF00 60deg, #00FF00 120deg, #00FFFF 180deg, #0000FF 240deg, #FF00FF 300deg, #FF0000 360deg); - border: 2.5px solid var(--border); - position: relative; - transition: all var(--t-fast); -} - -.modern-picker-wrapper:hover .modern-picker-visual { - transform: scale(1.05); - border-color: var(--blue-500); -} - -.modern-picker-icon { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - color: white; - font-size: 0.85rem; - text-shadow: 0 1px 2px rgba(0,0,0,0.3); - pointer-events: none; -} - -/* Modern Presets */ -.modern-presets-container { - flex: 1; - overflow-x: auto; - padding: 4px; - scrollbar-width: none; - -ms-overflow-style: none; - display: flex; - align-items: center; -} -.modern-presets-container::-webkit-scrollbar { display: none; } - -.modern-presets-list { - display: flex; - gap: 8px; - align-items: center; -} - -.modern-color-dot { - width: 24px; - height: 24px; - border-radius: 50%; - border: 2px solid var(--border); - cursor: pointer; - transition: all var(--t-fast); - position: relative; - padding: 0; -} - -.modern-color-dot:hover { - transform: scale(1.15); - border-color: var(--text-muted); - box-shadow: var(--shadow-sm); -} - -.modern-color-dot:active { - transform: scale(0.95); -} - -/* White color needs a border */ -.modern-color-dot[style*="#ffffff"] { - border: 2px solid var(--border); -} - -/* 特效控制行 */ -.led-effects-row { - display: flex; - align-items: center; - gap: 8px; -} - -.led-quick-effects { - display: flex; - gap: 6px; - flex: 1; - flex-wrap: wrap; -} - -.led-quick-effect { - width: 36px; - height: 36px; - border: 1px solid var(--border); - border-radius: 50%; - background: var(--bg-card); - color: var(--text-muted); - cursor: pointer; - font-size: 0.95rem; - display: flex; - align-items: center; - justify-content: center; - transition: all var(--t-fast); - box-shadow: var(--shadow-sm); -} - -.led-quick-effect:hover { - border-color: var(--blue-500); - color: var(--blue-500); - background: var(--blue-50); -} - -.led-quick-effect.active { - background: var(--blue-500); - border-color: var(--blue-500); - color: white; -} - -.led-quick-effect.more { - font-size: 0.75rem; - font-weight: 600; -} - -.led-matrix-btns { - display: flex; - gap: 6px; - margin-left: auto; -} - -.led-stop-btn { - display: flex; - align-items: center; - gap: 4px; - padding: 5px 12px; - border: none; - border-radius: var(--radius-sm); - background: var(--rose-50); - color: var(--rose-600); - cursor: pointer; - font-size: 0.7rem; - font-weight: 600; - white-space: nowrap; - transition: all var(--t-fast); - font-family: inherit; -} - -.led-stop-btn:hover { - background: var(--rose-100); -} - -/* 卡片底部操作栏 */ -.led-card-footer { - display: flex; - gap: 8px; - padding: 12px 20px; - background: var(--bg-subtle); - border-top: 1px solid var(--border); - flex-wrap: wrap; -} - -.led-power-btn { - display: flex; - align-items: center; - justify-content: center; - padding: 0; - border: none; - background: transparent; - cursor: pointer; - transition: all var(--t-fast); - font-size: 1.8rem; - color: var(--text-faint); -} - -.led-power-btn:hover { - opacity: 0.8; -} - -.led-power-btn.on { - color: var(--amber-500); -} - -.led-power-btn .power-icon { - font-size: 1.8rem; -} - -.led-func-btn { - width: 36px; - height: 36px; - border: 1px solid var(--border); - border-radius: 50%; - background: var(--bg-card); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all var(--t-fast); - color: var(--text-muted); - box-shadow: var(--shadow-sm); -} - -.led-func-btn:hover { - border-color: var(--blue-500); - color: var(--blue-500); - background: var(--blue-50); -} - -.led-func-btn .func-icon { - font-size: 1.1rem; -} - -.led-save-btn { - margin-left: auto; - border: none; - background: transparent; - color: var(--text-muted); - cursor: pointer; - font-size: 1.5rem; - transition: all var(--t-fast); - display: flex; - align-items: center; - justify-content: center; -} - -.led-save-btn:hover { - color: var(--emerald-500); -} - -.led-save-btn:hover { - opacity: 0.8; -} - -/* LED 空状态 */ -.led-empty-state { - grid-column: 1 / -1; - text-align: center; - padding: 60px 20px; - background: var(--bg-card); - border-radius: var(--radius-lg); - border: 2px dashed var(--border); -} - -.led-empty-state .empty-icon { - font-size: 3rem; - margin-bottom: 16px; -} - -.led-empty-state h3 { - margin: 0 0 12px; -} - -.led-empty-state p { - color: var(--text-secondary); - margin: 0 0 8px; -} - -.led-empty-state ul { - text-align: left; - display: inline-block; - margin: 12px 0; - color: var(--text-secondary); -} - -/* 加载内联状态 */ -.loading-inline { - grid-column: 1 / -1; - text-align: center; - padding: 40px; - color: var(--text-secondary); -} - -/* ========================================================================= - LED 控制页面 - 旧版紧凑型设备面板(保留兼容) - ========================================================================= */ - -.led-panels { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); - gap: 16px; -} - -.led-panel { - background: var(--bg-card); - border-radius: var(--radius-lg); - overflow: hidden; - border: 1px solid var(--border); - display: flex; - flex-direction: column; - box-shadow: var(--shadow); -} - -/* Compact mode panel header */ -.led-panel.compact .panel-header { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 10px; - padding: 12px 16px; - background: var(--bg-subtle); - border-bottom: 1px solid var(--border); -} - -.led-panel.compact .device-icon { - font-size: 1.5rem; -} - -.led-panel.compact .device-info { - display: flex; - flex-direction: column; -} - -.led-panel.compact .device-info strong { - font-size: 1rem; - text-transform: capitalize; -} - -.led-panel.compact .device-meta { - font-size: 0.75rem; - color: var(--text-secondary); -} - -.btn-action { - padding: 7px 14px; - font-size: 0.8rem; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: all var(--t-fast); - color: var(--text-primary); - white-space: nowrap; - font-family: inherit; - font-weight: 500; - box-shadow: var(--shadow-sm); -} - -.btn-action:hover { - background: var(--bg-muted); - border-color: var(--border-hover); -} - -/* Toggle button */ -.btn-action.btn-toggle { - background: var(--bg-card); - border-color: var(--border); -} - -.btn-action.btn-toggle.on { - background: var(--emerald-50); - border-color: transparent; - color: var(--emerald-600); -} - -.btn-action.btn-toggle:hover { - border-color: var(--border-hover); -} - -.btn-action.btn-save { - background: var(--emerald-50); - border-color: transparent; - color: var(--emerald-600); -} - -.btn-action.btn-save:hover { - background: var(--emerald-100); -} - -/* 颜色预设网格(模态框内) */ -.preset-colors-grid { - display: grid; - grid-template-columns: repeat(6, 1fr); - gap: 8px; - margin-top: 8px; -} - -.color-preset { - width: 100%; - aspect-ratio: 1; - border: 2px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: all var(--t-fast); - min-height: 36px; - padding: 0; -} - -.color-preset:hover { - transform: scale(1.1); - border-color: var(--text-muted); - box-shadow: var(--shadow-sm); -} - -/* 紧凑亮度控制 */ -.brightness-compact { - display: flex; - align-items: center; - gap: 6px; - background: var(--bg-color); - padding: 4px 10px; - border-radius: 20px; - border: 1px solid var(--border-color); -} - -.brightness-compact .brightness-icon { - font-size: 0.9rem; -} - -.brightness-compact input[type="range"] { - width: 80px; - height: 4px; - -webkit-appearance: none; - appearance: none; - background: var(--border-color); - border-radius: 2px; -} - -.brightness-compact input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 14px; - height: 14px; - background: var(--primary-color); - border-radius: 50%; - cursor: pointer; -} - -.brightness-compact .brightness-val { - font-size: 0.75rem; - color: var(--text-secondary); - min-width: 24px; - text-align: right; -} - -/* =========== 旧版 LED 面板样式(保留兼容) =========== */ - -.led-panel .panel-header { - display: flex; - align-items: center; - gap: 12px; - padding: 16px 20px; - background: var(--bg-color); - border-bottom: 1px solid var(--border-color); -} - -.led-panel .panel-header .device-icon { - font-size: 1.8rem; -} - -.led-panel .panel-header .device-title { - flex: 1; -} - -.led-panel .panel-header .device-title h2 { - margin: 0; - font-size: 1.2rem; -} - -.led-panel .panel-header .device-title .device-desc { - font-size: 0.8rem; - color: var(--text-secondary); -} - -.led-panel .panel-header .led-count { - font-size: 0.9rem; - color: var(--text-secondary); - background: var(--card-bg); - padding: 4px 12px; - border-radius: 12px; -} - -/* 标题栏保存按钮 */ -.btn-header-save { - padding: 6px 10px; - font-size: 1rem; - background: transparent; - border: 1px solid var(--border-color); - border-radius: 8px; - cursor: pointer; - transition: all 0.2s ease; - margin-left: 8px; -} - -.btn-header-save:hover { - background: var(--primary-color); - border-color: var(--primary-color); - transform: scale(1.1); -} - -.led-panel .panel-body { - padding: 20px; -} - -/* 两栏布局 */ -.led-panel .panel-body.two-columns { - display: flex; - gap: 20px; -} - -.control-column { - flex: 1; - background: var(--bg-color); - padding: 16px; - border-radius: 10px; -} - -.control-column .column-title { - display: block; - font-weight: 600; - margin-bottom: 14px; - padding-bottom: 10px; - border-bottom: 1px solid var(--border-color); - color: var(--text-color); - font-size: 0.9rem; -} - -.control-column .effect-count { - font-weight: normal; - color: var(--text-secondary); - font-size: 0.8rem; -} - -/* 基础控制列 */ -.basic-controls .control-row { - margin-bottom: 14px; -} - -.basic-controls .control-row label { - display: block; - font-size: 0.8rem; - color: var(--text-secondary); - margin-bottom: 6px; -} - -.basic-controls .control-row input[type="range"] { - width: 100%; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: var(--border-color); - border-radius: 3px; - cursor: pointer; -} - -.basic-controls .control-row input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 16px; - height: 16px; - background: var(--primary-color); - border-radius: 50%; - cursor: pointer; -} - -.basic-controls .color-control { - display: flex; - align-items: center; - gap: 8px; -} - -.basic-controls .color-control input[type="color"] { - width: 50px; - height: 32px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - padding: 2px; - box-shadow: var(--shadow-sm); -} -.basic-controls .color-control input[type="color"]::-webkit-color-swatch-wrapper { - padding: 0; - border-radius: calc(var(--radius-sm) - 2px); - overflow: hidden; -} -.basic-controls .color-control input[type="color"]::-webkit-color-swatch { - border: none; - border-radius: calc(var(--radius-sm) - 2px); -} - -/* 预设颜色 */ -.preset-colors { - display: flex; - gap: 6px; - flex-wrap: wrap; - margin-top: 10px; -} - -.color-preset { - width: 24px; - height: 24px; - border: 2px solid var(--border-color); - border-radius: 4px; - cursor: pointer; - transition: transform 0.15s ease; -} - -.color-preset:hover { - transform: scale(1.15); - border-color: var(--primary-color); -} - -/* 程序动画列 */ -.effects-column .effects-grid { - display: flex; - gap: 6px; - flex-wrap: wrap; - margin-bottom: 10px; -} - -.effect-btn { - padding: 5px 10px; - font-size: 0.8rem; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; -} - -.effect-btn:hover { - background: var(--primary-color); - border-color: var(--primary-color); - color: white; -} - -.effect-btn.active { - background: var(--primary-color); - border-color: var(--primary-color); - color: white; -} - -/* 特效数量提示 */ -.effect-count { - font-weight: normal; - color: var(--text-secondary); - font-size: 0.75rem; -} - -/* 设备布局标签 */ -.device-layout { - background: var(--primary-color); - color: white; - padding: 2px 8px; - border-radius: 10px; - font-size: 0.7rem; - text-transform: uppercase; - margin-right: 8px; -} - -/* 特效配置面板 */ -.effect-controls { - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--border-color); -} - -.effect-config { - background: var(--bg-color); - padding: 12px; - border-radius: 8px; -} - -.effect-config .current-effect { - display: block; - font-size: 1rem; - font-weight: 600; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border-color); -} - -.effect-config .config-row { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 10px; -} - -.effect-config .config-row > label { - min-width: 40px; - font-size: 0.85rem; - color: var(--text-secondary); -} - -.effect-config .config-row input[type="range"] { - flex: 1; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: var(--border-color); - border-radius: 3px; -} - -.effect-config .config-row input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 16px; - height: 16px; - background: var(--primary-color); - border-radius: 50%; - cursor: pointer; -} - -.effect-config .config-row input[type="color"] { - width: 36px; - height: 28px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - padding: 2px; - box-shadow: var(--shadow-sm); -} -.effect-config .config-row input[type="color"]::-webkit-color-swatch-wrapper, -.effect-config .config-row input[type="color"]::-webkit-color-swatch { - border-radius: calc(var(--radius-sm) - 2px); -} -.effect-config .config-row input[type="color"]::-webkit-color-swatch { - border: none; -} - -.effect-config .checkbox-label { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.85rem; - cursor: pointer; -} - -.effect-config .checkbox-label input[type="checkbox"] { - width: 16px; - height: 16px; - cursor: pointer; -} - -.effect-config .config-actions { - display: flex; - gap: 8px; - margin-top: 12px; - padding-top: 10px; - border-top: 1px solid var(--border-color); -} - -.effect-config .config-actions .btn { - flex: 1; -} - -/* =========== Matrix 专属功能区 =========== */ -.matrix-extras { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 16px; - padding: 16px; - background: var(--bg-color); - border-top: 1px solid var(--border-color); -} - -.matrix-section { - background: var(--card-bg); - border-radius: 10px; - padding: 14px; - border: 1px solid var(--border-color); -} - -.matrix-section .section-title { - display: block; - font-weight: 600; - font-size: 0.9rem; - margin-bottom: 12px; - padding-bottom: 8px; - border-bottom: 1px solid var(--border-color); - color: var(--text-color); -} - -.matrix-controls .control-row { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 10px; - flex-wrap: wrap; -} - -.matrix-controls .control-row:last-child { - margin-bottom: 0; -} - -.matrix-controls .input-full { - flex: 1; - min-width: 120px; - padding: 8px 10px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 6px; - color: var(--text-color); - font-size: 0.85rem; -} - -.matrix-controls .input-full:focus { - outline: none; - border-color: var(--primary-color); -} - -.matrix-controls select { - padding: 8px 10px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 6px; - color: var(--text-color); - font-size: 0.875rem; - cursor: pointer; -} - -.matrix-controls input[type="color"] { - width: 32px; - height: 28px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - padding: 2px; - box-shadow: var(--shadow-sm); -} -.matrix-controls input[type="color"]::-webkit-color-swatch-wrapper { - padding: 0; - border-radius: calc(var(--radius-sm) - 2px); - overflow: hidden; -} -.matrix-controls input[type="color"]::-webkit-color-swatch { - border: none; - border-radius: calc(var(--radius-sm) - 2px); -} - -.matrix-controls input[type="number"] { - width: 50px; - padding: 4px 6px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 4px; - color: var(--text-color); - font-size: 0.8rem; - text-align: center; -} - -.matrix-controls label { - font-size: 0.8rem; - color: var(--text-secondary); - display: flex; - align-items: center; - gap: 4px; -} - -.matrix-controls input[type="checkbox"] { - width: 16px; - height: 16px; - cursor: pointer; -} - -.filters-grid { - display: flex; - flex-wrap: wrap; - gap: 6px; -} - -.filter-btn { - padding: 5px 10px; - font-size: 0.75rem; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - transition: all var(--t-fast); -} - -.filter-btn:hover { - background: var(--blue-500); - border-color: var(--blue-500); - color: white; -} - -.filter-btn.active { - background: var(--blue-500); - border-color: var(--blue-500); - color: white; -} - -.filter-btn.selected { - background: var(--bg-card); - border-color: var(--blue-500); - box-shadow: 0 0 0 2px var(--blue-100); -} - -/* 滤镜分类 */ -.filter-category { - margin-bottom: 12px; -} - -.filter-category .filter-label { - display: block; - font-size: 0.75rem; - color: var(--text-secondary); - margin-bottom: 6px; - font-weight: 500; -} - -/* 滤镜参数区域 */ -.filter-params { - background: var(--bg-muted); - padding: 12px; - border-radius: var(--radius); - margin-top: 10px; -} - -.filter-param-row { - display: flex; - align-items: center; - gap: 10px; -} - -.filter-param-row label { - min-width: 40px; - font-size: 0.85rem; - color: var(--text-secondary); -} - -.filter-param-row input[type="range"] { - flex: 1; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: var(--border); - border-radius: var(--radius-sm); -} - -.filter-param-row input[type="range"]::-webkit-slider-thumb { - -webkit-appearance: none; - width: 16px; - height: 16px; - background: var(--blue-500); - border-radius: 50%; - cursor: pointer; -} - -.filter-param-row span { - min-width: 30px; - text-align: right; - font-size: 0.85rem; - color: var(--text-primary); -} - -/* 小按钮(全站统一) */ -.btn-sm, -.btn.btn-sm { - padding: 5px 10px; - font-size: 0.8rem; - box-shadow: none !important; /* 消除 btn-gray 与 btn-danger 的阴影差异 */ -} -/* btn-sm 内图标固定宽度 — 不同 RemixIcon 字符宽度不同, - 固定 1em 后所有图标占据相同空间,按钮宽度完全一致 */ -.btn.btn-sm > i { - display: inline-block; - width: 1em; - text-align: center; -} - -.btn-success { - background: var(--emerald-50); - color: var(--emerald-600); - border: 1px solid transparent; -} -.btn-success:hover { - background: var(--emerald-100); - border-color: transparent; -} - -/* 空状态提示 */ -.empty-state { - background: var(--card-bg); - padding: 30px; - border-radius: 12px; - text-align: center; -} - -.commands-list .empty-state { - min-height: 220px; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 8px; -} - -.commands-list .empty-state p { - margin: 0 0 6px 0; -} - -.empty-state p { - margin: 8px 0; -} - -.empty-state .hint { - color: var(--text-secondary); - font-size: 0.9rem; -} - -.empty-state ul { - text-align: left; - display: inline-block; - margin: 12px 0; - padding-left: 20px; -} - -.empty-state li { - margin: 4px 0; - font-size: 0.9rem; - color: var(--text-secondary); -} - -.empty-state code { - background: var(--bg-color); - padding: 2px 6px; - border-radius: 4px; - font-family: monospace; -} - -/* 辅助样式 */ -.page-led .loading, -.page-led .empty, -.page-led .error { - color: var(--text-secondary); - font-style: italic; - margin: 0; - padding: 8px 0; -} - -.page-led .error { - color: var(--error-color); -} - -/* 响应式 */ -@media (max-width: 600px) { - .led-panel .panel-body { - grid-template-columns: 1fr; - } - - .effects-row { - gap: 6px; - } - - .effect-btn { - padding: 5px 8px; - font-size: 0.8rem; - } -} - -/* 风扇卡片 */ -.fan-page-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 16px; - flex-wrap: wrap; - gap: 12px; -} - -.fan-page-header h2 { - margin: 0; - font-size: 1.2rem; -} - -.fan-quick-actions { - display: flex; - gap: 8px; -} - -.fans-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - gap: 16px; -} - -/* 风扇卡片 - 简洁版 */ -.fan-card { - background: var(--bg-card); - padding: 20px; - border-radius: 12px; - border: 2px solid var(--border-color); - transition: all 0.2s ease; -} - -.fan-card:hover { - border-color: var(--primary-color); -} - -.fan-card.is-off { - opacity: 0.6; -} - -/* 顶部 */ -.fan-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - margin-bottom: 16px; -} - -.fan-header-main { - display: flex; - align-items: center; - flex: 1; - flex-wrap: wrap; - gap: 8px 10px; - min-width: 0; -} - -.fan-header-actions { - display: flex; - align-items: center; - flex: 0 0 auto; - gap: 8px; -} - -.fan-title { - font-size: 1.1rem; - font-weight: 600; - line-height: 1.5; - white-space: nowrap; -} - -.fan-status-badge { - padding: 4px 10px; - border-radius: 20px; - font-size: 0.8rem; - font-weight: 500; -} - -/* 转速大数字 */ -.fan-speed-display { - text-align: center; - padding: 12px 0; - margin-bottom: 16px; -} - -.fan-speed-num { - font-size: 3.5rem; - font-weight: 700; - color: var(--primary-color); - line-height: 1; -} - -.fan-speed-percent { - font-size: 1.5rem; - color: var(--text-light); - margin-left: 2px; -} - -.fan-auto-info-btn { - width: 22px; - height: 22px; - padding: 0; - border: none; - border-radius: 50%; - background: transparent; - color: var(--text-muted); - display: inline-flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all var(--t-fast); -} - -.fan-auto-info-btn:hover { - color: var(--text-primary); - background: transparent; -} - -.fan-auto-info-btn i { - font-size: 0.95rem; - line-height: 1; -} - -.fan-rpm-small { - font-size: 0.85rem; - color: var(--text-light); - margin-top: 6px; -} - -.fan-auto-help-content { - max-width: 460px; -} - -.fan-auto-help-header { - position: relative; - display: flex; - align-items: center; - padding-right: 32px; -} - -.fan-auto-help-header h2 { - height: 28px; - min-width: 0; - line-height: 28px; -} - -.fan-auto-help-header .modal-close { - position: absolute; - top: 2px; - right: 0; - width: 24px; - height: 24px; - min-width: 24px; - margin-left: 0; - font-size: 1rem; - border-radius: var(--radius-sm); -} - -.fan-auto-help-header .modal-close i { - line-height: 1; -} - -.fan-auto-help-header .modal-close:hover { - background: var(--bg-tertiary); - color: var(--text-primary); -} - -.fan-auto-help-body { - color: var(--text-secondary); - font-size: 0.92rem; - line-height: 1.65; -} - -.fan-auto-help-body p { - margin: 0 0 12px; -} - -.fan-auto-help-body p:last-child { - margin-bottom: 0; -} - -.fan-auto-meta { - display: flex; - flex-wrap: wrap; - gap: 6px; - align-items: center; - margin: 0; - font-size: 0.75rem; - color: var(--text-light); - min-width: 0; -} - -.fan-auto-meta span { - padding: 3px 6px; - border-radius: 6px; - background: var(--bg-elevated); - white-space: nowrap; -} - -.fan-auto-meta.is-guard span, -.fan-auto-meta.is-stale span { - color: var(--error); -} - -/* 模式选择Tabs */ -.fan-mode-tabs { - display: flex; - gap: 4px; - margin-bottom: 16px; - background: var(--bg-elevated); - padding: 4px; - border-radius: 8px; -} - -.fan-mode-tab { - flex: 1; - padding: 10px 8px; - border: none; - background: transparent; - border-radius: 6px; - font-size: 0.85rem; - font-weight: 500; - cursor: pointer; - color: var(--text-light); - transition: all 0.2s; -} - -.fan-mode-tab:hover { - background: var(--bg-hover); - color: var(--text-color); -} - -.fan-mode-tab.active { - color: #fff; - font-weight: 600; -} - -.fan-mode-tab.active.off { - background: var(--text-secondary); -} - -.fan-mode-tab.active.manual { - background: var(--amber-50); - color: var(--amber-600); - border: 1px solid transparent; -} - -.fan-mode-tab.active.auto { - background: var(--emerald-50); - color: var(--emerald-600); - border: 1px solid transparent; -} - -.fan-mode-tab.active.curve { - background: var(--blue-50); - color: var(--blue-600); - border: 1px solid transparent; -} - -/* 滑块区域 */ -.fan-slider-wrap { - padding: 12px; - background: var(--bg-muted); - border-radius: var(--radius); - margin-bottom: 12px; -} - -.fan-slider-wrap.disabled { - opacity: 0.4; -} - -.fan-slider-label { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 10px; - font-size: 0.85rem; -} - -.fan-slider-value { - font-weight: 600; - color: var(--blue-500); -} - -.fan-slider { - width: 100%; - height: 8px; - border-radius: var(--radius-sm); - -webkit-appearance: none; - appearance: none; - background: var(--border); - outline: none; -} - -.fan-slider::-webkit-slider-thumb { - -webkit-appearance: none; - width: 24px; - height: 24px; - border-radius: 50%; - background: var(--blue-500); - cursor: pointer; - box-shadow: var(--shadow); - transition: transform var(--t-fast); -} - -.fan-slider::-webkit-slider-thumb:hover { - transform: scale(1.1); -} - -.fan-slider::-webkit-slider-thumb:active { - transform: scale(1.2); -} - -.fan-slider:disabled { - cursor: not-allowed; -} - -.fan-slider:disabled::-webkit-slider-thumb { - background: var(--text-muted); - cursor: not-allowed; -} - -/* 曲线编辑按钮 */ -.fan-curve-btn { - width: 100%; - padding: 10px; - background: var(--blue-50); - border: 1px solid transparent; - border-radius: var(--radius); - color: var(--blue-600); - font-size: 0.9rem; - cursor: pointer; - transition: all var(--t-normal); - margin-bottom: 8px; -} - -.fan-curve-btn:hover { - border-color: var(--blue-500); - color: var(--blue-600); - background: var(--blue-100); -} - -/* 温度信息 */ -.fan-temp-info { - text-align: center; - font-size: 0.85rem; - color: var(--text-secondary); - padding-top: 8px; - border-top: 1px solid var(--border); -} - -/* 风扇曲线编辑器 */ -.fan-curve-points { - display: flex; - flex-direction: column; - gap: 8px; - padding: 12px; - background: var(--bg-muted); - border-radius: var(--radius); - border: 1px solid var(--border); - max-height: 200px; - overflow-y: auto; -} - -.curve-point-row { - display: flex; - align-items: center; - gap: 10px; - padding: 8px; - background: var(--bg-card); - border-radius: var(--radius-sm); - transition: background var(--t-normal); -} - -.curve-point-row:hover { - background: var(--bg-muted); -} - -.curve-point-inputs { - display: flex; - align-items: center; - gap: 8px; - flex: 1; -} - -.curve-point-field { - display: flex; - align-items: center; - gap: 4px; -} - -.curve-point-field .field-icon { - font-size: 14px; -} - -.curve-point-field input { - width: 70px; - padding: 6px 8px; - text-align: center; -} - -.curve-point-field .field-unit { - font-size: 12px; - color: var(--text-secondary); - min-width: 20px; -} - -.curve-arrow { - color: var(--text-primary); - font-weight: bold; -} - -.curve-point-delete { - padding: 4px 8px; - font-size: 14px; -} - -.curve-point-delete:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -/* 加权温度变量绑定列表 */ -.temp-var-bindings { - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px; - background: var(--bg-card); - border-radius: var(--radius); - border: 1px solid var(--border); - max-height: 220px; - overflow-y: auto; -} - -.temp-var-row { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 10px; - background: var(--bg-muted); - border-radius: var(--radius-sm); - transition: background var(--t-normal); -} - -.temp-var-row:hover { - background: var(--bg-color); -} - -.temp-var-row select { - flex: 1; - min-width: 0; -} - -.temp-var-weight { - display: flex; - align-items: center; - gap: 4px; - flex-shrink: 0; -} - -.temp-var-weight label { - font-size: 12px; - color: var(--text-secondary); - margin: 0; - white-space: nowrap; -} - -.temp-var-weight input { - width: 60px; - padding: 5px 6px; - text-align: center; - font-size: 13px; -} - -.temp-var-delete { - padding: 4px 8px; - font-size: 14px; - flex-shrink: 0; -} - -.temp-var-formula { - padding: 8px 12px; - background: var(--bg-card); - border-radius: var(--radius-sm); - border: 1px solid var(--border); - font-family: 'SF Mono', 'Menlo', 'Consolas', monospace; - font-size: 13px; - color: var(--text-secondary); - line-height: 1.6; - word-break: break-all; -} - -.temp-var-formula .result { - color: var(--primary); - font-weight: 600; -} - -.temp-var-weight-warn { - color: var(--warning); - font-size: 12px; - margin-top: 4px; -} - -/* 曲线预览画布 */ -.fan-curve-preview { - background: var(--bg-muted); - border-radius: var(--radius); - padding: 10px; - border: 1px solid var(--border); -} - -.fan-curve-preview canvas { - width: 100%; - border-radius: var(--radius-sm); -} - -/* 输入框带按钮 */ -.input-with-btn { - display: flex; - gap: 8px; -} - -.input-with-btn input { - flex: 1; -} - -/* 表单行 */ -.form-row { - display: flex; - gap: 16px; -} - -.form-row .form-group { - flex: 1; -} - -/* 风扇卡片模式指示 */ -.fan-mode-badge { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border-radius: 4px; - font-size: 11px; - font-weight: 600; -} - -.fan-mode-badge.mode-manual { - background: var(--amber-50); - color: var(--amber-600); -} - -.fan-mode-badge.mode-auto { - background: var(--emerald-50); - color: var(--emerald-600); -} - -.fan-mode-badge.mode-curve { - background: var(--blue-50); - color: var(--blue-600); -} - -.fan-mode-badge.mode-off { - background: var(--bg-muted); - color: var(--text-muted); -} - -/* 设备状态样式 */ -.status-value.status-on { - color: #22c55e; - font-weight: bold; -} - -.status-value.status-off { - color: #6b7280; -} - -.device-status-grid p { - margin: 6px 0; -} - -.card-large { - min-width: 280px; -} - -/* 电源信息 */ -.power-info { - display: flex; - gap: 20px; -} - -.power-card { - background: var(--bg-color); - padding: 16px 24px; - border-radius: 8px; -} - -.power-card p { - margin: 8px 0; -} - -/* 配置表单 */ -.config-filter { - display: flex; - gap: 12px; - margin-bottom: 20px; -} - -.config-filter input { - flex: 1; - max-width: 400px; - padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: 4px; -} - -.config-form { - max-width: 800px; -} - -/* SSH 表单 */ -.ssh-form { - max-width: 800px; - margin-bottom: 20px; -} - -.result-box { - padding: 16px; - border-radius: 8px; - background: var(--bg-color); - margin-top: 16px; -} - -.result-box.success { - background: var(--emerald-100); - color: var(--emerald-600); -} - -.result-box.error { - background: var(--rose-100); - color: var(--rose-600); -} - -/* 导出加密配置包:文件列表排版(网格固定列,避免错位) */ -.pack-export-file-list-inner { - padding: 4px; -} -.pack-export-file-row { - display: grid; - grid-template-columns: 20px 22px 1fr auto; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-bottom: 1px solid var(--border); -} -.pack-export-dir-row { - grid-template-columns: 22px 1fr; - cursor: pointer; -} -.pack-export-row-cb { - margin: 0; - justify-self: start; -} -.pack-export-row-icon { - color: var(--text-secondary); - display: flex; - align-items: center; - justify-content: center; -} -.pack-export-row-icon i { - font-size: 1rem; -} -.pack-export-row-name { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.pack-export-row-label { - cursor: pointer; - margin: 0; - min-width: 0; -} -.pack-export-row-size { - color: var(--text-muted); - font-size: 0.9em; - justify-self: end; -} - -/* Hidden */ -.hidden { - display: none !important; -} - -/* 页面标题 */ -.page-system h1, -.page-led h1, -.page-network h1, -.page-device h1, -.page-config h1, -/* 安全页面:第一个 section 与顶栏间距与指令等页面一致 */ -.page-security .section:first-child { - margin-top: 0; -} - -/* 安全页按钮统一为 btn-sm 尺寸 */ -.page-security .btn, -.page-security .btn-sm { - padding: 5px 10px; - font-size: 0.8rem; -} -/* 安全页按钮 icon 与文字间距统一(与其他页面一致) */ -.page-security .btn { - display: inline-flex; - align-items: center; - gap: 6px; -} -/* HTTPS 证书一行:CSR / 证书 / 删除 等按钮统一宽度,视觉一致(含表格行内按钮) */ -.page-security .button-group .btn { - min-width: 6rem; - box-sizing: border-box; -} -.page-security .data-table td .btn { - width: 6rem; - min-width: 6rem; - height: 2rem; - min-height: 2rem; - line-height: 2rem; - padding-top: 0; - padding-bottom: 0; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - box-sizing: border-box; - text-align: center; -} - -.page-security .form-actions .btn, -.page-security .form-actions .btn-sm { - padding: 5px 10px; - font-size: 0.8rem; -} - -.page-security h1, -.dashboard h1 { - font-size: 1.8rem; - margin-bottom: 20px; -} - -/* 安全页正文/表格/信息卡/弹窗字体与大小统一为与「TianShanOS Intermediate CA」一致 */ -.page-security .section, -.page-security .info-card, -.page-security .data-table, -.page-security .data-table th, -.page-security .data-table td, -.page-security .modal .modal-content { - font-size: 0.9rem; - font-family: inherit; -} -.page-security .section h2 { - font-size: 1.15rem; - font-weight: 600; -} - -.account-security-desc { - color: var(--text-muted); - margin: 0 0 15px; -} - -.account-password-block { - margin-bottom: 16px; -} - -.account-password-block + .account-password-block { - border-top: 1px solid var(--border); - padding-top: 12px; -} - -.account-password-block h3 { - font-size: 1rem; - font-weight: 600; - margin: 0 0 6px; -} - -.account-password-desc { - color: var(--text-muted); - font-size: 0.9rem; - margin: 0 0 12px; -} - -.account-password-form { - display: grid; - grid-template-columns: minmax(240px, 320px) minmax(240px, 320px) minmax(16px, 1fr) auto; - gap: 12px; - align-items: end; - margin-bottom: 12px; -} - -.account-password-group, -.account-security-actions { - margin-bottom: 0; -} - -.account-security-actions { - grid-column: 4; - justify-self: end; -} - -.account-password-field { - position: relative; -} - -.form-group .account-password-input { - min-height: 38px; - padding-right: 38px; - line-height: 20px; - box-sizing: border-box; -} - -.password-visibility-toggle { - position: absolute; - top: 50%; - right: 6px; - width: 28px; - height: 28px; - transform: translateY(-50%); - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - border: 0; - border-radius: var(--radius-sm); - background: transparent; - color: var(--text-muted); - cursor: pointer; -} - -.password-visibility-toggle:hover { - background: var(--bg-tertiary); - color: var(--blue-600); -} - -.password-visibility-toggle.active { - background: var(--blue-50); - color: var(--blue-600); -} - -.account-danger-row { - border-top: 1px solid var(--border); - padding-top: 12px; - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 12px; - align-items: end; -} - -.account-danger-row .btn { - justify-self: end; - align-self: end; -} - -@media (max-width: 780px) { - .account-password-form { - grid-template-columns: 1fr; - } - - .account-security-actions { - grid-column: 1; - justify-self: stretch; - } - - .account-danger-row { - grid-template-columns: 1fr; - } - - .account-security-actions .btn, - .account-danger-row .btn { - width: 100%; - } -} - -.page-security .modal .modal-content h2 { - font-size: 1.1rem; -} -/* 表格内 code、只读 input、CN/设备名等与「TianShanOS Intermediate CA」统一字号 */ -.page-security .data-table code { - font-size: 1em; -} -.page-security input[readonly], -.page-security .modal .modal-content input[type="text"], -.page-security .modal .modal-content label, -.page-security .modal .modal-content .form-group div { - font-size: inherit; -} -.page-security .modal .modal-content textarea { - font-size: inherit; -} - -/* ========================================================================= - 终端页面样式 - ========================================================================= */ - -.terminal-page { - display: flex; - flex-direction: column; - height: calc(100vh - var(--header-height) - var(--footer-height) - 40px); - padding: 0; -} - -.terminal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 10px; - border-bottom: 1px solid var(--border); - margin-bottom: 10px; -} - -.terminal-header h1 { - font-size: 1.5rem; - margin: 0; -} - -.terminal-actions { - display: flex; - gap: 10px; -} - -.terminal-container { - flex: 1; - background: #1e293b; - border-radius: var(--radius); - overflow: hidden; - min-height: 400px; -} - -.terminal-container .xterm { - padding: 10px; - height: 100%; -} - -.terminal-container .xterm-viewport { - overflow-y: auto !important; -} - -.terminal-help { - padding: 10px 0; - color: var(--text-secondary); - font-size: 0.85rem; -} - -.terminal-help code { - background: var(--bg-muted); - padding: 2px 6px; - border-radius: var(--radius-sm); - font-family: monospace; -} - -/* 小按钮 → 已合并到全站统一 .btn-sm */ - -/* ========================================================================= - 文件管理页面样式 - ========================================================================= */ - -.page-files { - max-width: 1200px; - margin: 0 auto; -} - -.file-toolbar { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; - flex-wrap: wrap; - gap: 10px; -} - -/* 批量操作工具栏 */ -.batch-toolbar { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 15px; - background: var(--blue-50); - border: 1px solid transparent; - border-radius: var(--radius); - margin-bottom: 15px; - animation: slideDown 0.2s ease-out; -} - -@keyframes slideDown { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.batch-toolbar.hidden { - display: none; -} - -.batch-toolbar #selected-count { - font-weight: bold; - color: var(--blue-600); - margin-right: 10px; -} - -/* 文件复选框样式 */ -.file-checkbox-cell { - width: 30px; - text-align: center; -} - -.file-checkbox { - width: 16px; - height: 16px; - cursor: pointer; - accent-color: var(--blue-500); -} - -#select-all-cb { - width: 16px; - height: 16px; - cursor: pointer; - accent-color: var(--blue-500); -} - -.breadcrumb { - display: flex; - align-items: center; - gap: 5px; - font-size: 0.95rem; - color: var(--text-secondary); - flex-wrap: wrap; -} - -.breadcrumb-item { - cursor: pointer; - padding: 2px 6px; - border-radius: var(--radius-sm); - transition: all var(--t-normal); -} - -.breadcrumb-item:hover { - background: var(--blue-100); - color: var(--blue-500); -} - -.breadcrumb-item.current { - font-weight: bold; - color: var(--text-primary); -} - -.file-actions { - display: flex; - gap: 8px; - position: relative; - z-index: 1; -} - -.page-files .files-refresh-btn { - min-width: 32px; - min-height: 32px; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid transparent; -} -.page-files .files-refresh-btn:active { - background: var(--blue-50); - color: var(--blue-600); - border-color: transparent; -} - -/* 文件页面白色卡片容器 */ -.file-card { - background: var(--bg-card); - border-radius: var(--radius); - padding: 20px; - box-shadow: var(--shadow); -} - -/* 文件操作按钮与登出按钮大小一致 */ -.btn-file-action { - padding: 4px 12px !important; - font-size: 0.85rem !important; -} - -.storage-tabs { - display: flex; - gap: 5px; - margin-bottom: 15px; - align-items: center; -} - -.storage-controls { - margin-left: auto; -} - -/* 挂载/卸载 SD 按钮与 Tab(SD卡、SPIFFS)高度一致 */ -.storage-tabs .storage-controls .btn { - height: 2rem; - min-height: 2rem; - padding-top: 0; - padding-bottom: 0; - display: inline-flex; - align-items: center; - justify-content: center; - box-sizing: border-box; -} - -.btn-success { - background-color: var(--emerald-50); - border-color: transparent; - color: var(--emerald-600); -} - -.btn-success:hover { - background-color: var(--emerald-100); - border-color: transparent; -} - -.btn-warning { - background-color: var(--amber-50); - border: 1px solid transparent; - color: var(--amber-600); -} - -.btn-warning:hover { - background-color: var(--amber-100); - border-color: transparent; -} - -.tab-btn.btn-service-style { - padding: 4px 12px; - font-size: 0.85rem; - background: var(--blue-50); - color: var(--blue-600); - border: 1px solid transparent; -} - -.tab-btn.btn-service-style:hover { - background: var(--blue-100); - border-color: transparent; -} - -.tab-btn.btn-service-style.active { - background: var(--blue-500); - color: white; - border-color: var(--blue-500); -} - -/* 灰色 Tab 按钮样式(SD卡、SPIFFS),统一宽高 */ -.storage-tabs .tab-btn.btn-gray { - width: 6rem; - min-width: 6rem; - height: 2rem; - min-height: 2rem; - line-height: 2rem; - padding-top: 0; - padding-bottom: 0; - text-align: center; - box-sizing: border-box; - display: inline-flex; - align-items: center; - justify-content: center; -} -.tab-btn.btn-gray { - padding: 4px 12px; - font-size: 0.85rem; - background: var(--bg-muted); - color: var(--text-secondary); - border: 1px solid var(--border); -} - -.tab-btn.btn-gray:hover { - background: var(--bg-muted); - border-color: var(--border-hover); -} - -.tab-btn.btn-gray.active { - background: var(--text-secondary); - color: white; - border-color: var(--text-secondary); -} - -.tab-btn { - padding: 4px 12px; - font-size: 0.85rem; - border: 1px solid var(--border); - background: var(--bg-card); - cursor: pointer; - border-radius: var(--radius-sm); - transition: all var(--t-normal); -} - -.tab-btn:hover { - background: var(--bg-muted); -} - -.tab-btn.active { - background: var(--blue-500); - color: white; - border-color: var(--blue-500); -} - -.file-list { - background: var(--bg-card); - border-radius: var(--radius); - border: 1px solid var(--border); - overflow: hidden; -} - -.file-table { - width: 100%; - border-collapse: collapse; -} - -.file-table th { - background: var(--bg-muted); - padding: 12px 15px; - text-align: left; - font-weight: 600; - border-bottom: 1px solid var(--border); -} - -.file-table td { - padding: 10px 15px; - border-bottom: 1px solid var(--border); - vertical-align: middle; -} - -.file-table tbody tr:last-child td { - border-bottom: none; -} - -.file-row:hover { - background: var(--blue-50); -} - -.file-name.clickable { - cursor: pointer; - color: var(--blue-500); -} - -.file-name.clickable:hover { - text-decoration: underline; -} - -.file-size { - color: var(--text-secondary); - font-size: 0.9rem; - text-align: center; -} - -.file-actions-cell { - white-space: nowrap; - text-align: center; -} - -.file-actions-cell .btn-sm { - margin: 0 2px; -} - -/* 操作列按钮(下载、重命名、删除)统一高度 */ -.file-actions-cell .btn.btn-sm { - height: 28px; - min-height: 28px; - padding-top: 0; - padding-bottom: 0; - display: inline-flex; - align-items: center; - justify-content: center; - box-sizing: border-box; -} - -.empty-folder { - padding: 40px; - text-align: center; - color: var(--text-secondary); - font-size: 1.1rem; -} - -.unmounted-notice { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 60px 20px; - text-align: center; -} - -.unmounted-icon { - font-size: 4rem; - margin-bottom: 15px; - opacity: 0.6; -} - -.unmounted-text { - font-size: 1.2rem; - color: var(--text-secondary); - margin-bottom: 20px; -} - -.storage-status { - margin-top: 15px; - padding: 10px 15px; - background: var(--bg-card); - border-radius: var(--radius); - border: 1px solid var(--border); -} - -.storage-info { - display: flex; - gap: 20px; - flex-wrap: wrap; -} - -.storage-info .mounted { - color: var(--emerald-600); - background: var(--emerald-50); - padding: 2px 8px; - border-radius: var(--radius-sm); - font-size: 0.85em; -} - -.storage-info .unmounted { - color: var(--text-secondary); -} - -/* 上传区域样式 */ -.upload-area { - border: 2px dashed var(--border); - border-radius: var(--radius); - padding: 40px; - text-align: center; - cursor: pointer; - transition: all var(--t-normal); - margin-bottom: 15px; -} - -.upload-area:hover, .upload-area.drag-over { - border-color: var(--blue-500); - background: var(--blue-50); -} - -.upload-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 12px; - background: var(--bg-muted); - border-radius: var(--radius-sm); - margin-bottom: 8px; -} - -.upload-item .uploading { - color: var(--blue-500); -} - -.upload-item .success { - color: var(--emerald-500); -} - -.upload-item .error { - color: var(--rose-500); -} - -/* 响应式调整 */ -@media (max-width: 600px) { - .form-row { - flex-direction: column; - } - - .memory-item { - flex-direction: column; - align-items: stretch; - } - - .memory-item label, - .memory-item span { - text-align: left; - min-width: 0; - } - - .terminal-page { - height: calc(100vh - var(--header-height) - var(--footer-height) - 20px); - } - - .terminal-container { - min-height: 300px; - } - - .file-toolbar { - flex-direction: column; - align-items: stretch; - } - - .file-actions { - justify-content: center; - } - - .file-table th:nth-child(2), - .file-table td:nth-child(2) { - display: none; - } -} - -/* ===================================================================== - 配置页面样式 - ===================================================================== */ - -.page-config .section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 15px; -} - -.page-config .section-header h2 { - margin: 0; -} - -.page-config .section-actions { - display: flex; - gap: 8px; -} - -/* 模块卡片网格 */ -.module-cards { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 12px; -} - -.module-card { - display: flex; - align-items: center; - gap: 12px; - padding: 15px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 10px; - cursor: pointer; - transition: all 0.2s ease; -} - -.module-card:hover { - border-color: var(--primary-color); - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.module-card.dirty { - border-left: 3px solid var(--warning-color); -} - -.module-card.clean { - border-left: 3px solid var(--success-color); -} - -.module-card .module-icon { - font-size: 2rem; -} - -.module-card .module-info { - flex: 1; - min-width: 0; -} - -.module-card .module-name { - font-weight: 600; - font-size: 1rem; - margin-bottom: 2px; -} - -.module-card .module-desc { - font-size: 0.75rem; - color: var(--text-secondary); - margin-bottom: 5px; -} - -.module-card .module-status { - display: flex; - align-items: center; - gap: 5px; - font-size: 0.7rem; - color: var(--text-secondary); -} - -.status-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: var(--text-secondary); -} - -.status-dot.clean { - background: var(--success-color); -} - -.status-dot.dirty { - background: var(--warning-color); -} - -.status-dot.disabled { - background: var(--text-secondary); -} - -/* 配置表单网格 */ -.config-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 15px; -} - -.config-item { - display: flex; - flex-direction: column; - gap: 5px; -} - -.config-item label { - font-size: 0.85rem; - font-weight: 500; - color: var(--text-secondary); -} - -.config-item input[type="text"], -.config-item input[type="number"], -.config-item input[type="password"], -.config-item select { - padding: 9px 12px; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - background: var(--bg-color); - color: var(--text-color); - font-size: 0.9rem; -} - -.config-item input:focus, -.config-item select:focus { - outline: none; - border-color: var(--primary-color); -} - -/* Toggle 开关 */ -.toggle-switch { - position: relative; - display: inline-block; - width: 50px; - height: 26px; -} - -.toggle-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.toggle-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: var(--border-color); - transition: 0.3s; - border-radius: 26px; -} - -.toggle-slider:before { - position: absolute; - content: ""; - height: 20px; - width: 20px; - left: 3px; - bottom: 3px; - background-color: white; - transition: 0.3s; - border-radius: 50%; -} - -.toggle-switch input:checked + .toggle-slider { - background-color: var(--primary-color); -} - -.toggle-switch input:checked + .toggle-slider:before { - transform: translateX(24px); -} - -.form-note { - margin-top: 15px; - padding: 10px; - background: rgba(255, 193, 7, 0.1); - border: 1px solid var(--warning-color); - border-radius: 6px; - color: var(--warning-color); - font-size: 0.85rem; -} - -.badge { - display: inline-block; - padding: 2px 6px; - font-size: 0.65rem; - border-radius: 4px; - font-weight: 600; -} - -.badge-warning { - background: var(--warning-color); - color: #000; -} - -.badge-success { - background: var(--success-color); - color: #fff; -} - -/* ========================================================================= - 网络配置页面样式 - ========================================================================= */ - -.page-network h1 { - margin-bottom: 16px; -} - -/* 网络状态概览 */ -.net-overview { - background: var(--card-bg); - border-radius: 8px; - padding: 16px; - margin-bottom: 20px; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} - -.net-status-row { - display: flex; - gap: 16px; -} - -.net-iface { - flex: 1; - display: flex; - align-items: center; - gap: 12px; - padding: 12px 16px; - background: var(--bg-color); - border-radius: 8px; - border: 2px solid transparent; - transition: all 0.2s; -} - -.net-iface.active { - border-color: var(--secondary-color); - background: var(--emerald-50); -} - -.net-iface.inactive { - opacity: 0.7; -} - -.net-iface .iface-icon { - font-size: 1.8rem; -} - -.net-iface .iface-info { - flex: 1; -} - -.net-iface .iface-name { - font-weight: 600; - font-size: 0.9rem; -} - -.net-iface .iface-status { - font-size: 0.8rem; - color: var(--text-light); -} - -.net-iface .iface-ip, -.net-iface .iface-clients { - font-family: monospace; - font-size: 0.85rem; - color: var(--text-color); -} - -/* 配置网格布局 */ -.net-config-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin-bottom: 20px; -} - -@media (max-width: 900px) { - .net-config-grid { - grid-template-columns: 1fr; - } - .net-status-row { - flex-direction: column; - } -} - -/* 面板样式 */ -.net-panel { - background: var(--card-bg); - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); - overflow: hidden; -} - -.panel-header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - height: 48px; - box-sizing: border-box; - border-bottom: 1px solid var(--border-color); - background: var(--bg-card); -} - -.panel-header h3 { - margin: 0; - font-size: 0.95rem; -} - -.panel-tabs { - display: flex; - gap: 4px; -} - -.panel-tab { - padding: 6px 12px; - font-size: 0.85rem; - background: transparent; - border: none; - border-radius: 4px; - cursor: pointer; - color: var(--text-light); - transition: all 0.2s; -} - -.panel-tab:hover { - background: rgba(0,0,0,0.05); -} - -.panel-tab.active { - background: var(--primary-color); - color: white; -} - -/* 网络页面 Tab 按钮使用登录按钮样式 */ -.panel-tab.btn-service-style { - padding: 4px 12px; - font-size: 0.85rem; - background: var(--blue-50) !important; - color: var(--blue-600) !important; - border: 1px solid transparent; - border-radius: 4px; -} - -.panel-tab.btn-service-style:hover { - background: var(--blue-100) !important; - border-color: transparent; -} - -.panel-tab.btn-service-style.active { - background: var(--blue-500) !important; - color: white !important; - border-color: var(--blue-500); -} - -.panel-content { - padding: 16px; -} - -.panel-content.hidden { - display: none; -} - -/* 配置行样式 */ -.config-section { - margin-bottom: 16px; -} - -.config-section h4 { - font-size: 0.85rem; - color: var(--text-light); - margin-bottom: 10px; - padding-bottom: 6px; - border-bottom: 1px dashed var(--border-color); -} - -.config-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 6px 0; - font-size: 0.9rem; -} - -.config-label { - color: var(--text-light); -} - -.config-value { - font-weight: 500; -} - -.config-value.mono { - font-family: monospace; -} - -.config-value.small { - font-size: 0.8rem; -} - -/* 状态点 */ -.status-dot { - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - margin-right: 6px; -} - -.status-dot.green { background: var(--emerald-500); } -.status-dot.red { background: var(--danger-color); } -.status-dot.gray { background: #999; } -.status-dot.yellow { background: var(--warning-color); } - -/* WiFi 模式选择器 */ -.wifi-mode-selector { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 16px; - padding: 10px; - background: var(--bg-color); - border-radius: 6px; -} - -.wifi-mode-selector label { - font-size: 0.9rem; - font-weight: 500; -} - -.wifi-mode-selector select { - flex: 1; - padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - font-size: 0.9rem; -} - -/* WiFi/AP 操作按钮 */ -.wifi-sta-actions, -.wifi-ap-actions { - display: flex; - gap: 8px; - margin-top: 10px; -} - -/* 服务块样式 */ -.service-block { - margin-bottom: 16px; - padding-bottom: 16px; - border-bottom: 1px solid var(--border-color); -} - -.service-block:last-child { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -.service-header { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 8px; -} - -.service-icon { - font-size: 1.2rem; -} - -.service-name { - flex: 1; - font-weight: 600; - font-size: 0.9rem; -} - -.service-value { - font-family: monospace; - font-size: 0.85rem; - color: var(--text-light); -} - -.service-badge { - padding: 2px 8px; - border-radius: 10px; - font-size: 0.75rem; - font-weight: 600; -} - -.service-badge.badge-ok { - background: var(--emerald-100); - color: var(--secondary-color); -} - -.service-badge.badge-warn { - background: var(--rose-100); - color: var(--danger-color); -} - -.service-detail { - font-size: 0.85rem; - color: var(--text-light); - margin-bottom: 8px; -} - -.service-config { - display: flex; - gap: 8px; - margin-top: 8px; -} - -.service-config .input-sm { - flex: 1; - padding: 8px 10px; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - font-size: 0.875rem; -} - -.service-actions { - display: flex; - gap: 8px; -} - -/* DHCP 接口行 */ -.dhcp-iface-row { - display: flex; - align-items: center; - gap: 8px; - padding: 4px 0; -} - -.dhcp-iface-row .iface-name { - font-weight: 500; -} - -.dhcp-iface-row .iface-detail { - margin-left: auto; - color: var(--text-light); -} - -/* NAT 状态行 */ -.nat-status-row { - display: flex; - align-items: center; - gap: 12px; -} - -.nat-status-row span:nth-child(odd) { - color: var(--text-light); -} - -/* 网络部分面板 */ -.net-section { - background: var(--card-bg); - border-radius: 8px; - padding: 16px; - margin-bottom: 20px; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); -} - -.net-section.hidden { - display: none; -} - -.section-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 12px; -} - -.section-header h2, -.section-header h3 { - margin: 0; - font-size: 1rem; -} - -.section-actions { - display: flex; - gap: 8px; -} - -.lpmu-access-btn, -.btn.btn-sm.lpmu-access-btn { - flex: 0 0 auto; - min-height: 30px; - padding: 5px 12px; - font-size: 0.85rem; - line-height: 1.2; - border-radius: var(--radius-sm); -} - -/* WiFi 网络卡片 */ -.wifi-networks { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 12px; -} - -.wifi-network-card { - display: flex; - align-items: center; - gap: 12px; - padding: 12px; - background: var(--bg-color); - border-radius: 8px; - cursor: pointer; - transition: all 0.2s; - border: 1px solid transparent; -} - -.wifi-network-card:hover { - border-color: var(--primary-color); - background: var(--blue-50); -} - -.wifi-signal { - font-size: 1.5rem; -} - -.wifi-info { - flex: 1; - min-width: 0; -} - -.wifi-ssid { - font-weight: 600; - font-size: 0.95rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.wifi-meta { - display: flex; - gap: 10px; - font-size: 0.8rem; - color: var(--text-light); - margin-top: 2px; -} - -/* 设备卡片 (AP 设备、DHCP 客户端) */ -.ap-stations-list, -.dhcp-clients-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); - gap: 10px; -} - -.device-card { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 12px; - background: var(--bg-color); - border-radius: 6px; -} - -.device-icon { - font-size: 1.3rem; -} - -.device-info { - flex: 1; - min-width: 0; -} - -.device-ip { - font-family: monospace; - font-weight: 600; - font-size: 0.9rem; -} - -.device-mac { - font-family: monospace; - font-size: 0.8rem; - color: var(--text-light); -} - -.device-hostname { - font-size: 0.8rem; - color: var(--text-light); -} - -.device-rssi { - font-size: 0.8rem; - color: var(--text-light); -} - -.device-badge { - padding: 2px 8px; - border-radius: 10px; - font-size: 0.7rem; - background: var(--bg-color); - border: 1px solid var(--border-color); -} - -/* 加载和空状态 */ -.loading-inline { - text-align: center; - padding: 20px; - color: var(--text-light); -} - -.empty-state { - text-align: center; - padding: 30px; - color: var(--text-light); -} - -.error-state { - text-align: center; - padding: 20px; - color: var(--danger-color); -} - -.error-icon { - font-size: 2rem; - margin-bottom: 8px; -} - -.error-text { - font-size: 0.9rem; -} - -/* 小型按钮 → 已合并到全站统一 .btn-sm */ - -/* 小型选择框 */ -.select-sm { - padding: 6px 10px; - font-size: 0.85rem; - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); -} - -/* 小型模态框 */ -.modal-sm { - max-width: 400px; -} - -/* 表单行(并排) */ -.form-row { - display: flex; - gap: 16px; -} - -.form-row .form-group { - flex: 1; -} - -.checkbox-label { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - height: 100%; - padding-top: 24px; -} - -.checkbox-label input[type="checkbox"] { - width: 16px; - height: 16px; -} -/* ========================================================================= - LED 页面响应式布局 - ========================================================================= */ -@media (max-width: 768px) { - .led-devices-grid { - grid-template-columns: 1fr; - } - - .led-page-header { - flex-direction: column; - align-items: flex-start; - } - - .led-card-footer { - justify-content: center; - } - - .led-save-btn { - margin-left: 0; - } - - .led-color-presets { - justify-content: center; - } - - .led-modal { - min-width: 95%; - max-width: 95%; - } -} - -@media (max-width: 400px) { - .led-card-controls { - padding: 12px 14px; - } - - .led-card-header { - padding: 12px 14px; - } - - .led-card-footer { - padding: 10px 14px; - } - - .led-func-btn { - width: 36px; - height: 36px; - } - - .led-power-btn { - font-size: 1.5rem; - } - - .color-dot { - width: 24px; - height: 24px; - } -} - -/* ========================================================================= - 内存详情模态框样式 - ========================================================================= */ - -.memory-detail-modal { - min-width: 600px; - max-width: 800px; -} - -.memory-overview { - display: flex; - gap: 30px; - justify-content: center; - margin-bottom: 24px; - padding: 20px; - background: var(--bg-subtle); - border-radius: var(--radius-lg); -} - -.memory-gauge { - display: flex; - align-items: center; - gap: 16px; -} - -.gauge-ring { - position: relative; - width: 100px; - height: 100px; -} - -.gauge-ring svg { - width: 100%; - height: 100%; - transform: rotate(-90deg); -} - -.gauge-text { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - text-align: center; -} - -.gauge-percent { - display: block; - font-size: 1.4rem; - font-weight: 700; - color: var(--text-color); -} - -.gauge-label { - display: block; - font-size: 0.75rem; - color: var(--text-light); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.gauge-info { - display: flex; - flex-direction: column; - gap: 6px; -} - -.info-row { - display: flex; - justify-content: space-between; - gap: 12px; - font-size: 0.85rem; -} - -.info-row span { - color: var(--text-light); -} - -.info-row strong { - font-weight: 600; -} - -.memory-details { - margin-top: 20px; - margin-bottom: 20px; -} - -.memory-details h4, -.memory-dma-info h4, -.memory-rtc h4, -.memory-caps h4, -.memory-nvs h4, -.memory-tips h4, -.memory-tasks h4, -.memory-history h4 { - font-size: 0.95rem; - font-weight: 600; - margin-bottom: 12px; - color: var(--text-primary); - padding-bottom: 6px; - border-bottom: 1px solid var(--border); -} - -.memory-table { - width: 100%; - border-collapse: collapse; - font-size: 0.85rem; - color: var(--text-secondary); -} - -.memory-table th, -.memory-table td { - padding: 10px 12px; - text-align: left; - border-bottom: 1px solid var(--border); -} - -.memory-table th { - background: var(--bg-muted); - font-weight: 600; - color: var(--text-secondary); - font-size: 0.8rem; - text-transform: uppercase; -} - -.memory-table tr:hover { - background: var(--bg-subtle); -} - -.memory-dma-info { - margin-bottom: 20px; -} - -.dma-bar { - background: var(--bg-card); - padding: 12px; - border-radius: var(--radius); - border: 1px solid var(--border); -} - -.dma-labels { - display: flex; - justify-content: space-between; - margin-top: 8px; - font-size: 0.8rem; - color: var(--text-secondary); -} - -.memory-tips { - margin-top: 20px; - margin-bottom: 20px; -} - -.memory-tip { - padding: 10px 14px; - margin-bottom: 8px; - border-radius: var(--radius-sm); - font-size: 0.85rem; - border-left: 3px solid currentColor; -} - -/* 任务内存统计 */ -.memory-tasks { - margin-top: 20px; -} - -.memory-tasks .task-table code { - background: var(--bg-muted); - padding: 2px 6px; - border-radius: var(--radius-sm); - font-size: 0.9em; -} - -/* 可排序表头 */ -.sortable-table th.sortable { - cursor: pointer; - user-select: none; - transition: background 0.15s; -} - -.sortable-table th.sortable:hover { - background: var(--bg-muted); -} - -/* 静态内存段 */ -.memory-static-sections { - margin-top: 16px; - padding: 12px 16px; - background: var(--blue-50); - border-radius: var(--radius); - border: 1px solid transparent; -} - -.static-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 12px; - margin-top: 8px; -} - -.static-item { - background: var(--bg-card); - padding: 10px 12px; - border-radius: var(--radius-sm); - text-align: center; - box-shadow: var(--shadow); -} - -.static-item.highlight { - background: var(--blue-50); - border: 1px solid transparent; -} - -.static-label { - display: block; - font-size: 0.75rem; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.static-value { - display: block; - font-size: 1.1rem; - font-weight: 600; - color: var(--text-primary); - margin: 4px 0; -} - -.static-desc { - display: block; - font-size: 0.7rem; - color: var(--text-muted); -} - -/* IRAM 信息 */ -.memory-iram { - margin-top: 16px; - padding: 12px 16px; - background: var(--amber-50); - border-radius: var(--radius); - border: 1px solid transparent; -} - -.iram-grid { - display: flex; - gap: 20px; - margin-top: 8px; - flex-wrap: wrap; -} - -.iram-item { - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 16px; - background: var(--bg-card); - border-radius: 6px; - min-width: 100px; -} - -.iram-label { - font-size: 0.75rem; - color: var(--text-secondary); -} - -.iram-value { - font-size: 1rem; - font-weight: 600; - color: var(--amber-600); -} - -/* RTC 内存 */ -.memory-rtc { - margin-top: 20px; - padding: 12px 16px; - background: var(--amber-50); - border-radius: var(--radius); - border: 1px solid transparent; -} - -.rtc-labels { - display: flex; - justify-content: space-between; - margin-top: 6px; - font-size: 0.85rem; - color: var(--text-secondary); -} - -/* 内存能力分布 */ -.memory-caps { - margin-top: 20px; -} - -/* NVS 存储 */ -.memory-nvs { - margin-top: 20px; - padding: 12px 16px; - background: var(--bg-muted); - border-radius: var(--radius); -} - -.nvs-stats { - display: flex; - flex-wrap: wrap; - gap: 16px; - margin-top: 8px; - font-size: 0.85rem; -} - -.nvs-stats span { - color: var(--text-secondary); -} - -.nvs-stats strong { - color: var(--text-primary); -} - -/* 历史统计 */ -.memory-history { - padding: 12px 16px; - background: var(--bg-subtle); - border-radius: var(--radius); - margin-top: 20px; -} - -.history-stats { - display: flex; - gap: 24px; - flex-wrap: wrap; -} - -.history-item { - display: flex; - flex-direction: column; -} - -.history-label { - font-size: 0.75rem; - color: var(--text-secondary); -} - -.history-value { - font-size: 1.1rem; - font-weight: 600; - color: var(--text-primary); -} - -.memory-history p { - margin: 0; - font-size: 0.9rem; -} - -.error-message { - padding: 24px; - text-align: center; - color: var(--danger-color); -} - -/* 响应式设计 */ -@media (max-width: 700px) { - .memory-detail-modal { - min-width: 95%; - max-width: 95%; - } - - .memory-overview { - flex-direction: column; - align-items: center; - gap: 20px; - } - - .memory-gauge { - flex-direction: column; - text-align: center; - } - - .gauge-info { - align-items: center; - } - - .memory-table { - font-size: 0.75rem; - } - - .memory-table th, - .memory-table td { - padding: 8px 6px; - } -} - -/* ============================================ - 自动化页面样式 - ============================================ */ - -/* 页面头部 */ -.page-automation .page-header-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20px; - flex-wrap: wrap; - gap: 12px; -} - -.page-automation .page-header-row h1 { - margin: 0; - font-size: 1.5rem; -} - -.page-automation .header-actions { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -/* 状态卡片网格 */ -.status-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); - gap: 12px; - margin-bottom: 24px; -} - -.status-card { - background: var(--bg-card); - border-radius: var(--radius); - padding: 16px; - text-align: center; - box-shadow: var(--shadow-sm); - display: flex; - flex-direction: column; - align-items: center; - justify-content: space-between; - min-height: 72px; - gap: 4px; -} - -.status-card .status-card-value-row { - display: flex; - align-items: center; - justify-content: center; - gap: 6px; -} - -.status-card.primary { - background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); - color: white; - flex-direction: row; - justify-content: center; - gap: 12px; -} - -.status-card.primary .status-label { - color: rgba(255,255,255,0.85); -} - -.status-card.error { - background: var(--rose-50); - color: var(--danger-color); - grid-column: 1 / -1; -} - -.status-card.success { - background: var(--emerald-50); - color: var(--emerald-600); -} - -.status-card.warning { - background: var(--amber-50); - color: var(--amber-600); -} - -.status-card.info { - background: var(--blue-50); - color: var(--blue-600); -} - -.status-card.loading { - grid-column: 1 / -1; - color: var(--text-secondary); -} - -.status-card .status-icon { - font-size: 1.8rem; -} - -.status-card .status-icon.state-running { color: var(--emerald-500); } -.status-card .status-icon.state-paused { color: var(--amber-500); } -.status-card .status-icon.state-stopped { color: var(--rose-500); } - -.status-card .status-value { - font-size: 1.4rem; - font-weight: 700; -} - -.status-card .status-label { - font-size: 0.8rem; - color: var(--text-secondary); -} - -.status-card .status-info { - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.status-card .status-card-value-row .status-icon { - flex-shrink: 0; -} - -/* Section 通用样式 */ -.page-automation .section { - margin-bottom: 20px; - margin-top: 12px; -} - -.page-automation .section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.page-automation .section-header h2 { - margin: 0; - font-size: 1.1rem; - font-weight: 600; -} - -.page-automation .section-actions { - display: flex; - gap: 6px; - position: relative; - z-index: 1; -} -.page-automation .automation-refresh-btn { - min-width: 32px; - min-height: 32px; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid transparent; -} -.page-automation .automation-refresh-btn:active { - background: var(--blue-50); - color: var(--blue-600); - border-color: transparent; -} - -/* 紧凑卡片 */ -.card.compact { - padding: 0; -} - -.card.compact .card-content { - padding: 12px; -} - -.card.compact .card-footer { - display: flex; - align-items: center; - gap: 8px; - padding: 12px; - border-top: 1px solid var(--border); - background: var(--bg-muted); - border-radius: 0 0 var(--radius) var(--radius); -} - -/* 测试面板 */ -.test-panels { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); - gap: 12px; -} - -.test-panel { - background: var(--bg-card); - border-radius: var(--radius); - overflow: hidden; - box-shadow: var(--shadow-sm); -} - -.test-panel.wide { - grid-column: 1 / -1; -} - -.test-panel-title { - padding: 10px 14px; - font-weight: 600; - font-size: 0.95rem; - background: var(--bg-muted); - border-bottom: 1px solid var(--border); -} - -.test-panel-body { - padding: 12px; - display: flex; - flex-wrap: wrap; - gap: 8px; - align-items: center; -} - -.test-panel-body .input { - flex: 1; - min-width: 80px; -} - -/* SSH 结果输出 */ -.test-result-output { - padding: 10px 14px; - margin: 0; - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; - font-size: 0.85rem; - background: #1e293b; - color: #e8e8e8; - border-top: 1px solid var(--border); - max-height: 200px; - overflow-y: auto; -} - -.test-result-output.loading { - background: var(--bg-muted); - color: var(--text-secondary); - text-align: center; -} - -.test-result-output.success { - border-top: 2px solid var(--secondary-color); -} - -.test-result-output.error { - border-top: 2px solid var(--danger-color); - background: #3b1c1c; - color: #fca5a5; -} - -.test-result-output pre { - margin: 0; - white-space: pre-wrap; - word-wrap: break-word; -} - -.test-result-output .exit-code { - margin-top: 8px; - padding-top: 8px; - border-top: 1px dashed rgba(255,255,255,0.2); - color: var(--text-muted); - font-size: 0.8rem; -} - -/* 按钮更小尺寸 */ -.btn-xs { - padding: 2px 6px; - font-size: 0.75rem; -} - -/* 执行日志 */ -.result-log { - max-height: 200px; - overflow-y: auto; - padding: 8px 12px; -} - -.result-log .empty-state { - text-align: center; - color: var(--text-secondary); - padding: 20px; - margin: 0; -} - -.result-log .log-item { - padding: 8px 10px; - margin-bottom: 4px; - border-radius: var(--radius-sm); - font-family: monospace; - font-size: 0.85em; - display: flex; - gap: 8px; - align-items: flex-start; -} - -.result-log .log-item.success { - background: var(--emerald-50); - border-left: 3px solid var(--emerald-500); -} - -.result-log .log-item.error { - background: var(--rose-50); - border-left: 3px solid var(--rose-500); -} - -.result-log .log-time { - color: var(--text-secondary); - font-size: 0.85em; - white-space: nowrap; -} - -/* 数据表格优化 - 圆角与 section 卡片一致 */ -.page-automation .data-table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - font-size: 0.9rem; - table-layout: auto; -} - -.page-automation .data-table th { - background: var(--bg-muted); - padding: 10px 14px; - text-align: left; - font-weight: 600; - font-size: 0.85rem; - color: var(--text-secondary); - border-bottom: 2px solid var(--border); - white-space: nowrap; -} - -.page-automation .data-table thead tr:first-child th:first-child { - border-top-left-radius: var(--radius); -} -.page-automation .data-table thead tr:first-child th:last-child { - border-top-right-radius: var(--radius); -} -.page-automation .data-table tbody tr:last-child td:first-child { - border-bottom-left-radius: var(--radius); -} -.page-automation .data-table tbody tr:last-child td:last-child { - border-bottom-right-radius: var(--radius); -} - -.page-automation .data-table td { - padding: 10px 14px; - border-bottom: 1px solid var(--border); - vertical-align: middle; -} - -.page-automation .data-table tr:hover { - background: var(--bg-muted); -} - -.page-automation .data-table code { - background: var(--bg-muted); - padding: 2px 6px; - border-radius: var(--radius); - font-size: 0.85em; -} - -/* 操作列图标按钮:统一为正方形,避免下载/删除按钮比前两个宽 */ -.page-automation .data-table td .btn.btn-sm.btn-icon-square, -.page-automation .data-table td .btn.btn-sm.btn-danger.btn-icon-square { - width: 32px; - min-width: 32px; - height: 32px; - padding: 0; - display: inline-flex; - align-items: center; - justify-content: center; -} - -/* 状态徽章 */ -.status-badge { - display: inline-block; - padding: 3px 10px; - border-radius: 12px; - font-size: 0.8rem; - font-weight: 500; -} - -.status-badge.status-running { - background: var(--emerald-50); - color: var(--emerald-600); - border: 1px solid transparent; -} - -.status-badge.status-stopped { - background: var(--rose-50); - color: var(--rose-600); - border: 1px solid transparent; -} - -/* 类型标签 */ -.type-badge { - display: inline-block; - padding: 2px 8px; - background: var(--primary-color); - color: white; - border-radius: 4px; - font-size: 0.8rem; -} - -/* 空状态 */ -.empty-state { - text-align: center; - color: var(--text-secondary); - padding: 24px; -} - -/* ============================================ - 自动化模态框样式 - ============================================ */ - -.automation-modal { - width: 90%; - max-width: 600px; - max-height: 85vh; - overflow: hidden; - display: flex; - flex-direction: column; -} - -.automation-modal.wide { - max-width: 750px; -} - -.automation-modal .modal-body { - padding: 20px; - overflow-y: auto; - flex: 1; -} - -/* 模态框标签页 */ -.automation-modal .modal-tabs { - display: flex; - border-bottom: 2px solid var(--border); - margin-bottom: 16px; -} - -.automation-modal .modal-tab { - flex: 1; - padding: 10px 16px; - border: none; - background: transparent; - cursor: pointer; - font-size: 0.95em; - color: var(--text-secondary); - border-bottom: 2px solid transparent; - margin-bottom: -2px; - transition: all 0.2s; -} - -.automation-modal .modal-tab:hover { - color: var(--text-primary); - background: var(--bg-muted); -} - -/* 与 OTA/btn-service-style 一致:浅蓝底、蓝色字、蓝边框 */ -.automation-modal .modal-tab.active { - background: var(--blue-50); - color: var(--blue-600); - border-bottom-color: var(--blue-500); - font-weight: 500; -} - -/* 表单样式 */ -.automation-modal .form-row { - display: flex; - gap: 12px; - margin-bottom: 16px; -} - -.automation-modal .form-row.three-col { - display: flex; - flex-wrap: wrap; -} - -.automation-modal .form-group { - flex: 1; - min-width: 0; -} - -/* 确保自动化模态框内所有输入框和选择框填满容器 */ -.automation-modal .form-group input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), -.automation-modal .form-group select, -.automation-modal .form-group textarea, -.automation-modal .config-section input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]), -.automation-modal .config-section select { - width: 100%; - display: block; - box-sizing: border-box; -} - -.automation-modal .form-group label { - display: block; - margin-bottom: 6px; - font-weight: 500; - font-size: 0.9rem; -} - -.automation-modal .form-group label .required { - color: var(--rose-500); -} - -.automation-modal .checkbox-label { - display: flex; - align-items: center; - gap: 8px; - cursor: pointer; - font-size: 0.9rem; -} - -/* 配置区块 */ -.automation-modal .config-section { - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 14px; - margin-bottom: 16px; - background: var(--bg-muted); -} - -.automation-modal .config-section .config-title { - font-weight: 600; - font-size: 0.95rem; - margin-bottom: 12px; - display: block; -} - -.automation-modal .config-section .config-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 12px; -} - -.automation-modal .empty-hint { - text-align: center; - color: var(--text-secondary); - padding: 16px; - margin: 0; - font-size: 0.9rem; -} - -/* 添加规则模态框:表单项左对齐 */ -#add-rule-modal .form-group label, -#add-rule-modal .config-section, -#add-rule-modal .icon-type-tabs, -#add-rule-modal .icon-picker { - text-align: left; -} - -/* 规则图标选择器:左对齐、布局整齐 */ -#add-rule-modal .icon-type-tabs { - justify-content: flex-start; - margin-bottom: 10px; -} - -#add-rule-modal .icon-picker { - justify-content: flex-start; - align-items: flex-start; - width: 100%; - box-sizing: border-box; -} - -#add-rule-modal .icon-picker .emoji-custom-input { - flex-basis: 100%; - width: 100%; - margin-bottom: 8px; -} - -#add-rule-modal .icon-picker .icon-btn { - flex-shrink: 0; -} - -#add-rule-modal .actions-hint-block { - text-align: center; -} - -#add-rule-modal .form-row.three-col { - display: flex; - flex-wrap: nowrap; - align-items: center; - gap: 12px; -} - -/* 条件逻辑缩短,避免与冷却时间、立即启用挤在一起 */ -#add-rule-modal .form-row.three-col .form-group-logic { - flex: 0 0 130px; - min-width: 0; -} - -#add-rule-modal .form-row.three-col .form-group-logic select.input, -#add-rule-modal .form-row.three-col .form-group input[type="number"] { - height: 38px; - min-height: 38px; - box-sizing: border-box; -} - -#add-rule-modal .form-row.three-col .form-group:nth-of-type(2) { - flex: 0 0 140px; - min-width: 0; -} - -#add-rule-modal .form-row.three-col .checkbox-label { - flex: 0 0 auto; - margin-left: 16px; - margin-bottom: 0; -} - -/* 冷却时间与条件逻辑样式统一:隐藏 number 的默认 spinner,与 select 下拉视觉一致 */ -#add-rule-modal input[type="number"] { - -moz-appearance: textfield; -} -#add-rule-modal input[type="number"]::-webkit-outer-spin-button, -#add-rule-modal input[type="number"]::-webkit-inner-spin-button { - -webkit-appearance: none; - margin: 0; -} - -/* 条件行 */ -.condition-row { - display: flex; - gap: 8px; - margin-bottom: 8px; - align-items: center; -} - -.condition-row .input { - flex: 1; -} - -.condition-row .cond-operator { - flex: 0 0 130px; -} - -.condition-row .btn { - flex: 0 0 32px; - padding: 6px; -} - -/* 动作行 */ -.action-row { - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 10px; - margin-bottom: 8px; - background: white; -} - -.action-row-header { - display: flex; - gap: 8px; - margin-bottom: 8px; - align-items: center; -} - -.action-row-header .action-type { - flex: 1; -} - -.action-row-header .btn { - flex: 0 0 32px; - padding: 6px; -} - -.action-params { - display: flex; - gap: 8px; - flex-wrap: wrap; - align-items: center; -} - -.action-params .input { - min-width: 0; -} - -/* 输入框带按钮 */ -.input-with-btn { - display: flex; - gap: 8px; -} - -.input-with-btn .input { - flex: 1; -} - -.input-with-btn .btn { - flex-shrink: 0; - white-space: nowrap; -} - -/* API 测试结果面板 */ -.test-result-panel { - margin: 12px 0; - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; - background: white; -} - -.test-result-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 12px; - background: var(--bg-color); - border-bottom: 1px solid var(--border-color); -} - -.test-result-header .test-status { - font-size: 0.9rem; -} - -/* JSON 预览 */ -.json-preview { - max-height: 150px; - overflow: auto; - padding: 10px 12px; - font-family: monospace; - font-size: 0.8rem; - background: #1e293b; - color: #d4d4d4; - white-space: pre; - border-bottom: 1px solid var(--border-color); -} - -/* 变量选择器 */ -.var-selector { - padding: 10px 12px; -} - -.var-selector-title { - font-size: 0.85rem; - font-weight: 500; - margin-bottom: 8px; - color: var(--text-light); -} - -.var-list { - display: flex; - flex-direction: column; - gap: 4px; - max-height: 200px; - overflow-y: auto; -} - -/* 变量数据源配置样式 */ -.config-hint { - display: flex; - align-items: flex-start; - gap: 8px; - padding: 10px 12px; - margin-bottom: 14px; - background: rgba(var(--primary-rgb, 64, 158, 255), 0.1); - border-radius: 6px; - font-size: 0.9rem; - color: var(--text-color); -} - -.config-hint .hint-icon { - font-size: 1.1rem; - flex-shrink: 0; -} - -.radio-group { - display: flex; - gap: 20px; - margin-top: 6px; -} - -.radio-label { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; - font-size: 0.95rem; -} - -.radio-label input[type="radio"] { - width: 16px; - height: 16px; - cursor: pointer; -} - -.var-mode-config { - margin-top: 12px; -} - -/* SSH 变量预览框 */ -.ssh-vars-preview { - margin-top: 10px; - padding: 12px; - background: var(--bg-color); - border: 1px solid var(--border-color); - border-radius: 6px; -} - -.ssh-vars-preview .preview-title { - font-size: 0.85rem; - color: var(--text-light); - margin-bottom: 8px; - font-weight: 500; -} - -.ssh-vars-list { - display: flex; - flex-direction: column; - gap: 4px; -} - -.ssh-vars-list .var-item-preview { - font-size: 0.85rem; - padding: 4px 8px; - background: white; - border-radius: 4px; - border: 1px solid var(--border-color); -} - -.ssh-vars-list .var-item-preview code { - color: var(--primary-color); - font-weight: 500; -} - -/* SSH 命令详情预览框 */ -.ssh-cmd-preview { - margin: 12px 0; - padding: 12px; - background: var(--blue-50); - border: 1px solid var(--blue-100); - border-radius: 8px; -} - -.ssh-cmd-preview .preview-title { - font-size: 0.85rem; - color: var(--primary-color); - margin-bottom: 8px; - font-weight: 600; -} - -.ssh-cmd-preview .preview-content { - display: flex; - flex-direction: column; - gap: 6px; -} - -.ssh-cmd-preview .preview-row { - font-size: 0.9rem; - display: flex; - align-items: flex-start; - gap: 8px; -} - -.ssh-cmd-preview .preview-label { - color: var(--text-light); - min-width: 50px; - flex-shrink: 0; -} - -.ssh-cmd-preview .preview-row code { - background: rgba(0, 0, 0, 0.05); - padding: 2px 6px; - border-radius: 4px; - font-family: 'Monaco', 'Consolas', monospace; - font-size: 0.85rem; - word-break: break-all; -} - -/* ========================================== - 动作模板 Modal 新布局 - ========================================== */ - -/* 动作模板模态框需要更宽的布局 */ -#action-modal .modal-content { - width: 800px; - max-width: 95vw; -} - -@media (max-width: 840px) { - #action-modal .modal-content { - width: 95vw; - } -} - -/* Modal 内容区 section */ -.action-section { - margin-bottom: 24px; - padding-bottom: 20px; - border-bottom: 1px solid var(--border); -} - -.action-section:last-of-type { - border-bottom: none; - margin-bottom: 0; - padding-bottom: 0; -} - -/* 动作模板表单行:确保并排布局不折行 */ -.action-section .form-row { - display: flex; - gap: 12px; - flex-wrap: nowrap; - align-items: flex-start; - margin-bottom: 16px; -} - -.action-section .form-row .form-group { - min-width: 0; -} - -.section-title { - display: flex; - align-items: center; - gap: 10px; - font-size: 1rem; - font-weight: 600; - color: var(--text-color); - margin-bottom: 16px; -} - -.section-title .step-num { - display: inline-flex; - align-items: center; - justify-content: center; - width: 26px; - height: 26px; - border-radius: 50%; - background: var(--blue-500); - color: white; - font-size: 0.85rem; - font-weight: 700; -} - -/* 动作类型卡片网格 */ -.action-type-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 12px; -} - -@media (max-width: 600px) { - .action-type-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -.action-type-card { - display: flex; - flex-direction: column; - align-items: center; - padding: 14px 10px; - border: 2px solid var(--border); - border-radius: var(--radius-lg); - cursor: pointer; - transition: all 0.2s ease; - background: white; - text-align: center; -} - -.action-type-card:hover { - border-color: var(--blue-500); - background: var(--blue-50); - transform: translateY(-2px); -} - -.action-type-card.selected, -.action-type-card:has(input:checked) { - border-color: var(--blue-500); - background: var(--blue-100); - box-shadow: 0 4px 12px var(--blue-100); -} - -.action-type-card input[type="radio"] { - position: absolute; - opacity: 0; - width: 0; - height: 0; -} - -.action-type-card .card-icon { - font-size: 1.8rem; - margin-bottom: 6px; -} - -.action-type-card .card-title { - font-weight: 600; - font-size: 0.9rem; - color: var(--text-color); - margin-bottom: 4px; -} - -.action-type-card .card-desc { - font-size: 0.75rem; - color: var(--text-secondary); - line-height: 1.3; -} - -/* 参数配置卡片 */ -.params-card { - background: var(--bg-muted); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 16px; -} - -.params-header { - display: flex; - align-items: center; - gap: 8px; - font-weight: 600; - font-size: 0.95rem; - color: var(--text-color); - margin-bottom: 14px; - padding-bottom: 10px; - border-bottom: 1px solid var(--border-color); -} - -.params-header .params-icon { - font-size: 1.2rem; -} - -/* 快捷命令按钮组 */ -.quick-commands { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 8px; - margin-top: 10px; - padding-top: 10px; - border-top: 1px dashed var(--border-color); -} - -.quick-label { - font-size: 0.8rem; - color: var(--text-light); - font-weight: 500; -} - -.quick-btn { - padding: 5px 12px; - font-size: 0.8rem; - background: white; - border: 1px solid var(--border-color); - border-radius: 6px; - cursor: pointer; - transition: all 0.15s ease; -} - -.quick-btn:hover { - background: var(--primary-color); - color: white; - border-color: var(--primary-color); -} - -/* 带单位的输入框 */ -.input-with-unit { - display: flex; - align-items: stretch; - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; - background: white; -} - -.input-with-unit input { - flex: 1; - border: none; - padding: 9px 12px; - font-size: 0.9rem; - outline: none; -} - -/* 覆盖 .form-group input 的高优先级规则,避免双重边框和阴影 */ -.form-group .input-with-unit input:not([type="checkbox"]):not([type="radio"]) { - border: none; - border-radius: 0; - box-shadow: none; - width: auto; -} - -.input-with-unit input:focus { - box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0.2); -} - -.form-group .input-with-unit input:focus { - box-shadow: inset 0 0 0 2px rgba(59, 130, 246, 0.2); -} - -.input-with-unit .unit { - display: flex; - align-items: center; - padding: 0 12px; - background: var(--bg-color); - color: var(--text-light); - font-size: 0.85rem; - border-left: 1px solid var(--border-color); -} - -/* 执行模式选项 - 异步/同步开关 */ -.execution-mode-option { - display: flex; - align-items: center; - gap: 14px; - margin-top: 16px; - padding: 14px 16px; - background: linear-gradient(135deg, rgba(59, 130, 246, 0.04), rgba(139, 92, 246, 0.04)); - border: 1px solid var(--border-color); - border-radius: 10px; - transition: all 0.2s ease; -} - -.execution-mode-option:hover { - border-color: var(--primary-color); - background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(139, 92, 246, 0.08)); -} - -.execution-mode-option:has(input:checked) { - border-color: var(--primary-color); - background: linear-gradient(135deg, rgba(59, 130, 246, 0.12), rgba(139, 92, 246, 0.1)); -} - -.mode-switch { - position: relative; - width: 52px; - height: 28px; - flex-shrink: 0; -} - -.mode-switch input { - opacity: 0; - width: 0; - height: 0; -} - -.mode-slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: var(--border-color); - border-radius: 28px; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); -} - -.mode-slider:before { - position: absolute; - content: ""; - height: 22px; - width: 22px; - left: 3px; - bottom: 3px; - background: white; - border-radius: 50%; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 2px 4px rgba(0,0,0,0.15); -} - -.mode-switch input:checked + .mode-slider { - background: linear-gradient(135deg, var(--blue-500), var(--violet-500)); -} - -.mode-switch input:checked + .mode-slider:before { - transform: translateX(24px); -} - -.mode-switch input:focus + .mode-slider { - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); -} - -.mode-info { - display: flex; - flex-direction: column; - gap: 2px; - flex: 1; -} - -.mode-title { - font-weight: 600; - font-size: 0.95rem; - color: var(--text-color); -} - -.mode-desc { - font-size: 0.8rem; - color: var(--text-light); - line-height: 1.4; -} - -.execution-mode-option:has(input:checked) .mode-title { - color: var(--primary-color); -} - -/* 等宽字体输入框 */ -.input-mono { - font-family: 'Monaco', 'Consolas', 'Courier New', monospace !important; -} - -/* 高级选项折叠 */ -.advanced-toggle { - margin-top: 14px; -} - -.advanced-toggle summary { - padding: 8px 12px; - background: rgba(0, 0, 0, 0.02); - border: 1px solid var(--border-color); - border-radius: 6px; - cursor: pointer; - font-size: 0.85rem; - color: var(--text-light); - user-select: none; -} - -.advanced-toggle summary:hover { - background: var(--blue-50); -} - -.advanced-toggle[open] summary { - border-radius: 6px 6px 0 0; - border-bottom: none; -} - -.advanced-content { - padding: 12px; - border: 1px solid var(--border-color); - border-top: none; - border-radius: 0 0 6px 6px; - background: rgba(0, 0, 0, 0.01); -} - -.advanced-content .form-group { - margin-bottom: 12px; -} - -.advanced-content .form-group:last-child { - margin-bottom: 0; -} - -/* LED 配置专用样式 */ -.led-color-config { - display: flex; - align-items: center; - gap: 12px; -} - -.led-color-picker-sm { - width: 50px; - height: 38px; - border: 1px solid var(--border-color); - border-radius: 6px; - padding: 2px; - cursor: pointer; -} - -.color-presets-inline { - display: flex; - gap: 6px; - flex-wrap: wrap; -} - -.color-presets-inline .color-dot { - width: 24px; - height: 24px; - border-radius: 50%; - border: 2px solid rgba(0, 0, 0, 0.1); - cursor: pointer; - transition: transform 0.15s, box-shadow 0.15s; -} - -.color-presets-inline .color-dot:hover { - transform: scale(1.15); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); -} - -.brightness-config { - display: flex; - align-items: center; - gap: 10px; -} - -.brightness-slider-sm { - flex: 1; - height: 6px; - -webkit-appearance: none; - appearance: none; - background: linear-gradient(to right, #333, #fff); - border-radius: 3px; - outline: none; -} - -.brightness-slider-sm::-webkit-slider-thumb { - -webkit-appearance: none; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--primary-color); - cursor: pointer; - border: 2px solid white; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); -} - -.brightness-val { - font-size: 0.85rem; - font-weight: 600; - color: var(--text-color); - min-width: 30px; - text-align: right; -} - -/* 基本信息区域的紧凑表单 */ -.compact-form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 12px; -} - -@media (max-width: 480px) { - .compact-form-row { - grid-template-columns: 1fr; - } -} - -/* 表单提示文字 */ -.form-hint { - display: block; - font-size: 0.78rem; - color: var(--text-light); - margin-top: 4px; -} - -/* 表单错误信息 */ -.form-error { - display: block; - background: rgba(239, 68, 68, 0.1); - border: 1px solid var(--rose-500); - border-radius: 6px; - padding: 8px 12px; - margin-top: 12px; - font-size: 0.85rem; - color: var(--rose-500); -} - -.form-error.hidden { - display: none; -} - -/* 必填标记 */ -.required { - color: var(--rose-500); -} - -/* CLI 预设按钮组 */ -.cli-presets { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-top: 6px; -} - -.cli-presets .btn { - font-size: 0.8rem; - padding: 4px 10px; - background: var(--bg-hover); - border: 1px solid var(--border-color); -} - -.cli-presets .btn:hover { - background: var(--primary-color); - color: white; - border-color: var(--primary-color); -} - -/* 高级选项折叠面板 */ -.advanced-options { - margin-top: 15px; - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.advanced-options summary { - padding: 10px 15px; - background: var(--bg-color); - cursor: pointer; - font-weight: 500; - user-select: none; -} - -.advanced-options summary:hover { - background: var(--blue-100); -} - -.advanced-options .advanced-content { - padding: 15px; - border-top: 1px solid var(--border-color); -} - -.advanced-options .advanced-content .form-group { - margin-bottom: 12px; -} - -.advanced-options .advanced-content .form-group:last-child { - margin-bottom: 0; -} - -.advanced-options .advanced-content small { - display: block; - color: var(--text-light); - font-size: 0.8rem; - margin-top: 4px; -} - -/* 变量浏览面板(保留用于其他地方) */ -.var-browse-panel { - margin-top: 14px; - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; - background: white; -} - -.var-browse-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 12px; - background: var(--bg-color); - border-bottom: 1px solid var(--border-color); - font-weight: 500; - font-size: 0.9rem; -} - -.var-browse-list { - max-height: 250px; - overflow-y: auto; - padding: 8px; -} - -.var-browse-list .var-group-header { - font-size: 0.8rem; - font-weight: 600; - color: var(--text-light); - padding: 8px 6px 4px; - margin-top: 4px; - border-bottom: 1px solid var(--border-color); -} - -.var-browse-list .var-group-header:first-child { - margin-top: 0; -} - -.var-browse-list .var-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 10px; - margin: 4px 0; - border-radius: 4px; - cursor: pointer; - transition: all 0.15s; - background: var(--bg-color); -} - -.var-browse-list .var-item:hover { - background: var(--primary-color); - color: white; -} - -.var-browse-list .var-item:hover .var-preview { - color: rgba(255,255,255,0.7); -} - -.var-browse-list .var-name { - font-family: 'Monaco', 'Consolas', monospace; - font-size: 0.85rem; - font-weight: 500; -} - -.var-browse-list .var-preview { - font-size: 0.8rem; - color: var(--text-light); - max-width: 150px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.var-item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 10px; - border-radius: 4px; - cursor: pointer; - transition: all 0.15s; - background: var(--bg-color); - border: 1px solid transparent; -} - -.var-item:hover { - background: var(--primary-color); - color: white; - border-color: var(--primary-color); -} - -.var-item:hover .var-type, -.var-item:hover .var-value { - color: rgba(255,255,255,0.8); -} - -.var-item.disabled { - cursor: not-allowed; - opacity: 0.6; -} - -.var-item.disabled:hover { - background: var(--bg-color); - color: inherit; -} - -.var-path { - font-family: monospace; - font-size: 0.85rem; - font-weight: 500; - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.var-type { - font-size: 0.75rem; - padding: 2px 6px; - background: var(--primary-color); - color: white; - border-radius: 3px; - flex-shrink: 0; -} - -.var-value { - font-size: 0.8rem; - color: var(--text-light); - max-width: 120px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex-shrink: 0; -} - -/* 响应式 */ -@media (max-width: 768px) { - .page-automation .page-header-row { - flex-direction: column; - align-items: flex-start; - } - - .status-grid { - grid-template-columns: repeat(3, 1fr); - } - - .test-panels { - grid-template-columns: 1fr; - } - - .page-automation .data-table { - display: block; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - } - - .page-automation .data-table th, - .page-automation .data-table td { - padding: 8px 10px; - white-space: nowrap; - } - - .page-automation .section { - padding: 16px; - margin-bottom: 16px; - } - - .page-automation .card.compact .card-content { - padding: 8px; - } - - .automation-modal { - width: 95%; - max-height: 90vh; - } - - .automation-modal .form-row { - flex-direction: column; - } - - .condition-row { - flex-wrap: wrap; - } - - .condition-row .cond-variable, - .condition-row .cond-value { - flex: 1 1 100%; - } - - .var-item { - flex-wrap: wrap; - } - - .var-value { - max-width: 100%; - flex-basis: 100%; - } -} - -/* ======================================== - 变量列表样式 - ======================================== */ - -.variables-container { - max-height: 500px; - overflow-y: auto; -} - -.variables-grouped { - display: flex; - flex-direction: column; - gap: 12px; -} - -.variable-group { - border: 1px solid var(--border-color); - border-radius: 8px; - overflow: hidden; -} - -.variable-group-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 10px 16px; - background: var(--bg-color); - cursor: pointer; - font-weight: 500; -} - -.variable-group-header:hover { - background: var(--bg-muted); -} - -.source-name { - color: var(--primary-color); -} - -.variable-count { - font-size: 0.85em; - color: var(--text-light); - background: var(--bg-card); - padding: 2px 8px; - border-radius: 10px; -} - -.variable-items { - padding: 0; -} - -.variable-items .data-table { - margin: 0; - border: none; -} - -.variable-items .data-table th { - position: sticky; - top: 0; - background: var(--bg-subtle); -} - -.variable-name { - font-family: 'Monaco', 'Consolas', monospace; - font-size: 0.85em; - color: var(--primary-dark); -} - -.variable-value { - max-width: 300px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.variable-time { - font-size: 0.85em; - color: var(--text-light); -} - -/* 类型徽章 */ -.type-badge { - display: inline-block; - padding: 2px 8px; - border-radius: 4px; - font-size: 0.75em; - font-weight: 500; - text-transform: uppercase; -} - -.type-number { - background: var(--blue-50); - color: var(--blue-600); -} - -.type-string { - background: var(--amber-50); - color: var(--amber-600); -} - -.type-boolean { - background: var(--emerald-50); - color: var(--emerald-600); -} - -.type-object, .type-array { - background: rgba(139, 92, 246, 0.06); - color: var(--violet-500); -} - -.type-unknown { - background: var(--bg-muted); - color: var(--text-secondary); -} - -/* 值格式化样式 */ -.null-value { - color: var(--text-muted); - font-style: italic; -} - -.number-value { - color: var(--blue-600); - font-family: 'Monaco', 'Consolas', monospace; -} - -.string-value { - color: var(--amber-600); -} - -.bool-true { - color: var(--emerald-600); - font-weight: 500; -} - -.bool-false { - color: var(--rose-600); - font-weight: 500; -} - -/* 搜索框 */ -.input-sm { - padding: 4px 10px; - font-size: 0.9em; - width: 180px; -} - -/* Badge */ -.badge { - display: inline-block; - background: var(--primary-color); - color: white; - padding: 2px 8px; - border-radius: 10px; - font-size: 0.8em; - font-weight: 500; - margin-left: 8px; -} - -/* 浅蓝标签(与 btn-service-style 配色一致:安全页密钥类型/SSH/HTTPS/部署密钥、配置包类型等) */ -.badge-service-style, -.badge-info { - background: var(--blue-50) !important; - color: var(--blue-600) !important; - border: 1px solid transparent; - margin-left: 0; -} - -/* ===== SSH Command Icon Picker ===== */ - -/* Icon type tabs */ -.icon-type-tabs { - display: flex; - gap: 8px; - margin-bottom: 12px; -} - -.icon-tab { - padding: 8px 16px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-bg); - color: var(--text-color); - cursor: pointer; - font-size: 0.95em; - transition: all 0.2s; -} - -.icon-tab:hover { - border-color: var(--primary-color); - color: var(--primary-color); -} - -.icon-tab.active { - background: var(--primary-color); - border-color: var(--primary-color); - color: white; -} - -/* Emoji icon picker */ -.icon-picker { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 12px; - background: var(--bg-color); - border-radius: 8px; - max-height: 180px; - overflow-y: auto; - align-items: center; -} - -.emoji-custom-input { - flex-basis: 100%; - margin-bottom: 8px; -} - -.emoji-custom-input input { - font-size: 1.3em; -} - -.icon-btn { - width: 36px; - height: 36px; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.4em; - border: 2px solid transparent; - border-radius: 8px; - background: var(--card-bg); - cursor: pointer; - transition: all 0.15s; -} - -.icon-btn:hover { - transform: scale(1.1); - border-color: var(--primary-color); -} - -.icon-btn.selected { - border-color: var(--primary-color); - background: var(--blue-100); - box-shadow: 0 0 0 3px var(--blue-100); -} - -/* Image icon picker */ -.icon-image-picker { - padding: 12px; - background: var(--bg-color); - border-radius: 8px; -} - -.icon-preview-row { - display: flex; - align-items: center; - gap: 12px; -} - -.icon-image-preview { - width: 64px; - height: 64px; - border: 2px dashed var(--border-color); - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; - background: var(--card-bg); - overflow: hidden; - flex-shrink: 0; -} - -.icon-image-preview img { - max-width: 100%; - max-height: 100%; - object-fit: contain; -} - -.icon-image-preview .placeholder-text { - color: var(--text-light); - font-size: 0.75em; - text-align: center; -} - -.icon-path-input { - flex: 1; - display: flex; - gap: 8px; -} - -.icon-path-input input { - flex: 1; - padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: 6px; - font-size: 0.9em; - background: var(--card-bg); -} - -.icon-path-input input:read-only { - background: var(--bg-color); - color: var(--text-light); -} - -.icon-path-input button { - padding: 8px 12px; - border: 1px solid var(--border-color); - border-radius: 6px; - background: var(--card-bg); - cursor: pointer; - transition: all 0.2s; - white-space: nowrap; -} - -.icon-path-input button:hover { - border-color: var(--primary-color); - color: var(--primary-color); -} - -/* Command card icon display */ -.cmd-icon { - font-size: 1.8em; - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - background: var(--bg-color); - border-radius: 12px; - flex-shrink: 0; -} - -.cmd-icon img { - max-width: 36px; - max-height: 36px; - object-fit: contain; -} - -/* ===== Quick Actions Panel (System Page) ===== */ - -.quick-actions-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - gap: 12px; - padding: 10px 0; -} - -.quick-action-card { - position: relative; - display: flex; - flex-direction: column; - align-items: center; - justify-content: flex-start; - padding: 14px 10px 38px; /* 统一底部 padding,为操作栏留出空间 */ - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: 12px; - cursor: pointer; - transition: all 0.2s ease; - text-align: center; - min-height: 130px; /* 固定最小高度 */ -} - -.quick-action-card:hover { - border-color: var(--primary-color); - box-shadow: 0 4px 12px var(--blue-100); - transform: translateY(-2px); -} - -.quick-action-card:active { - transform: scale(0.95); - background: var(--blue-100); -} - -.quick-action-card.triggering { - transform: scale(0.95); - background: var(--blue-100); - pointer-events: none; - opacity: 0.7; -} - -/* 触发中的加载动画 */ -.quick-action-card.triggering .quick-action-icon { - animation: pulse-icon 0.8s ease-in-out infinite; -} - -@keyframes pulse-icon { - 0%, 100% { opacity: 1; transform: scale(1); } - 50% { opacity: 0.6; transform: scale(0.9); } -} - -/* 小型加载指示器 */ -.spinner-small { - display: inline-flex; - align-items: center; - justify-content: center; - width: 1em; - height: 1em; - animation: spin 1s linear infinite; -} - -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -} - -.quick-action-icon { - font-size: 1.8em; - margin-bottom: 6px; - width: 44px; - height: 44px; - display: flex; - align-items: center; - justify-content: center; -} - -.quick-action-icon img { - max-width: 36px; - max-height: 36px; - object-fit: contain; -} - -.quick-action-name { - font-size: 0.8em; - font-weight: 500; - color: var(--text-color); - margin-bottom: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 100%; - line-height: 1.3; -} - -/* 设备面板分隔线 */ -.device-panel-divider { - height: 1px; - background: linear-gradient(90deg, transparent, var(--border) 20%, var(--border) 80%, transparent); - margin: 16px 0; -} - -/* 设备面板 + 风扇控制并排布局 */ -.panel-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin-bottom: 20px; -} - -/* 系统页:Resource Monitor / Device Panel / Fan Control / LED 卡片间距统一为 20px */ -.page-system .panel-row { - margin-top: 20px; - margin-bottom: 20px; -} -.page-system .panel-row .section { - margin-top: 0; /* 由 panel-row margin-top 提供与上方卡片的间距 */ -} -.page-system .panel-row + .section { - margin-top: 20px; /* LED 与 panel-row 的间距,覆盖 .section 默认 16px */ -} - -@media (max-width: 1200px) { - .panel-row { - grid-template-columns: 1fr; - } -} - -.fan-control-section { - min-width: 0; -} - -/* 风扇控制标题栏按钮:确保可点击、点击时蓝色底色(遵循文档 btn-service-style) */ -.fan-control-section .fan-section-actions { - position: relative; - z-index: 1; -} -.fan-refresh-btn { - min-width: 32px; - min-height: 32px; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid transparent; -} -.fan-refresh-btn:active { - background: var(--blue-50); - color: var(--blue-600); - border-color: transparent; -} - -/* 风扇温度状态栏 */ -.fan-temp-status-bar { - display: flex; - align-items: center; - gap: 16px; - padding: 12px 16px; - background: var(--bg-elevated); - border-radius: 10px; - margin-bottom: 16px; - flex-wrap: wrap; -} - -.temp-status-item { - display: flex; - align-items: center; - gap: 12px; -} - -.temp-status-item .temp-label { - font-size: 0.85rem; - color: var(--text-secondary); -} - -.temp-status-item .temp-value { - font-size: 1.1rem; - font-weight: 600; - color: var(--primary-color); - min-width: 50px; -} - -/* 测试温度控制 */ -.test-temp-control { - margin-left: auto; -} - -.test-temp-input-wrap { - display: flex; - align-items: center; - gap: 8px; -} - -.test-temp-input-wrap .input-sm { - padding: 4px 8px; - font-size: 0.85rem; - text-align: center; -} - -.test-temp-input-wrap .btn-sm { - padding: 4px 10px; - font-size: 0.75rem; -} - -/* nohup 后台任务卡片样式 */ -.quick-action-card.has-nohup { - /* 统一高度,不再额外增加 padding */ -} - -.quick-action-card.has-nohup.is-running { - border-color: var(--emerald-500); - background: linear-gradient(180deg, rgba(39, 174, 96, 0.05) 0%, transparent 50%); -} - -/* 状态指示器 - 右上角 */ -.nohup-status-badge { - position: absolute; - top: 6px; - right: 6px; - font-size: 10px; - line-height: 1; -} - -/* nohup 操作栏 - 底部固定 */ -.quick-action-nohup-bar { - position: absolute; - bottom: 0; - left: 0; - right: 0; - display: flex; - border-top: 1px solid var(--border-color); - border-radius: 0 0 11px 11px; - overflow: hidden; - background: var(--bg-color); -} - -.quick-action-nohup-bar button { - flex: 1; - padding: 6px 4px; - border: none; - background: transparent; - cursor: pointer; - font-size: 0.75em; - color: var(--text-color); - transition: background 0.15s; - display: flex; - align-items: center; - justify-content: center; - gap: 3px; -} - -.quick-action-nohup-bar button:hover { - background: var(--blue-100); -} - -.quick-action-nohup-bar button:first-child { - border-right: 1px solid var(--border-color); -} - -.quick-action-nohup-bar button.btn-stop { - color: var(--rose-500); -} - -.quick-action-nohup-bar button.btn-stop:hover { - background: var(--rose-50); -} - -.quick-action-nohup-bar button:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -.quick-action-nohup-bar button:disabled:hover { - background: transparent; -} - -.quick-actions-empty { - grid-column: 1 / -1; - text-align: center; - padding: 30px 20px; - color: var(--text-light); -} - -.quick-actions-empty span { - font-size: 2em; - display: block; - margin-bottom: 8px; -} - -.quick-actions-empty p { - margin: 0 0 4px; - font-size: 0.95em; -} - -.quick-actions-empty small { - font-size: 0.8em; - opacity: 0.7; -} - -.quick-actions-empty .empty-icon { - font-size: 48px; - margin-bottom: 12px; - opacity: 0.35; -} - -/* LPMu 监控面板 */ -/* ==================== 数据监控面板组件 ==================== */ - -.data-widgets-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 12px; - margin-top: 10px; /* 与 quick-actions-grid 的 padding-bottom 对齐,使分隔线上下间距一致 */ -} - -@media (max-width: 1200px) { - .data-widgets-grid { - grid-template-columns: repeat(3, 1fr); - } -} - -@media (max-width: 900px) { - .data-widgets-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 600px) { - .data-widgets-grid { - grid-template-columns: 1fr; - } -} - -.data-widgets-empty { - text-align: center; - padding: 30px 20px; - color: var(--text-secondary); -} - -.data-widgets-empty .empty-icon { - font-size: 48px; - margin-bottom: 12px; - opacity: 0.35; -} - -.data-widgets-empty p { - margin: 0 0 4px; - font-size: 0.95em; -} - -/* 组件卡片 */ -.dw-card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 12px; - text-align: center; - transition: all 0.2s; - display: flex; - flex-direction: column; - min-height: 160px; -} - -.dw-card:hover { - border-color: var(--blue-500); - box-shadow: 0 2px 8px var(--blue-100); -} - -/* 长按拖拽排序:等待中、拖拽中、插入位置指示 */ -.drag-pending { - outline: 2px solid var(--blue-500) !important; - outline-offset: 2px; - opacity: 0.8; - transition: opacity 3s linear; -} -.drag-active-source { - opacity: 0.25 !important; -} -.drag-over-before { - border-top: 2px solid var(--blue-500) !important; -} - -.dw-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.dw-card-label { - font-size: 0.85em; - font-weight: 500; - font-style: normal; - color: var(--text-secondary); -} - -.dw-card-actions { - display: flex; - gap: 2px; - opacity: 0; - transition: opacity 0.2s; -} - -.dw-card:hover .dw-card-actions { - opacity: 1; -} - -.dw-btn-icon { - background: none; - border: none; - padding: 2px 4px; - cursor: pointer; - font-size: 12px; - opacity: 0.6; - transition: opacity 0.2s; -} - -.dw-btn-icon:hover { - opacity: 1; -} - -.dw-card-var { - font-size: 0.7em; - color: var(--text-secondary); - background: var(--bg-muted); - padding: 4px 8px; - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.2s; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-top: auto; -} - -.dw-card-var:hover { - background: var(--blue-500); - color: white; -} - -.dw-card-var.bound { - color: var(--blue-500); - background: var(--blue-100); -} - -/* 环形进度条 */ -.dw-ring-container { - position: relative; - width: 85px; - height: 85px; - margin: 4px auto; -} - -.dw-ring { - width: 100%; - height: 100%; - transform: rotate(-90deg); -} - -.dw-ring-bg { - fill: none; - stroke: var(--border); - stroke-width: 8; -} - -.dw-ring-progress { - fill: none; - stroke-width: 8; - stroke-linecap: round; - stroke-dasharray: 264; - stroke-dashoffset: 264; - transition: stroke-dashoffset 0.5s ease; -} - -.dw-ring-value { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 1.2em; - font-weight: 600; - color: var(--text-primary); -} - -/* 仪表盘 */ -.dw-gauge-container { - position: relative; - width: 90px; - height: 55px; - margin: 8px auto; -} - -.dw-gauge { - width: 100%; - height: 100%; -} - -.dw-gauge-bg { - fill: none; - stroke: var(--border); - stroke-width: 8; - stroke-linecap: round; -} - -.dw-gauge-progress { - fill: none; - stroke-width: 8; - stroke-linecap: round; - stroke-dasharray: 126; - stroke-dashoffset: 126; - transition: stroke-dashoffset 0.5s ease; -} - -.dw-gauge-value { - position: absolute; - bottom: 0; - left: 50%; - transform: translateX(-50%); - font-size: 1.1em; - font-weight: 600; - color: var(--text-primary); -} - -/* 温度计 */ -.dw-temp-container { - display: flex; - align-items: center; - justify-content: center; - gap: 10px; - height: 75px; - margin: 4px 0; -} - -.dw-temp-bar { - position: relative; - width: 22px; - height: 65px; - background: var(--bg-muted); - border-radius: 11px; - overflow: hidden; - border: 2px solid var(--border); -} - -.dw-temp-fill { - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 0%; - background: linear-gradient(to top, var(--blue-500), var(--emerald-500), var(--amber-500), var(--rose-500)); - border-radius: 0 0 9px 9px; - transition: height 0.5s ease; -} - -.dw-temp-scale { - position: absolute; - right: -26px; - top: 0; - bottom: 0; - display: flex; - flex-direction: column; - justify-content: space-between; - font-size: 0.55em; - color: var(--text-secondary); -} - -.dw-temp-value { - font-size: 1.4em; - font-weight: 600; - color: var(--text-primary); - min-width: 45px; -} - -/* 数字显示 */ -.dw-number-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 75px; - margin: 4px 0; -} - -.dw-number-icon { - font-size: 1.6em; - margin-bottom: 4px; - animation: dw-pulse 2s ease-in-out infinite; -} - -@keyframes dw-pulse { - 0%, 100% { opacity: 0.7; transform: scale(1); } - 50% { opacity: 1; transform: scale(1.05); } -} - -.dw-number-value { - display: flex; - align-items: baseline; - gap: 2px; -} - -.dw-number-num { - font-size: 1.6em; - font-weight: 700; -} - -.dw-number-unit { - font-size: 0.85em; - color: var(--text-secondary); - font-weight: 500; -} - -/* 进度条 */ -.dw-bar-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 75px; - margin: 4px 0; - padding: 0 8px; -} - -.dw-bar-track { - width: 100%; - height: 12px; - background: var(--bg-muted); - border-radius: var(--radius-sm); - overflow: hidden; - margin-bottom: 8px; -} - -.dw-bar-fill { - height: 100%; - width: 0%; - border-radius: var(--radius-sm); - transition: width 0.5s ease; -} - -.dw-bar-value { - font-size: 1.2em; - font-weight: 600; - color: var(--text-primary); -} - -/* 文本显示 */ -.dw-text-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 75px; - margin: 4px 0; -} - -.dw-text-icon { - font-size: 1.6em; - margin-bottom: 6px; - opacity: 0.7; -} - -.dw-text-value { - font-size: 1.1em; - font-weight: 500; - word-break: break-all; - max-width: 100%; -} - -/* 添加组件模态框 */ -.dw-add-section { - margin-bottom: 20px; -} - -.dw-add-section h4 { - font-size: 0.9em; - color: var(--text-secondary); - margin-bottom: 10px; - padding-bottom: 6px; - border-bottom: 1px solid var(--border); -} - -.dw-presets-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px; -} - -.dw-preset-item { - display: flex; - flex-direction: column; - align-items: center; - padding: 12px 8px; - background: var(--bg-card); - border-radius: var(--radius); - cursor: pointer; - transition: all 0.2s; - border: 1px solid transparent; -} - -.dw-preset-item:hover { - background: var(--bg-muted); - border-color: var(--blue-500); -} - -.dw-preset-icon { - font-size: 1.5em; - margin-bottom: 4px; -} - -.dw-preset-label { - font-size: 0.85em; - font-weight: 500; -} - -.dw-preset-type { - font-size: 0.7em; - color: var(--text-secondary); -} - -.dw-types-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.dw-type-item { - display: flex; - align-items: center; - gap: 12px; - padding: 10px 12px; - background: var(--bg-card); - border-radius: var(--radius); - cursor: pointer; - transition: all 0.2s; - border: 1px solid transparent; -} - -.dw-type-item:hover { - background: var(--bg-muted); - border-color: var(--blue-500); -} - -.dw-type-icon { - font-size: 1.4em; -} - -.dw-type-info { - flex: 1; -} - -.dw-type-name { - font-weight: 500; - margin-bottom: 2px; -} - -.dw-type-desc { - font-size: 0.75em; - color: var(--text-secondary); -} - -/* 表单行 */ -.form-row { - display: flex; - gap: 12px; -} - -.form-row .form-group { - flex: 1; -} - -/* ===================== 组件管理器样式 ===================== */ - -/* 加大管理器模态框 */ -.dw-manager-modal { - max-width: 900px !important; - width: 95%; -} - -.dw-manager-body { - display: flex; - gap: 20px; - padding: 0 !important; - min-height: 500px; -} - -.dw-manager-sidebar { - width: 280px; - flex-shrink: 0; - border-right: 1px solid var(--border); - padding: 16px; - background: var(--bg-muted); - overflow-y: auto; -} - -.dw-manager-section h4 { - font-size: 0.85em; - color: var(--text-secondary); - margin-bottom: 12px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.dw-manager-list { - display: flex; - flex-direction: column; - gap: 6px; -} - -.dw-manager-empty-list { - text-align: center; - padding: 30px 10px; - color: var(--text-secondary); - font-size: 0.9em; -} - -.dw-manager-item { - display: flex; - align-items: center; - flex-wrap: nowrap; - gap: 8px; - padding: 8px 10px; - padding-right: 80px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius-sm); - cursor: pointer; - transition: all 0.2s; - position: relative; - overflow: hidden; -} - -.dw-manager-item:hover { - border-color: var(--blue-500); -} - -.dw-manager-item.active { - border-color: var(--blue-500); - background: var(--blue-100); -} - -.dw-manager-item-icon { - flex-shrink: 0; - font-size: 1.1em; -} - -.dw-manager-item-label { - flex: 1; - min-width: 0; - font-weight: 500; - font-style: normal; - font-size: 0.9em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.dw-manager-item-type { - flex-shrink: 0; - white-space: nowrap; - font-size: 0.7em; - color: var(--text-secondary); - background: var(--bg-muted); - padding: 2px 6px; - border-radius: var(--radius-sm); -} - -.dw-manager-item-actions { - position: absolute; - right: 8px; - top: 50%; - transform: translateY(-50%); - display: flex; - gap: 2px; - opacity: 0; - background: transparent; - padding: 2px 4px; - border-radius: var(--radius-sm); - transition: opacity 0.2s; -} - -.dw-manager-item.active .dw-manager-item-actions { - background: transparent; -} - -.dw-manager-item:hover .dw-manager-item-actions { - opacity: 1; -} - -.dw-manager-item-actions .dw-btn-icon { - padding: 2px 4px; - font-size: 11px; - border-radius: var(--radius-sm); -} - -.dw-manager-item-actions .dw-btn-icon:hover { - background: var(--bg-muted); -} - -.dw-manager-item-actions .dw-btn-icon:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -.dw-manager-main { - flex: 1; - padding: 16px; - overflow-y: auto; -} - -.dw-manager-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: var(--text-secondary); - text-align: center; -} - -/* 添加面板 */ -.dw-add-panel h4 { - font-size: 0.9em; - color: var(--text-secondary); - margin-bottom: 12px; - border-bottom: 1px solid var(--border); - padding-bottom: 8px; -} - -.dw-presets-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); - gap: 10px; -} - -.dw-types-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 10px; -} - -.dw-type-card { - display: flex; - flex-direction: column; - align-items: center; - padding: 12px; - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: all 0.2s; - text-align: center; -} - -.dw-type-card:hover { - border-color: var(--blue-500); -} - -.dw-type-card .dw-type-icon { - font-size: 1.8em; - margin-bottom: 6px; -} - -.dw-type-card .dw-type-name { - font-weight: 500; - font-size: 0.9em; - margin-bottom: 4px; -} - -.dw-type-card .dw-type-desc { - font-size: 0.7em; - color: var(--text-secondary); -} - -/* 编辑面板 */ -.dw-edit-panel { - max-width: 500px; -} - -.dw-edit-panel .form-row { - align-items: flex-start; - gap: 16px; -} - -.dw-edit-panel .form-row .form-group { - min-width: 0; -} - - -/* 仅第一行(图标+颜色):图标占剩余宽度,颜色固定一列,避免拥挤不对齐 */ -.dw-edit-panel .form-row:first-of-type .form-group:first-child { - flex: 1; -} - -.dw-edit-panel .form-row:first-of-type .form-group:last-child { - flex: 0 0 100px; -} - -.dw-edit-panel .form-row:first-of-type .form-group:last-child .input-color, -.dw-edit-panel .input-color { - width: 36px !important; - height: 36px !important; - min-width: 36px !important; - max-width: 36px !important; - flex: 0 0 36px !important; -} -.dw-edit-panel .input-color::-webkit-color-swatch-wrapper { - padding: 0; - border-radius: calc(var(--radius-sm) - 2px); - overflow: hidden; -} -.dw-edit-panel .input-color::-webkit-color-swatch { - border: none; - border-radius: calc(var(--radius-sm) - 2px); -} - -.dw-edit-header { - margin-bottom: 16px; -} - -.dw-edit-type-badge { - display: inline-block; - padding: 4px 12px; - background: var(--blue-50); - color: var(--blue-600); - border: 1px solid transparent; - border-radius: 20px; - font-size: 0.85em; -} - -.dw-expression-group { - background: var(--bg-muted); - padding: 16px; - border-radius: var(--radius); - margin-top: 16px; -} - -.dw-expression-group label { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 10px; -} - -.dw-expression-group .badge { - font-size: 0.65em; - background: var(--blue-50); - color: var(--blue-600); - border: 1px solid transparent; - padding: 2px 6px; - border-radius: 3px; -} - -.dw-expression-input { - display: flex; - gap: 8px; -} - -.dw-expression-group .form-hint { - margin-top: 10px; -} - -.dw-expression-input .input { - flex: 1; -} - -.form-hint { - display: block; - font-size: 0.75em; - color: var(--text-secondary); - margin-top: 6px; -} - -.form-hint code { - background: var(--bg-card); - padding: 1px 4px; - border-radius: var(--radius-sm); - font-family: 'Consolas', monospace; -} - -.dw-edit-preview { - margin-top: 20px; - padding-top: 16px; - border-top: 1px solid var(--border); -} - -.dw-edit-preview label { - font-size: 0.85em; - color: var(--text-secondary); - margin-bottom: 8px; - display: block; -} - -.dw-preview-card { - background: var(--bg-muted); - padding: 12px; - border-radius: var(--radius); -} - -.dw-preview-card .dw-card { - max-width: 180px; - margin: 0 auto; -} - -.dw-edit-actions { - display: flex; - justify-content: space-between; - margin-top: 20px; - padding-top: 16px; - border-top: 1px solid var(--border); -} - -/* 新组件类型样式 */ - -/* 状态灯 */ -.dw-status-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 75px; -} - -.dw-status-light { - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--text-muted); - box-shadow: 0 0 15px currentColor; - margin-bottom: 8px; - transition: all 0.3s; -} - -.dw-status-label { - font-size: 0.9em; - font-weight: 500; - color: var(--text-primary); -} - -/* 图标状态 */ -.dw-icon-container { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 75px; -} - -.dw-icon-display { - font-size: 2.5em; - margin-bottom: 6px; - transition: all 0.3s; -} - -.dw-icon-label { - font-size: 0.85em; - color: var(--text-secondary); -} - -/* 双数值 */ -.dw-dual-container { - display: flex; - align-items: baseline; - justify-content: center; - height: 75px; - gap: 4px; -} - -.dw-dual-main { - font-size: 2em; - font-weight: 700; -} - -.dw-dual-sep { - font-size: 1.2em; - color: var(--text-secondary); - margin: 0 2px; -} - -.dw-dual-sub { - font-size: 1.2em; - color: var(--text-secondary); - font-weight: 500; -} - -.dw-dual-unit { - font-size: 0.9em; - color: var(--text-secondary); - margin-left: 4px; -} - -/* 百分比显示 */ -.dw-percent-container { - display: flex; - align-items: baseline; - justify-content: center; - height: 75px; -} - -.dw-percent-value { - font-size: 2.4em; - font-weight: 700; -} - -.dw-percent-symbol { - font-size: 1.4em; - font-weight: 500; - color: var(--text-secondary); - margin-left: 2px; -} - -/* 日志组件样式 */ -.dw-log-container { - height: 160px; - overflow-y: auto; - background: #1e293b; - border-radius: var(--radius-sm); - font-family: 'Consolas', 'Monaco', 'Courier New', monospace; - font-size: 11px; - line-height: 1.4; - padding: 8px; -} - -.dw-log-line { - white-space: nowrap; - padding: 3px 6px; - border-bottom: 1px solid rgba(255,255,255,0.05); - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; -} - -.dw-log-line:last-child { - border-bottom: none; -} - -/* 日志级别颜色 */ -.dw-log-error-line { color: var(--rose-500); } -.dw-log-warn-line { color: var(--amber-500); } -.dw-log-info-line { color: var(--blue-500); } -.dw-log-debug-line { color: var(--emerald-500); } - -.dw-log-loading, -.dw-log-empty, -.dw-log-error { - display: flex; - align-items: center; - justify-content: center; - height: 100%; - color: var(--text-muted); - font-family: inherit; - font-size: 13px; -} - -.dw-log-error { - color: var(--rose-500); -} - -/* 日志工具栏 */ -.dw-log-toolbar { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 0; - border-bottom: 1px solid rgba(0,0,0,0.1); - margin-bottom: 8px; -} - -/* 折叠时工具栏精简 */ -.dw-log-toolbar-collapsed { - border-bottom: none; - margin-bottom: 0; - padding: 2px 0; -} - -.dw-log-toolbar .btn-sm { - padding: 2px 8px; - font-size: 11px; -} - -.dw-log-collapse-btn { - min-width: 24px; - font-size: 9px !important; - padding: 2px 6px !important; -} - -.dw-log-status { - margin-left: auto; - font-size: 11px; - color: var(--text-muted); -} - -/* 日志折叠状态 */ -.dw-log-collapsed { - height: 0 !important; - min-height: 0 !important; - padding: 0 !important; - margin: 0 !important; - overflow: hidden; - border: none !important; -} - -.dw-log-container { - transition: height 0.2s ease-out; -} - -/* 日志组件卡片尺寸调整 */ -.dw-card .dw-log-container { - margin: 0 -12px -12px -12px; - border-radius: 0 0 var(--radius) var(--radius); - height: 200px; -} - -/* 布局宽度类 */ -.dw-layout-small { - grid-column: span 1; - min-width: 120px; -} -.dw-layout-medium { - grid-column: span 2; -} -.dw-layout-large { - grid-column: span 3; -} -.dw-layout-full { - grid-column: 1 / -1; /* 独占一整行 */ -} - -/* 日志组件独占一行时高度更大 */ -.dw-layout-full .dw-log-container { - height: 250px; -} - -/* 布局选项样式 */ -.dw-layout-options { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin-top: 6px; -} - -.dw-layout-option { - display: flex; - flex-direction: column; - align-items: center; - padding: 8px 12px; - border: 2px solid var(--border); - border-radius: var(--radius); - cursor: pointer; - transition: all 0.2s; - min-width: 70px; -} - -.dw-layout-option:hover { - border-color: var(--blue-500); - background: var(--blue-50); -} - -.dw-layout-option.active { - border-color: var(--blue-500); - background: var(--blue-50); -} - -.dw-layout-option input[type="radio"] { - display: none; -} - -.dw-layout-label { - font-weight: 600; - font-size: 14px; -} - -.dw-layout-desc { - font-size: 11px; - color: var(--text-secondary); - margin-top: 2px; -} - -/* 颜色输入框样式 */ -.input-color { - width: 60px; - height: 36px; - padding: 2px; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - box-shadow: var(--shadow-sm); - cursor: pointer; -} -.input-color::-webkit-color-swatch-wrapper { - padding: 0; - border-radius: calc(var(--radius-sm) - 2px); - overflow: hidden; -} -.input-color::-webkit-color-swatch { - border: none; - border-radius: calc(var(--radius-sm) - 2px); -} - -/* 按钮块级 */ -.btn-block { - display: block; - width: 100%; -} - -/* ========================================================================= - TianshanOS UI/UX Updates - RemixIcon & Unified Styles - ========================================================================= */ - -/* RemixIcon 图标样式 */ -.ri-question-line, -.ri-service-line, -.ri-restart-line, -.ri-download-cloud-line, -.ri-refresh-line, -.ri-time-line, -.ri-usb-line, -.ri-information-line, -.ri-add-line, -.ri-upload-line, -.ri-download-line, -.ri-rocket-line, -.ri-alert-line, -.ri-error-warning-line, -.ri-lock-unlock-line, -.ri-close-line, -.ri-eye-line, -.ri-delete-bin-line { - font-style: normal; - line-height: 1; - display: inline-block; - vertical-align: middle; -} - -/* 服务徽章统一样式 */ -.service-badge, -.btn-service-style { - background: var(--blue-50) !important; - color: var(--blue-600) !important; - border-radius: var(--radius-sm); - padding: 4px 12px; - border: 1px solid transparent; -} - -.btn-service-style:hover { - background: var(--blue-100) !important; - border-color: transparent; -} - -/* 深灰色文本(用于服务状态) */ -.text-gray { - color: var(--text-secondary) !important; -} - -/* 健康状态图标颜色 */ -.health-ok { color: var(--emerald-500); } -.health-fail { color: var(--rose-500); } - -/* ========================================================================= - * SSH 指令 - 服务模式样式 - * ========================================================================= */ - -/* 服务模式选项区域 */ -.service-mode-options { - margin-top: 15px; - padding: 12px; - background: var(--blue-50); - border: 1px dashed var(--primary-color); - border-radius: 8px; -} - -.service-mode-header { - margin-bottom: 10px; -} - -.service-mode-header .checkbox-label { - font-weight: 500; -} - -.service-mode-header small { - display: block; - margin-top: 4px; - color: var(--text-secondary); -} - -.service-mode-fields { - margin-top: 12px; - padding-top: 12px; - border-top: 1px solid var(--blue-100); -} - -.service-mode-fields .form-row { - display: flex; - gap: 12px; -} - -.service-mode-hint { - margin-top: 12px; - padding: 10px; - background: var(--amber-50); - border-radius: 6px; - font-size: 0.85em; -} - -.service-mode-hint code { - background: rgba(0, 0, 0, 0.1); - padding: 2px 6px; - border-radius: 3px; - font-family: monospace; -} - -/* 服务模式标签(指令卡片中) */ -.pattern-tag.service-mode { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 2px 8px; - border-radius: 12px; - font-size: 0.8em; - background: var(--blue-100); - color: var(--primary-color); -} - -.pattern-tag.service-mode .service-status { - font-weight: 600; - font-size: 0.9em; -} - -/* 服务模式状态显示(简洁样式,无色块背景) */ -.service-mode-status { - display: inline-flex; - align-items: center; - font-size: 0.85em; - padding: 2px 6px; - border-radius: 4px; -} - -.service-mode-status .service-status { - font-weight: 500; -} - -/* 服务状态颜色(仅文字颜色) */ -.service-status.status-ready { - color: var(--emerald-600); -} - -.service-status.status-checking { - color: var(--blue-500); - animation: service-checking-text 1.5s ease-in-out infinite; -} - -.service-status.status-timeout { - color: var(--amber-500); -} - -.service-status.status-failed { - color: var(--rose-500); -} - -.service-status.status-idle { - color: var(--text-secondary); -} - -.service-status.status-unknown { - color: var(--text-muted); -} - -@keyframes service-checking-text { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -@keyframes service-checking { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } -} - -/* ========================================================================= - * 快捷操作卡片 - 服务状态显示 - * ========================================================================= */ */ - -.quick-action-service-status { - position: absolute; - bottom: 38px; /* 在操作栏上方 */ - left: 0; - right: 0; - display: flex; - align-items: center; - justify-content: center; - gap: 4px; - padding: 4px 8px; - font-size: 0.75em; - background: transparent; - border-top: 1px solid rgba(0, 0, 0, 0.08); -} - -.quick-action-service-status .service-label { - opacity: 0.7; -} - -.quick-action-service-status .service-value { - font-weight: 600; -} - -.quick-action-service-status.status-ready { - background: transparent; - color: var(--emerald-600); -} - -.quick-action-service-status.status-checking { - background: transparent; - color: var(--blue-500); - animation: service-checking 1.5s ease-in-out infinite; -} - -.quick-action-service-status.status-timeout { - background: transparent; - color: var(--amber-500); -} - -.quick-action-service-status.status-failed { - background: transparent; - color: var(--rose-500); -} - -.quick-action-service-status.status-idle { - background: transparent; - color: var(--text-secondary); -} - -/* 有服务模式的卡片样式 */ -.quick-action-card.has-service.is-running { - /* 统一高度,不再额外增加 padding */ +/* TianshanOS WebUI · macOS 27 / Liquid Glass(浅色) + 令牌与组件取自 docs/macos27/design-system(tokens.json、boards/spec.css)。 + 写法约束(tools/minify_web.py 是简易压缩器):选择器与伪类之间不留空格(写 .a *:hover 而不是 .a :hover); + content 字符串里不放 : , ; > ~ 前后的空格;不在字符串或 url() 里写注释符。 + CRITICAL 区会被 tests/macos27-r2/critical.py 内联进 index.html(首屏用),改这一区后要重新生成。 */ + +/*CRITICAL*/ +:root{ +--font:-apple-system,BlinkMacSystemFont,"SF Pro Text","PingFang SC","Segoe UI",sans-serif; +--display:"Quantify",var(--font); +--mono:"SF Mono",Menlo,monospace; +--ink:#1d1d1f;--ink-2:#4f5761;--ink-3:#5f6670; +--accent:#007aff;--accent-text:#0064d2;--accent-press:#0055b3;--accent-tint:rgba(0,122,255,.10); +--ok:#187030;--ok-dot:#34c759;--warn:#944b00;--warn-dot:#ff9500;--bad:#ad1b21;--bad-dot:#ff3b30; +--hair:rgba(0,0,0,.10);--hair-strong:rgba(0,0,0,.16);--fill:rgba(0,0,0,.045);--fill-2:rgba(0,0,0,.07); +--r-panel:32px;--r-card:16px;--r-inner:12px;--r-menu:16px;--r-item:10px;--r-ctl-sm:8px;--r-ctl:10px;--r-track:10px;--r-thumb:8px; +--wall:radial-gradient(ellipse at 12% 0%,#f8d9a4 0%,transparent 48%),radial-gradient(ellipse at 90% 12%,#c4dfef 0%,transparent 45%),linear-gradient(140deg,#b9cddc 0%,#e1d5bd 42%,#bbd7ef 73%,#9ec0e0 100%); +} +*{box-sizing:border-box} +html{-webkit-text-size-adjust:100%} +body{margin:0;background:#cddbe7;color:var(--ink);font:400 14px/20px var(--font);-webkit-font-smoothing:antialiased} +body::before{content:"";position:fixed;inset:0;z-index:-1;background-color:#cddbe7;background-image:var(--wall)} +h1,h2,h3,h4,p{margin:0} +a{color:var(--accent-text);text-decoration:none;cursor:pointer} +button,input,select,textarea{font:inherit;color:inherit} +button{cursor:default} +.hidden,[hidden]{display:none!important} +.m-bar{background:rgba(255,255,255,.60);-webkit-backdrop-filter:blur(24px) saturate(1.8);backdrop-filter:blur(24px) saturate(1.8);border:1px solid rgba(255,255,255,.62);box-shadow:inset 0 1px 0 rgba(255,255,255,.7),0 1px 2px rgba(0,0,0,.05)} +.m-surface{position:relative;isolation:isolate;border:1px solid transparent}.m-surface::before{content:"";position:absolute;inset:-1px;z-index:-1;border-radius:inherit;pointer-events:none;background:rgba(255,255,255,.70);-webkit-backdrop-filter:blur(32px) saturate(1.7);backdrop-filter:blur(32px) saturate(1.7);border:1px solid rgba(255,255,255,.7);box-shadow:inset 0 1px 0 rgba(255,255,255,.8),0 8px 30px rgba(20,40,70,.08)} +.m-float{background:rgba(250,251,253,.72);-webkit-backdrop-filter:blur(44px) saturate(1.9);backdrop-filter:blur(44px) saturate(1.9);border:1px solid rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.85),0 0 0 .5px rgba(0,0,0,.10),0 24px 60px rgba(20,30,50,.22)} +@media (prefers-reduced-transparency:reduce){.m-bar,.m-surface::before,.m-float{-webkit-backdrop-filter:none;backdrop-filter:none;background:#f5f5f7}} +@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){.m-bar,.m-surface::before,.m-float{background:#f5f5f7}} +.t-label{font-size:13px;line-height:18px;font-weight:400;color:var(--ink-2)} +.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:34px;padding:0 14px;border-radius:var(--r-ctl);border:1px solid var(--hair-strong);background:rgba(255,255,255,.85);color:var(--ink);font:500 14px/1 var(--font);white-space:nowrap;cursor:default} +.btn:hover{background:#fff} +.btn:active{background:var(--fill-2)} +.btn.sm{height:28px;padding:0 10px;font-size:13px;gap:5px;border-radius:var(--r-ctl-sm)} +.btn.quiet{background:transparent;border-color:transparent;color:var(--accent-text)} +.state{display:inline-flex;align-items:center;gap:6px;font-size:13px;line-height:18px;color:var(--ink-2);font-weight:400} +.state::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--ink-3);flex:none} +.state.ok::before,.state.connected::before{background:var(--ok-dot)} +.state.ok,.state.connected{color:var(--ok)} +#app{min-height:100vh;display:flex;flex-direction:column} +.header{position:fixed;top:20px;left:max(24px,calc((100% - 1920px)/2));right:max(24px,calc((100% - 1920px)/2));height:60px;z-index:100;border-radius:30px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;padding:0 16px} +.brand{display:flex;align-items:flex-start;gap:9px;justify-self:start;margin-left:6px;height:30px} +.brand .wm{margin-top:5.4px} +.nav{display:flex;gap:2px;height:40px;padding:3px;border-radius:999px;background:rgba(255,255,255,.45);border:1px solid rgba(255,255,255,.6)} +.nav a{display:block;height:32px;line-height:32px;padding:0 14px;border-radius:999px;font-size:14px;font-weight:500;color:var(--ink-2);text-decoration:none} +.nav a.active{background:#fff;color:var(--ink);box-shadow:0 1px 2px rgba(0,0,0,.14),0 0 0 .5px rgba(0,0,0,.06)} +.user-menu{justify-self:end;display:flex;align-items:center;gap:14px} +.main{flex:1;display:flex;flex-direction:column;margin:100px max(24px,calc((100% - 1920px)/2)) 0;padding:16px;border-radius:var(--r-panel)} +#page-content{display:flex;flex-direction:column;gap:16px;flex:1} +.foot{font-size:13px;line-height:18px;color:var(--ink-2);text-align:center} +footer.foot{padding:19px 0 18px} +.loading{display:flex;align-items:center;justify-content:center;gap:10px;padding:60px 0;color:var(--ink-3);font-size:12px;line-height:16px} +.spin{display:inline-block;width:16px;height:16px;border-radius:50%;border:2px solid var(--fill-2);border-top-color:var(--accent-text);animation:spin .8s linear infinite} +@keyframes spin{to{transform:rotate(360deg)}} +.lang-notice{position:fixed;top:100px;left:24px;right:24px;z-index:1100;display:flex;align-items:center;gap:12px;padding:16px;border-radius:var(--r-menu);font-size:14px} +.modal{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;background:rgba(20,30,45,.20)} +@media (max-width:1200px){ +.header{top:12px;left:12px;right:12px;height:auto;padding:12px 14px;border-radius:28px;grid-template-columns:1fr auto;gap:10px 12px} +.brand{margin-left:0} +.nav{grid-row:2;grid-column:1/-1;width:100%;overflow-x:auto;scrollbar-width:none} +.nav::-webkit-scrollbar{display:none} +.nav a{flex:none;white-space:nowrap} +.user-menu{gap:10px} +.main{margin:124px 12px 0;border-radius:28px} +} +@media (max-width:600px){ +#ws-status{font-size:0;gap:0} +#user-name{display:none} +.main{margin-top:118px;padding:10px;border-radius:22px} +} +/*END*/ + +@font-face{font-family:Quantify;font-weight:700;font-display:swap;src:url(/fonts/quantify-bold.woff2) format("woff2")} +:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(0,122,255,.38)} +code,pre,kbd,samp{font-family:var(--mono)} + +/* ===== 文字角色 ===== */ +.t-title{font-size:26px;line-height:32px;font-weight:600;letter-spacing:-.4px} +.t-section{font-size:15px;line-height:20px;font-weight:600;letter-spacing:-.2px} +.t-body{font-size:14px;line-height:20px;font-weight:400} +.t-note{font-size:12px;line-height:16px;font-weight:400;color:var(--ink-3)} +.t-value{font-size:14px;line-height:20px;font-weight:500;font-variant-numeric:tabular-nums} +.t-big{font-size:40px;line-height:44px;font-weight:300;letter-spacing:-1px;font-variant-numeric:tabular-nums} +.t-unit{font-size:14px;line-height:20px;color:var(--ink-2);font-weight:400} +.num{font-variant-numeric:tabular-nums} + +/* ===== 控件:三档 28 / 34 / 40 ===== */ +.btn.lg{height:40px;padding:0 20px;border-radius:999px} +.btn.primary{background:var(--accent-text);border-color:var(--accent-text);color:#fff} +.btn.primary:hover{background:var(--accent-press)} +.btn.danger,.btn.dg{color:var(--ink-2)} +.btn.danger:hover,.btn.dg:hover{color:var(--bad)} +.btn.icon{width:34px;padding:0} +.btn.icon.sm{width:28px} +.btn.round{border-radius:50%} +.btn:disabled,.btn.is-disabled{opacity:.4} +.btn.pw .i{color:var(--ink-3)} +.btn.pw.ok .i{color:var(--ok-dot)} +.btn.pw.bad .i{color:var(--bad-dot)} +.btn.pw.warn .i{color:var(--warn-dot)} +.pw .nm{color:var(--ink)} +.pw .st{font-weight:400;color:var(--ink-2)} +.pw.ok .st{color:var(--ok)} +.pw.bad .st{color:var(--bad)} +.dot{width:7px;height:7px;border-radius:50%;background:var(--ink-3);flex:none} +.dot.ok{background:var(--ok-dot)} +.dot.warn{background:var(--warn-dot)} +.dot.bad{background:var(--bad-dot)} +.switch{position:relative;width:40px;height:24px;border-radius:999px;background:var(--fill-2);border:0;padding:0;flex:none;cursor:default} +.switch::after{content:"";position:absolute;left:2px;top:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.3),0 0 0 .5px rgba(0,0,0,.06)} +.switch.on{background:var(--ok-dot)} +.switch.on::after{left:18px} +.i{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;flex:none} +.tile-i .i{width:24px;height:24px} +.t-value>.i{vertical-align:-3px} +.state.warn::before{background:var(--warn-dot)} +.state.bad::before{background:var(--bad-dot)} +.state.warn{color:var(--warn)} +.state.bad{color:var(--bad)} +.field{height:34px;padding:0 12px;border-radius:var(--r-ctl);border:1px solid var(--hair-strong);background:rgba(255,255,255,.9);font:400 14px/1 var(--font);color:var(--ink);min-width:0} +.field.sm{height:28px;font-size:13px;border-radius:var(--r-ctl-sm)} +.field.lg{height:40px;border-radius:12px} +.field:focus{outline:none;box-shadow:0 0 0 3px rgba(0,122,255,.38);border-color:var(--accent-text)} +input[type=number]{-moz-appearance:textfield;appearance:textfield} +input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0} +.mono{font-family:var(--mono);font-size:13px} +.seg{display:inline-flex;max-width:100%;padding:2px;border-radius:var(--r-track);background:var(--fill-2);gap:2px} +.seg button{min-width:0;height:28px;padding:0 14px;border:0;border-radius:var(--r-thumb);background:transparent;font:500 13px/1 var(--font);color:var(--ink-2)} +.seg button.on{background:#fff;color:var(--ink);box-shadow:0 1px 2px rgba(0,0,0,.14),0 0 0 .5px rgba(0,0,0,.06)} +.seg.full{display:flex;width:100%} +.seg.full button{flex:1;padding:0 6px} +.bar{height:6px;border-radius:999px;background:var(--fill-2);overflow:hidden} +.bar>i{display:block;height:100%;border-radius:inherit;background:var(--accent)} +.bar>i.warn{background:var(--warn-dot)} +.bar>i.bad{background:var(--bad-dot)} +.chk{width:16px;height:16px;border-radius:4px;border:1px solid var(--hair-strong);background:#fff;flex:none;display:inline-block} +.chk.on{background:var(--accent-text) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/100% 100% no-repeat;border-color:var(--accent-text)} +.radio{width:16px;height:16px;border-radius:50%;border:1px solid var(--hair-strong);background:#fff;display:inline-block;flex:none;position:relative} +.radio.on{border:5px solid var(--accent-text)} +.sel{position:relative;padding-right:30px} +.sel::after{content:"";position:absolute;right:12px;top:50%;width:6px;height:6px;border-right:1.5px solid var(--ink-3);border-bottom:1.5px solid var(--ink-3);transform:translateY(-70%) rotate(45deg)} +.tag{display:inline-block;font-size:12px;line-height:16px;padding:1px 6px;border-radius:5px;border:1px solid var(--hair-strong);color:var(--ink-2)} +.kbd{font:12px/16px var(--mono);padding:1px 6px;border-radius:5px;background:var(--fill-2)} +.dotc{width:22px;height:22px;border-radius:50%;border:0;box-shadow:inset 0 0 0 .5px rgba(0,0,0,.18);padding:0;flex:none} +.sw{width:30px;height:30px;border-radius:50%;box-shadow:inset 0 0 0 .5px rgba(0,0,0,.2);flex:none} +.sl{position:relative;flex:1;height:4px;border-radius:2px;background:var(--fill-2)} +.sl>i{position:absolute;left:0;top:0;bottom:0;border-radius:2px;background:var(--accent)} +.sl>b{position:absolute;top:-8px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.3),0 0 0 .5px rgba(0,0,0,.08)} + +/* ===== 结构:卡片 / 键值行 / 表格行 / 分组表单 ===== */ +.card{background:var(--fill);border-radius:var(--r-card);padding:20px;min-width:0} +.inner{border-radius:var(--r-inner)} +.card-h{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px 12px;margin-bottom:16px;min-height:28px} +.card-h>.t-section{white-space:nowrap} +.card-h>.acts{margin-left:auto} +.kv{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:7px 0} +.kv>span:first-child{color:var(--ink-2);font-size:13px;line-height:18px} +.kv>span:last-child{font-size:14px;font-weight:500;text-align:right;font-variant-numeric:tabular-nums} +.sep{height:1px;background:var(--hair);border:0;margin:0} +.w-tile{background:rgba(255,255,255,.62);border-radius:var(--r-inner);padding:14px 16px;min-width:0} +.tile{display:flex;align-items:center;gap:12px;padding:12px 16px;border-radius:var(--r-inner);background:rgba(255,255,255,.62)} +.tile.on{box-shadow:inset 0 0 0 2px var(--accent)} +.tr{display:grid;grid-template-columns:var(--cols);gap:16px;align-items:center;min-height:44px;padding:0 16px;border-top:1px solid var(--hair);font-size:14px} +.tr.th{min-height:34px;border-top:0;font-size:12px;color:var(--ink-3)} +.tr .act{display:flex;gap:6px;justify-content:flex-end} +.tr.hov,.tr:hover{background:rgba(0,0,0,.035)} +.tr.rsel{background:rgba(0,122,255,.13);border-radius:8px;margin:0 4px;padding:0 12px} +.stat{display:grid;grid-template-columns:repeat(6,1fr)} +.stat>div{padding:4px 20px;border-left:1px solid var(--hair)} +.stat>div:first-child{border-left:0;padding-left:0} +.term{background:#1e1f2b;border-radius:var(--r-card);color:#e6e6ef;font:13px/20px var(--mono);padding:16px} +.sec-h{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:8px 0 12px;min-height:28px} +.gt{font-size:13px;line-height:18px;color:var(--ink-2);margin:18px 4px 6px} +.gt:first-child{margin-top:0} +.grp{background:rgba(255,255,255,.62);border-radius:var(--r-inner);padding:0 16px} +.grp>.row,.grp>.rows>.row{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:46px;border-top:1px solid var(--hair)} +.grp>.rows{display:contents} +.grp>.row:first-child,.grp>.rows:first-child>.row:first-child{border-top:0} +.row .rl{font-size:14px;line-height:20px;flex:none} +.row .rl small{display:block;font-size:12px;line-height:16px;color:var(--ink-3)} +.row .rc{display:flex;align-items:center;justify-content:flex-end;gap:8px;min-width:0} +.fl{display:grid;gap:6px} +.fl>label{font-size:13px;line-height:18px;color:var(--ink-2)} + +/* ===== 外壳补充 ===== */ +.foot .q{font-family:var(--display);font-weight:700} + +/* ===== 浮层:语言菜单 / Toast / 弹窗基础 ===== */ +.lang-switch{position:relative} +.lang-menu{position:absolute;top:100%;right:0;margin-top:6px;width:180px;padding:6px;border-radius:var(--r-menu);z-index:1000} +.lang-menu-item{height:32px;border-radius:var(--r-item);padding:0 10px 0 34px;display:flex;align-items:center;gap:8px;cursor:default} +.lang-menu-item.active{padding-left:10px} +.lang-menu-item:hover{background:var(--accent-text);color:#fff} +.toast{position:fixed;bottom:80px;left:50%;z-index:9999;display:flex;align-items:center;gap:10px;width:max-content;max-width:min(560px,calc(100vw - 48px));padding:12px 16px;border-radius:var(--r-menu);font-size:14px;line-height:20px;background:rgba(250,251,253,.72);-webkit-backdrop-filter:blur(44px) saturate(1.9);backdrop-filter:blur(44px) saturate(1.9);border:1px solid rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.85),0 0 0 .5px rgba(0,0,0,.10),0 24px 60px rgba(20,30,50,.22);opacity:0;pointer-events:none;transform:translate(-50%,16px);transition:opacity .2s,transform .2s} +.toast.show{opacity:1;transform:translate(-50%,0)} +.toast::before{content:"";width:7px;height:7px;border-radius:50%;background:var(--ink-3);flex:none} +.toast-success::before{background:var(--ok-dot)} +.toast-warning::before{background:var(--warn-dot)} +.toast-error::before{background:var(--bad-dot)} +.sheet{border-radius:32px;padding:24px} +.sheet .sh{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px} +.sheet .st{font-size:20px;line-height:26px;font-weight:600;letter-spacing:-.3px} +.sheet .sf{display:flex;justify-content:flex-end;gap:8px;margin-top:20px} +.modal-content{position:relative;width:calc(100% - 32px);max-width:600px;max-height:90vh;overflow-y:auto;padding:24px;border-radius:32px;background:rgba(250,251,253,.72);-webkit-backdrop-filter:blur(44px) saturate(1.9);backdrop-filter:blur(44px) saturate(1.9);border:1px solid rgba(255,255,255,.75);box-shadow:inset 0 1px 0 rgba(255,255,255,.85),0 0 0 .5px rgba(0,0,0,.10),0 24px 60px rgba(20,30,50,.22)} + +/* ===== 页面公共布局 ===== */ +.page{display:flex;flex-direction:column;gap:16px;flex:1;min-width:0} +.cols{display:grid;grid-template-columns:minmax(0,1fr) 1px minmax(0,1fr);gap:16px} +.cols>*{min-width:0} +.cols.g20{gap:20px} +.sep.v{width:1px;height:auto} +.acts{display:flex;flex-wrap:wrap;gap:8px} +.acts.end{justify-content:flex-end;margin-top:14px} +.between{display:flex;align-items:center;justify-content:space-between} +.bigrow{display:flex;align-items:baseline;gap:4px;margin:6px 0 10px} +.inl{display:inline-flex;align-items:center;gap:10px} + +/* ===== 页面:系统 ===== */ +.sys-top{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px} +.sys-top>:last-child:nth-child(odd){grid-column:1/-1} +@media (min-width:1420px){.sys-top{grid-template-columns:5fr 6fr 5fr}.sys-top>:last-child:nth-child(odd){grid-column:auto}} +/* 系统总览等卡片:标签不折行不收缩;值单行,放不下时省略号,悬停后可横向滚动(完整内容在 title 里) */ +.sys-top .kv>:first-child{flex:none;white-space:nowrap} +.sys-top .kv>:last-child{min-width:0} +.sys-top .kv>span:last-child:not(.inl){overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.sys-top .kv>span:last-child:not(.inl):hover{overflow-x:auto;text-overflow:clip;scrollbar-width:none} +.sys-top .kv>span:last-child:not(.inl):hover::-webkit-scrollbar{display:none} +.meter{display:grid;gap:6px;padding:6px 0} +.meter .kv{padding:0} +.mblock{display:grid;gap:4px;margin-top:14px} +.mblock.first{margin-top:8px} +.mblock .kv{padding:4px 0} + +/* ===== 页面:系统(设备面板 / 风扇 / LED) ===== */ +.acts{align-items:center} +.sys-mid{display:grid;grid-template-columns:minmax(0,1fr) clamp(340px,24vw,420px);gap:16px;align-items:stretch;flex:none} +.sys-mid>.card{display:flex;flex-direction:column} +.card.in{background:rgba(255,255,255,.62);border-radius:var(--r-inner)} +.empty{padding:20px 0;text-align:center;color:var(--ink-3)} +.qa-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:12px} +.quick-action-card{display:flex;flex-direction:column;justify-content:space-between;gap:6px;background:rgba(255,255,255,.62);border-radius:var(--r-inner);padding:12px 14px;min-width:0;cursor:default} +.quick-action-card[data-allowed=true]:hover{background:rgba(255,255,255,.8)} +.quick-action-head,.quick-action-foot{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0} +.quick-action-name{min-width:0;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:14px;line-height:20px;font-weight:500} +.quick-action-card small{display:flex;align-items:center;min-width:0;flex:1 1 auto;overflow:hidden;white-space:nowrap;font-size:12px;line-height:16px;color:var(--ink-3)} +.quick-action-service-status{display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:100%} +.quick-action-service-status::before{content:"";flex:none;width:8px;height:8px;border-radius:50%;background:var(--ink-3);opacity:.55} +.quick-action-service-status .service-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:inherit} +.quick-action-card[data-state=running] .quick-action-service-status::before{background:var(--ok-dot);opacity:1} +.quick-action-card[data-state=starting] .quick-action-service-status::before,.quick-action-card[data-state=stopping] .quick-action-service-status::before{background:var(--warn-dot);opacity:1} +.quick-action-card[data-state=error] .quick-action-service-status::before,.quick-action-card[data-state=failed] .quick-action-service-status::before{background:var(--bad-dot);opacity:1} +.quick-action-card[data-state=running] .quick-action-service-status{color:var(--ok)} +.quick-action-card[data-state=error] .quick-action-service-status,.quick-action-card[data-state=failed] .quick-action-service-status{color:var(--bad)} +.quick-action-nohup-bar{display:flex;flex:none;gap:8px} +.dw-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:12px} +.dw-card>.t-label{display:flex;align-items:center;gap:6px} +.dw-card[data-type=log][data-layout=auto]{grid-column:span 2} +.dw-layout-medium{grid-column:span 2} +.dw-layout-large{grid-column:span 3} +.dw-layout-full{grid-column:1/-1} +.dw-ring{display:flex;justify-content:center;padding:8px 0 2px} +.bigrow.w32{margin:10px 0 0} +.w32 .t-big{font-size:32px;line-height:36px} +.dw-st{margin-top:14px} +.dw-ic{display:flex;align-items:center;gap:8px;margin-top:10px} +.dw-tx{margin-top:10px;font-weight:500;word-break:break-word} +.dw-log-h{display:flex;align-items:center;justify-content:space-between;gap:8px;flex-wrap:wrap} +.dw-log-tools{display:flex;gap:6px;align-items:center;flex-wrap:wrap} +.dw-log-container{margin-top:10px;max-height:220px;overflow:auto;background:#1e1f2b;border-radius:var(--r-inner);padding:10px 12px;color:#e6e6ef;font:12px/18px var(--mono)} +.dw-log-collapsed{display:none} +.dw-log-empty{color:#9a9ca8} +.dw-log-line{white-space:pre-wrap;word-break:break-all} +.dw-log-error-line{color:#ff8a8a} +.dw-log-warn-line{color:#ffcf7a} +.dw-log-info-line{color:#8ec5ff} +.dw-log-debug-line{color:#9a9ca8} +.fan-panel{min-width:0} +.fan-body{flex:1;display:flex;flex-direction:column;gap:10px} +.fan-body>.fan-test{margin-top:auto} +.fan-status{display:flex;align-items:center;flex-wrap:wrap;gap:6px 20px} +.fan-status .t-value{color:var(--ink)} +.fan-status .t-value.warn{color:var(--warn)} +.fan-test{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--hair)} +.fan-test .t-label{white-space:nowrap} +.fans-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:16px} +.fan-meta{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0 12px;height:32px;margin-top:8px} +.fan-stat{display:flex;flex-direction:column;min-width:0} +.fan-stat i{font-style:normal;font-size:11px;line-height:14px;color:var(--ink-3);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.fan-stat b{font-weight:500;font-size:13px;line-height:18px;color:var(--ink-2);font-variant-numeric:tabular-nums;white-space:nowrap} +.fan-big{gap:6px;margin:14px 0 0} +.fan-head{margin-bottom:10px} +.tti{display:inline-flex;align-items:center;height:18px;padding:0 6px;border:1px solid rgba(0,102,214,.28);border-radius:6px;background:rgba(0,122,255,.08);color:var(--accent-text);font:700 10.5px/1 var(--font);letter-spacing:.5px;cursor:default} +.tti:hover{background:rgba(0,122,255,.16)} +.tti:active{background:rgba(0,122,255,.24)} +.tti:focus-visible{outline:none;box-shadow:0 0 0 3px rgba(0,122,255,.38)} +.fan-readout{display:flex;align-items:flex-end;justify-content:space-between;gap:12px;margin-bottom:12px} +.fan-rpm{padding-bottom:6px;white-space:nowrap} +.fans-grid>.fan-card:only-child{background:none;border-radius:0;padding:0} +.fans-grid>.fan-card:only-child .fan-head .t-section{display:none} +.fans-grid>.fan-card:only-child .fan-head{margin-bottom:0} +.fans-grid>.fan-card:only-child .t-big{font-size:52px;line-height:56px} +.fans-grid>.fan-card:only-child .fan-big{margin-top:6px} +.slrow{display:flex;align-items:center;gap:12px;margin-top:16px} +.slrow>.t-value{width:36px;text-align:right} +.sl>i{width:var(--p,0)} +.sl>b{left:calc(var(--p,0%) - 10px)} +.sl>input{position:absolute;left:0;right:0;top:-8px;width:100%;height:20px;margin:0;opacity:0;cursor:default;-webkit-appearance:none;appearance:none;background:transparent} +.sl>input::-webkit-slider-thumb{-webkit-appearance:none;width:0;height:20px} +.sl>input::-moz-range-thumb{width:0;height:20px;border:0} +.led-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px} +.led-device-card{padding:0;overflow:hidden;background:rgba(255,255,255,.62)} +.led-h{padding:16px 16px 12px;display:grid;gap:12px} +.led-c{padding:14px 16px;display:grid;gap:14px} +.led-br{display:flex;align-items:center;gap:12px} +.led-br>.i{color:var(--ink-3)} +.led-br>.t-value{width:32px;text-align:right} +.led-dots{display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.dotc.pick{position:relative;overflow:hidden;background:conic-gradient(#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00)} +.dotc.pick input{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;opacity:0;cursor:default} +.led-f{padding:10px 16px;display:flex;align-items:center;gap:8px} +.hdr-r{display:flex;align-items:center;gap:10px} + +/* ===== 页面:网络 ===== */ +.net-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:16px} +.net-tiles .tile{padding:16px} +.tile-i{color:var(--ink-2)} +.tile-m{flex:1} +.tile-m>div:last-child{margin-top:2px} +.net-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;flex:1} +.sec-t{font-size:17px} +.net-wifi{display:grid;gap:12px} +.svc{display:grid;gap:0} +.nat-row{display:flex;align-items:center;gap:16px;margin-top:12px} +.net-lpmu{display:flex;align-items:center;justify-content:space-between;padding:16px 20px} +.dhcp-iface-row{display:flex;align-items:center;gap:8px} +select.field{-webkit-appearance:none;appearance:none;padding-right:30px;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M.7 1.35 6 6.65l5.3-5.3' fill='none' stroke='%235f6670' stroke-width='1.5'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 9.3px top calc(50% + 1.05px)} +.sel{position:relative;padding-right:30px} +.sel::after{content:"";position:absolute;right:12px;top:50%;width:6px;height:6px;border-right:1.5px solid var(--ink-3);border-bottom:1.5px solid var(--ink-3);transform:translateY(-70%) rotate(45deg)} +button.field.sel{text-align:left;white-space:nowrap;text-overflow:ellipsis;cursor:default} +.switch:disabled{opacity:.4} + +/* ===== 页面:文件 ===== */ +input[type=checkbox]{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin:0;border-radius:4px;border:1px solid var(--hair-strong);background:#fff;flex:none;display:inline-block} +input[type=checkbox]:checked{background:var(--accent-text) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 8.5l3 3 6-7' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/100% 100% no-repeat;border-color:var(--accent-text)} +input[type=checkbox]:indeterminate{background:var(--accent-text) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 8h7' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round'/%3E%3C/svg%3E") center/100% 100% no-repeat;border-color:var(--accent-text)} +input[type=radio]{-webkit-appearance:none;appearance:none;width:16px;height:16px;margin:0;border-radius:50%;border:1px solid var(--hair-strong);background:#fff;flex:none;display:inline-block} +input[type=radio]:checked{border:5px solid var(--accent-text)} +.file-name .fn{display:flex;align-items:center;gap:8px} +.file-name.clickable{color:var(--accent-text);cursor:pointer} +.batch-toolbar{border-radius:22px;padding:8px 8px 8px 20px;display:flex;align-items:center;gap:12px;align-self:center} +.batch-toolbar.hidden{display:none} +.vdiv{width:1px;height:20px;background:var(--hair)} +.stor-st{display:flex;gap:20px;padding:0 4px} + +/* ===== 页面:终端 ===== */ +.terminal-box{flex:1;min-height:320px;display:flex;background:#1e1f2b;border-radius:var(--r-card);padding:16px;overflow:hidden} +.terminal-container{flex:1;min-width:0;min-height:0} +.terminal-help{display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px} +.terminal-help>span{display:contents} +.terminal-help code{font:12px/16px var(--mono);padding:1px 6px;border-radius:5px;background:var(--fill-2)} + +/* ===== 页面:自动化 ===== */ +.stat-v{font-size:24px;line-height:30px;font-weight:500} +.stat-st{display:inline-flex;align-items:center;gap:8px} +.stat-st i{width:9px;height:9px;border-radius:50%;background:var(--ink-3)} +.stat-st.ok{color:var(--ok)} +.stat-st.ok i{background:var(--ok-dot)} +.stat-st.warn{color:var(--warn)} +.stat-st.warn i{background:var(--warn-dot)} + +/* ===== 页面:指令 ===== */ +.host-selector{display:flex;gap:12px;flex-wrap:wrap} +.host-selector>.empty{flex:1 1 100%} +.host-card{cursor:default} +.cmd-code{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.cmd-name .tag{margin-left:4px} +.exec-card{display:grid;gap:12px} +.exec-card[style*="display:none"]{display:none} +.exec-result{margin:0;padding:12px;min-height:96px;max-height:420px;overflow:auto;white-space:pre-wrap;word-break:break-all} +.vsep{width:1px;background:var(--hair);margin:0 4px} +.wrap{flex-wrap:wrap} +.match-panel{background:rgba(255,255,255,.62);border-radius:var(--r-inner);padding:16px;display:grid;gap:12px} +.match-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px} +.match-value{word-break:break-all} + +/* ===== 页面:固件升级 ===== */ +.dis{display:flex;align-items:center;gap:8px;font-size:15px;font-weight:600;padding:12px 16px} +.ota-col{width:720px;max-width:100%;margin:0 auto;display:grid;gap:16px;align-content:start} +.ota-ver{display:flex;align-items:baseline;gap:12px} +.ota-vnum{font-size:20px;line-height:26px} +.ota-server{display:flex;align-items:center;gap:8px} +.ota-update-status{margin-top:16px;padding:14px 16px;border-radius:var(--r-inner);background:rgba(255,255,255,.62)} +.ota-section>summary{list-style:none;cursor:default} +.ota-section>summary::-webkit-details-marker{display:none} +.ota-section>summary::before{content:"\203A";display:inline-block;color:var(--ink-3);transition:transform .2s} +.ota-section[open]>summary::before{transform:rotate(90deg)} +.ota-partitions{padding:0 16px 16px;display:grid;grid-template-columns:1fr 1fr;gap:12px} +.partition-card{padding:16px;display:grid;gap:10px} +.ota-manual-body{padding:0 16px 16px;display:grid;gap:12px} +.ota-opts{display:flex;gap:20px;font-size:13px} +.ota-opts label{display:flex;gap:6px;align-items:center} +.ota-prog{display:flex;align-items:center;gap:12px} +.ota-prog-note{margin-top:6px} + +/* ===== 页面:安全 ===== */ +.page-security{gap:20px;padding:0 4px} +.hint{display:flex;gap:6px} +.cols-keys{--cols:1fr .7fr 1.5fr .6fr .6fr 330px} +.cols-hosts{--cols:1fr 1.1fr .5fr .8fr 1fr 250px} +.cols-known{--cols:1fr .5fr .8fr 2fr 1fr .6fr} +.ta-r{text-align:right} +.pwrow{display:flex;gap:12px;align-items:flex-end} +.pwrow>.btn{flex:0 0 190px;padding:0 12px} +.pwf{position:relative} +.pwf .field{width:100%;padding-right:40px} +.pwt{position:absolute;right:4px;top:4px;width:28px;height:28px;padding:0;border:0;background:none;color:var(--ink-3);display:flex;align-items:center;justify-content:center;cursor:pointer} +.pwt:hover{color:var(--ink-2)} +.pwt.active{background:none;box-shadow:none;color:var(--ink)} +.cert-lines{margin-top:8px;display:grid;gap:2px;font-size:13px;color:var(--ink-2)} +.cert-lines>:empty{display:none} +.kv-grid{display:grid;gap:0 24px;margin-top:8px} +.kv-grid.k4{grid-template-columns:repeat(4,1fr)} +.kv-grid.k2{grid-template-columns:repeat(2,1fr);margin-top:12px} +.kv-grid .kv>span:last-child{min-width:0;overflow-wrap:anywhere} +.form-error{color:var(--bad);font-size:12px;line-height:16px;margin-top:8px} +.row .rl small.form-error{color:var(--bad)} +.field.err{border-color:var(--bad);box-shadow:0 0 0 1px var(--bad)} + +/* ===== 弹窗(分组表单,见 Audit 十七 / NativeDialogs)===== */ +.modal>.sheet{position:relative;display:flex;flex-direction:column;max-width:calc(100% - 32px);max-height:calc(100% - 32px)} +.sheet>.sb{min-height:0;overflow-y:auto;margin:0 -24px;padding:0 24px} +.sheet>.sb>.gt:first-child{margin-top:2px} +.gw>.gt:first-child{margin-top:18px} +.xbtn{border-color:transparent;background:var(--fill-2)} +.btn.primary.bad{background:var(--bad);border-color:var(--bad)} +.btn.primary.bad:hover{background:var(--bad);filter:brightness(.9)} +input[type=checkbox].switch{-webkit-appearance:none;appearance:none;position:relative;width:40px;height:24px;margin:0;border:0;border-radius:999px;background:var(--fill-2);flex:none;display:inline-block;cursor:default} +input[type=checkbox].switch::after{content:"";position:absolute;left:2px;top:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.3),0 0 0 .5px rgba(0,0,0,.06)} +input[type=checkbox].switch:checked{background:var(--ok-dot);border:0} +input[type=checkbox].switch:checked::after{left:18px} +input[type=checkbox].switch:disabled{opacity:.4} +textarea.field{height:auto;padding:8px 12px;width:100%;resize:vertical}textarea.field:not(:placeholder-shown){line-height:1.4;padding-block:5.4px} +.grp>.row.off{opacity:.4} +.result-box{font-size:13px;line-height:18px;overflow-wrap:anywhere;color:var(--ink-2)} +.result-box.success,.result-box.error,.result-box.warning{display:flex;gap:6px;align-items:flex-start;padding-top:2.5px} +.result-box.success::before,.result-box.error::before,.result-box.warning::before{content:"";flex:none;width:7px;height:7px;margin-top:5.5px;border-radius:50%} +.result-box.success{color:var(--ok)}.result-box.success::before{background:var(--ok-dot)} +.result-box.error{color:var(--bad)}.result-box.error::before{background:var(--bad-dot)} +.result-box.warning{color:var(--warn)}.result-box.warning::before{background:var(--warn-dot)} + +.warnrow{margin-top:12px;display:flex;gap:8px;align-items:center;color:var(--warn)} +.warnrow span{color:var(--ink)} +.cols-packs{--cols:1.5fr .6fr .9fr .5fr .6fr 56px;gap:8px;font-size:13px} +.rc span.mono{overflow-wrap:anywhere;text-align:right} +.dz{border:1.5px dashed var(--hair-strong);border-radius:16px;height:110px;display:grid;place-items:center;color:var(--ink-2);text-align:center;cursor:pointer} +.dz.drag-over{border-color:var(--accent-text);background:rgba(0,122,255,.06)} +.grp:empty{display:none} +.upload-item{gap:12px} +.upload-item>:first-child{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.upload-item>span:nth-child(2){color:var(--ink-3);font-size:12px;line-height:16px;white-space:pre-line;text-align:right} +.cvbox{height:230px;border-radius:var(--r-inner);background:rgba(255,255,255,.62);position:relative} +.cvbox canvas{position:absolute;inset:0;width:100%;height:100%} +.warn-t{color:var(--warn)} +.sheet.mem{padding:24px 28px 20px} +.sheet.mem>.sb{margin:0 -28px;padding:0 28px} +.sheet.mem .sh{margin-bottom:20px} +.sheet.mem .xbtn{width:28px;height:28px;font-size:13px} +.sheet.hl>.sf{margin-top:20px;padding-top:16px;border-top:1px solid var(--hair);align-items:center} +.mgrid{display:grid;gap:12px} +.mgrid.t-note{padding-bottom:8px} +.mgrid.t-body{padding:10px 0;border-top:1px solid var(--hair)} +.mtable{width:100%;border-collapse:collapse;font-size:14px} +.mtable th{font-size:12px;line-height:16px;font-weight:400;color:var(--ink-3);text-align:left;padding:0 12px 8px 0;white-space:nowrap} +.mtable td{padding:10px 12px 10px 0;border-top:1px solid var(--hair);font-variant-numeric:tabular-nums} +.mtable th.sortable{cursor:pointer} +.logv{height:200px;font-size:12px;line-height:18px;overflow-y:auto} +#modal-log-container{height:320px} +.log-entry{display:grid;grid-template-columns:70px 50px 100px 1fr;gap:8px;color:#8ee59a} +.log-tag{overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.log-message{overflow-wrap:anywhere} +.log-task{color:#9a9fb0} +.level-warn{color:#ffd98a} +.level-error{color:#ff9f9a} +.level-debug{color:#57c7c0} +.level-verbose{color:#9a9fb0} +.log-highlight{background:rgba(255,217,138,.3);color:inherit;border-radius:2px} +.log-empty{color:#9a9fb0;text-align:center;padding:70px 0} +.ws-status{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ink-3);flex:none} +.ws-status.connected{background:var(--ok-dot)} +button.tile{border:0;font:inherit;color:inherit;cursor:default;text-align:center} +input[type=color].swi{-webkit-appearance:none;appearance:none;width:30px;height:30px;padding:0;border:0;border-radius:50%;background:none;box-shadow:inset 0 0 0 .5px rgba(0,0,0,.2);flex:none;cursor:pointer} +input[type=color].swi::-webkit-color-swatch-wrapper{padding:0} +input[type=color].swi::-webkit-color-swatch{border:0;border-radius:50%} +.cols-wifi{--cols:1fr 90px 80px} +.cols-sta{--cols:1.2fr 1fr .6fr} +.cols-dhcp{--cols:1.2fr 1fr 1fr} +.icon-btn.selected{box-shadow:inset 0 0 0 2px var(--accent)} +.icon-image-preview{width:40px;height:40px;border-radius:var(--r-ctl);background:rgba(255,255,255,.62);display:grid;place-items:center;overflow:hidden;flex:none} +.icon-image-preview img{width:100%;height:100%;object-fit:contain} +.preview-placeholder{color:var(--ink-3);font-size:12px} +.var-list{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0 0} +.var-list:empty{display:none} + +/* ===== 响应式(Resp 稿:≥820 两列 → ≤1100 三列栅格收成两列(第三张独占一行),≤820 收紧,≤600 一列 / 底部 sheet / 表格横向滚动) ===== */ +.card:has(>.tr),.grp:has(>.tr){overflow-x:auto} +@media (max-width:1100px){ +.led-grid,.net-tiles{grid-template-columns:repeat(2,minmax(0,1fr))} +.led-grid>:last-child:nth-child(odd),.net-tiles>:last-child:nth-child(odd){grid-column:1/-1} +} +@media (max-width:1389px){ +.sys-mid{grid-template-columns:minmax(0,1fr)} +.fan-body{display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr);grid-template-areas:"fan status" "fan test";gap:16px 32px;align-content:start} +.fan-body>.fans-grid{grid-area:fan;max-width:560px} +.fan-body>.fan-status{grid-area:status;align-self:start} +.fan-body>.fan-test{grid-area:test;border-top:0;padding-top:0;align-self:end} +} +@media (max-width:700px){ +.fan-body{display:flex;flex-direction:column} +.fan-test{border-top:1px solid var(--hair);padding-top:14px} +} +@media (max-width:819px){ +.sys-top,.led-grid,.net-tiles{grid-template-columns:minmax(0,1fr)} +} +@media (max-width:600px){ +.modal>.sheet::before{content:"";display:block;flex:none;width:36px;height:5px;border-radius:3px;background:rgba(60,60,67,.3);margin:-8px auto 14px} +.sheet .sh{margin-bottom:12px} +.sheet .sf{margin-top:16px} +.between{flex-wrap:wrap;gap:8px 12px} +.pwrow{flex-wrap:wrap} +.pwrow>.fl{flex:1 1 100%} +.pwrow>.btn{flex:1 1 100%;width:100%} +#page-content [style*="grid-template-columns"]{grid-template-columns:1fr!important} +.tr{min-width:max-content} +.modal{align-items:flex-end} +.modal>.sheet{width:100%!important;max-width:100%;max-height:92%;border-radius:32px 32px 0 0;padding:20px 16px 24px} +.sheet>.sb,.sheet.mem>.sb{margin:0 -16px;padding:0 16px} +.sheet .sf{flex-direction:column-reverse} +.sheet .sf .btn{width:100%;justify-content:center} +.grp>.row{flex-wrap:wrap} +.row .rc{flex-wrap:wrap} +.field{max-width:100%} +#page-content .btn:not(.icon),.sheet .btn:not(.icon){max-width:100%;white-space:normal;height:auto;min-height:34px;padding-top:5px;padding-bottom:5px;line-height:1.25} +#page-content .btn.sm:not(.icon),.sheet .btn.sm:not(.icon){min-height:28px;padding-top:4px;padding-bottom:4px} +#page-content .btn.lg:not(.icon),.sheet .btn.lg:not(.icon){min-height:40px;padding-top:8px;padding-bottom:8px} +.row .rl{flex:1 1 auto;min-width:0;max-width:100%} +.row .rc{margin-left:auto} +.cols{grid-template-columns:1fr} +.cols>.sep{display:none} +.net-tiles{grid-template-columns:1fr} +.card-h{flex-wrap:wrap} +.stat{grid-template-columns:repeat(2,1fr);row-gap:12px} +.stat>div:nth-child(odd){border-left:0;padding-left:0} +.net-grid{grid-template-columns:1fr} +.led-f{flex-wrap:wrap} +.seg button{padding:0 8px} +.sys-top,.sys-mid,.fans-grid,.led-grid,.ota-partitions,.kv-grid.k2,.match-grid{grid-template-columns:1fr} +.qa-grid,.kv-grid.k4{grid-template-columns:repeat(2,1fr)} +.dw-grid{grid-template-columns:repeat(2,1fr)} } diff --git a/components/ts_webui/web/fonts/quantify-bold.woff2 b/components/ts_webui/web/fonts/quantify-bold.woff2 new file mode 100644 index 0000000..d02fa68 Binary files /dev/null and b/components/ts_webui/web/fonts/quantify-bold.woff2 differ diff --git a/components/ts_webui/web/fonts/remixicon.css b/components/ts_webui/web/fonts/remixicon.css deleted file mode 100644 index 69bba4d..0000000 --- a/components/ts_webui/web/fonts/remixicon.css +++ /dev/null @@ -1,145 +0,0 @@ -/* -* Remix Icon v4.9.1 (Minimal Build - TianshanOS) -* https://remixicon.com -* Only includes icons used in TianshanOS Web UI -*/ -@font-face { - font-family: "remixicon"; - src: url('/fonts/remixicon.woff2') format('woff2'); - font-display: swap; -} - -[class^="ri-"], [class*=" ri-"] { - font-family: 'remixicon' !important; - font-style: normal; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -/* Icon sizes */ -.ri-lg { font-size: 1.3333em; line-height: 0.75em; vertical-align: -.0667em; } -.ri-xl { font-size: 1.5em; line-height: 0.6666em; vertical-align: -.075em; } -.ri-xxs { font-size: .5em; } -.ri-xs { font-size: .75em; } -.ri-sm { font-size: .875em } -.ri-1x { font-size: 1em; } -.ri-2x { font-size: 2em; } -.ri-fw { text-align: center; width: 1.25em; } - -/* TianshanOS Icons - Only what we use */ -.ri-download-cloud-line:before { content: "\ec58"; } -.ri-question-line:before { content: "\f045"; } -.ri-refresh-line:before { content: "\f064"; } -.ri-restart-line:before { content: "\f080"; } -.ri-service-line:before { content: "\f0e2"; } -.ri-time-line:before { content: "\f20f"; } -.ri-usb-line:before { content: "\f252"; } -.ri-user-line:before { content: "\f264"; } -.ri-key-line:before { content: "\eea5"; } -.ri-lock-line:before { content: "\eef1"; } -.ri-login-box-line:before { content: "\eef7"; } -.ri-shield-user-line:before { content: "\f0da"; } -.ri-admin-line:before { content: "\ee03"; } -.ri-check-line:before { content: "\eb7b"; } -.ri-close-line:before { content: "\eb98"; } -.ri-door-open-line:before { content: "\ec4c"; } -.ri-checkbox-blank-circle-fill:before { content: "\eb7c"; } -.ri-apps-line:before { content: "\ea44"; } -.ri-line-chart-line:before { content: "\eeab"; } -.ri-temp-hot-line:before { content: "\f1f4"; } -.ri-dashboard-3-line:before { content: "\ec12"; } -.ri-scan-line:before { content: "\f0bd"; } -.ri-box-3-line:before { content: "\f2f5"; } -.ri-delete-bin-line:before { content: "\ec2a"; } -.ri-add-line:before { content: "\ea13"; } -.ri-arrow-right-line:before { content: "\ea6c"; } -.ri-link-unlink:before { content: "\eeb1"; } -.ri-network-line:before { content: "\f65e"; } -.ri-signal-wifi-3-line:before { content: "\f130"; } -.ri-broadcast-line:before { content: "\eaf9"; } -.ri-edit-line:before { content: "\ec86"; } -.ri-download-line:before { content: "\ec5a"; } -.ri-upload-line:before { content: "\f250"; } -.ri-folder-add-line:before { content: "\ed5a"; } -.ri-folder-line:before { content: "\ed6a"; } -.ri-home-line:before { content: "\ee2b"; } -.ri-toggle-line:before { content: "\f219"; } -.ri-toggle-fill:before { content: "\f218"; } -.ri-eject-line:before { content: "\ec88"; } -.ri-lightbulb-line:before { content: "\eea9"; } -.ri-lightbulb-fill:before { content: "\eea6"; } -.ri-lightbulb-flash-line:before { content: "\eea8"; } -.ri-information-line:before { content: "\ee59"; } -.ri-question-answer-line:before { content: "\f04e"; } -.ri-save-line:before { content: "\f0b3"; } -.ri-save-fill:before { content: "\f0b2"; } -.ri-shut-down-fill:before { content: "\f125"; } -.ri-shut-down-line:before { content: "\f126"; } -.ri-contrast-line:before { content: "\ebda"; } -.ri-palette-line:before { content: "\efc5"; } -.ri-hourglass-line:before { content: "\f339"; } -.ri-alert-line:before { content: "\ea21"; } -.ri-sun-line:before { content: "\f1bf"; } -.ri-stop-circle-line:before { content: "\f19f"; } -.ri-qr-code-line:before { content: "\f03d"; } -.ri-text:before { content: "\f201"; } -.ri-color-filter-line:before { content: "\f42e"; } - -/* Icons for Commands page icon picker */ -.ri-rocket-line:before { content: "\f096"; } -.ri-thunderstorms-line:before { content: "\f209"; } -.ri-tools-line:before { content: "\f21b"; } -.ri-bar-chart-line:before { content: "\ea9e"; } -.ri-search-line:before { content: "\f0d1"; } -.ri-stop-line:before { content: "\f1a1"; } -.ri-stop-fill:before { content: "\f1a0"; } -.ri-play-line:before { content: "\f00b"; } -.ri-settings-line:before { content: "\f0ee"; } -.ri-settings-3-line:before { content: "\f0e6"; } -.ri-run-line:before { content: "\f0a5"; } - -/* Host card icon */ -.ri-server-line:before { content: "\f0e0"; } - -/* Automation page icons */ -.ri-bar-chart-box-line:before { content: "\ea98"; } -.ri-upload-2-line:before { content: "\f24a"; } -.ri-pause-line:before { content: "\efd8"; } -.ri-play-circle-line:before { content: "\f009"; } -.ri-notification-line:before { content: "\ef9a"; } -.ri-plug-line:before { content: "\f019"; } -.ri-file-text-line:before { content: "\ed0f"; } -.ri-database-2-line:before { content: "\ec16"; } -.ri-global-line:before { content: "\edcf"; } -.ri-movie-line:before { content: "\ef81"; } -.ri-music-line:before { content: "\ef85"; } -.ri-smartphone-line:before { content: "\f15a"; } -.ri-computer-line:before { content: "\ebca"; } -.ri-shield-line:before { content: "\f108"; } -.ri-focus-line:before { content: "\ed4e"; } -.ri-timer-line:before { content: "\f215"; } -.ri-shield-keyhole-line:before { content: "\f107"; } -.ri-terminal-box-line:before { content: "\f1f6"; } -.ri-folder-open-line:before { content: "\ed70"; } -.ri-image-line:before { content: "\ee4b"; } - -/* Security page */ -.ri-lock-unlock-line:before { content: "\eed2"; } -.ri-eye-line:before { content: "\ecb5"; } -.ri-error-warning-line:before { content: "\eca1"; } -.ri-arrow-up-s-line:before { content: "\ea78"; } -.ri-checkbox-blank-circle-line:before { content: "\eb7d"; } - -/* Data widget manager - types and presets */ -.ri-dashboard-line:before { content: "\ec14"; } -.ri-progress-6-line:before { content: "\f485"; } -.ri-numbers-line:before { content: "\efaa"; } -.ri-record-circle-fill:before { content: "\f059"; } -.ri-emotion-line:before { content: "\ec90"; } -.ri-percent-line:before { content: "\efe6"; } -.ri-file-list-line:before { content: "\ecf1"; } -.ri-cpu-line:before { content: "\ebf0"; } -.ri-brain-line:before { content: "\f2f7"; } -.ri-hard-drive-line:before { content: "\edfb"; } -.ri-gamepad-line:before { content: "\edab"; } -.ri-ruler-line:before { content: "\f0a3"; } \ No newline at end of file diff --git a/components/ts_webui/web/fonts/remixicon.woff2 b/components/ts_webui/web/fonts/remixicon.woff2 deleted file mode 100644 index 5e86a2f..0000000 Binary files a/components/ts_webui/web/fonts/remixicon.woff2 and /dev/null differ diff --git a/components/ts_webui/web/index.html b/components/ts_webui/web/index.html index eea293c..3e57428 100644 --- a/components/ts_webui/web/index.html +++ b/components/ts_webui/web/index.html @@ -4,46 +4,11 @@ TianshanOS - - + + - - - + + @@ -51,207 +16,86 @@ +
-
- - +