// H5版本的小程序组件库
// 参考 alf/ss-components.js 的组件形式

(function () {
  const {
    ref,
    createApp,
    watch,
    inject,
    onMounted,
    onBeforeUnmount,
    computed,
  } = Vue;

  // ===== 公共上传函数 =====
  /**
   * 统一文件上传函数
   * @param {File|Blob} file - 文件或Blob对象
   * @param {String} type - 'image' | 'file'
   * @param {String} fileName - 文件名（可选）
   * @returns {Promise<String>} 服务器路径
   */
  async function uploadFile(file, type = "image", fileName) {
    try {
      window.showToast?.("上传中...", "loading");

      const formData = new FormData();
      const name = fileName || (file instanceof Blob ? "file" : file.name);
      formData.append("fileEdit", file, name);
      formData.append("application", "");

      // 根据类型选择接口参数
      const apiType = type === "image" ? "img" : "file";

      const result = await window.request.post(
        `/service?ssServ=ulByHttp&type=${apiType}`,
        formData,
        { loading: false }
      );

      if (result?.data?.fileList?.[0]?.path) {
        const serverPath = result.data.fileList[0].path;
        window.showToast?.("上传成功", "success");
        return serverPath;
      } else {
        throw new Error("上传返回数据格式错误");
      }
    } catch (error) {
      console.error("上传失败:", error);
      window.showToast?.("上传失败，请重试", "error");
      throw error;
    }
  }

  const SsCommonIcon = {
    name: "SsCommonIcon",
    props: {
      class: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      const { h } = Vue;
      return () =>
        h("i", {
          class: props.class + " common-icon",
        });
    },
  };

  // ss-input 智能输入组件
  const SsInput = {
    name: "SsInput",
    inheritAttrs: false, // 不直接继承属性到组件根元素
    props: {
      // v-model 绑定的值
      modelValue: {
        type: [String, Number],
        default: "",
      },
      // 字段名称
      name: {
        type: String,
        default: "",
      },
      // 占位符
      placeholder: {
        type: String,
        default: "请输入",
      },
      // 错误提示
      errTip: {
        type: String,
        default: "",
      },
    },
    emits: ["update:modelValue", "input", "blur", "change", "focus"],
    setup(props, { emit }) {
      const inputValue = ref(props.modelValue || "");
      const validationState = ref({
        hasError: false,
        errorMessage: "",
        isRequired: false,
        isEmpty: true,
        hasInteracted: false, // 是否已经交互过
        isSubmitMode: false, // 是否处于提交模式
      });

      // 从ValidatedTd注入事件处理函数（兼容小程序方式）
      const onInputInject = inject("onInput", null);
      const onBlurInject = inject("onBlur", null);

      // 检查是否为必填字段
      const checkRequired = () => {
        if (window.ssVm && props.name) {
          validationState.value.isRequired = window.ssVm.isRequired(props.name);
        }
      };

      // 更新父级td的class
      const updateTdClass = () => {
        // 找到父级td元素
        const inputElement = document.querySelector(
          `input[name="${props.name}"]`
        );
        if (inputElement) {
          const tdElement = inputElement.closest("td");
          if (tdElement) {
            // 移除所有校验相关的class
            tdElement.classList.remove("td-required", "td-error");

            // 逻辑分离：
            // 1. 初始状态：必填且为空 → 只有左侧红线 (td-required)
            // 2. 实时校验失败 → 左侧红线 + 底部红线 + 错误文字 (td-error)

            if (
              validationState.value.hasError &&
              validationState.value.hasInteracted
            ) {
              // 用户交互后校验失败：左侧红线 + 底部红线 + 错误文字
              tdElement.classList.add("td-error");
            } else if (
              validationState.value.isRequired &&
              validationState.value.isEmpty
            ) {
              // 必填且为空：只有左侧红线
              tdElement.classList.add("td-required");
            }
          }
        }
      };

      // 校验字段
      const validateField = (value) => {
        if (window.ssVm && props.name) {
          const result = window.ssVm.validateField(props.name);
          validationState.value.hasError = !result.valid;
          validationState.value.errorMessage = result.message || "";
          validationState.value.isEmpty = !value || value.trim() === "";

          // 更新td的class
          setTimeout(updateTdClass, 0);

          return result;
        }
        return { valid: true, message: "" };
      };

      // 监听props变化
      watch(
        () => props.modelValue,
        (newVal) => {
          inputValue.value = newVal;
          validateField(newVal);
        }
      );

      // 挂载时初始化
      onMounted(() => {
        checkRequired();
        validateField(inputValue.value);

        // 监听ssVm规则更新事件
        const inputElement = document.querySelector(
          `input[name="${props.name}"]`
        );
        if (inputElement) {
          inputElement.addEventListener("ssvm-rules-updated", () => {
            console.log(`收到规则更新通知: ${props.name}`);
            checkRequired();
            validateField(inputValue.value);
            updateTdClass();
          });
        }

        // 确保初始状态正确显示，延迟更长时间确保DOM完全渲染
        setTimeout(() => {
          updateTdClass();
        }, 200);
      });

      // 事件处理函数
      const handleInput = (event) => {
        const value = event.target.value;
        inputValue.value = value;

        // 标记已交互
        validationState.value.hasInteracted = true;

        // 1. 支持v-model
        emit("update:modelValue", value);
        emit("input", event);

        // 2. 内部校验（实时校验）
        validateField(value);

        // 3. 兼容小程序inject方式
        if (onInputInject) {
          onInputInject(event);
        }
      };

      const handleBlur = (event) => {
        // 标记已交互
        validationState.value.hasInteracted = true;

        emit("blur", event);

        // 失焦时再次校验
        validateField(inputValue.value);

        // 兼容小程序inject方式
        if (onBlurInject) {
          onBlurInject(event);
        }
      };

      const handleFocus = (event) => {
        emit("focus", event);
      };

      return {
        inputValue,
        validationState,
        handleInput,
        handleBlur,
        handleFocus,
      };
    },
    template: `
		<!-- 移动端简化的错误提示 - 显示在输入框右下角 -->
        <div v-if="validationState.hasError && validationState.hasInteracted" class="ss-input__error">
          {{ validationState.errorMessage }}
        </div>
      <div class="ss-input">
        <input
          class="ss-input__field"
          :name="name"
          type="text"
          :value="inputValue"
          :placeholder="placeholder"
          @input="handleInput"
          @blur="handleBlur"
          @focus="handleFocus"
          autocomplete="off"
          v-bind="$attrs"
        />

      </div>
    `,
  };

  // icon 图标组件 - 从小程序转换
  const Icon = {
    name: "Icon",
    props: {
      // 图标名称，对应iconfont中的类名，如'icon-home'
      name: {
        type: String,
        required: true,
      },
      // 图标颜色
      color: {
        type: String,
        default: "#000",
      },
      // 图标大小，单位rpx (H5中转换为px)
      size: {
        type: [Number, String],
        default: 32,
      },
    },
    emits: ["click"],
    setup(props, { emit }) {
      // 计算完整的图标类名
      const iconClass = computed(() => {
        return props.name;
      });

      // 计算样式
      const iconStyle = computed(() => ({
        color: props.color,
        fontSize: parseInt(props.size) / 2 + "px", // rpx转px，除以2
        verticalAlign: "middle",
      }));

      // 点击事件处理
      const handleClick = (event) => {
        emit("click", event);
      };

      return {
        iconClass,
        iconStyle,
        handleClick,
      };
    },
    template: `
			<span
				:class="['iconfont', iconClass]"
				:style="iconStyle"
				@click="handleClick"
			></span>
		`,
  };

  // ss-card 卡片组件 - 从小程序转换
  const SsCard = {
    name: "SsCard",
    props: {
      item: {
        type: Object,
        default: () => ({}),
      },
    },
    emits: ["click", "buttonClick"],
    setup(props, { emit }) {
      // 状态
      const showButtonMenu = ref(false);
      const swipeOffset = ref(0);
      const touchStartX = ref(0);
      const touchStartY = ref(0);
      const swipeOpening = ref(false);
      const isSwiping = ref(false);

      const swipeActions = computed(() => {
        return Array.isArray(props.item?.swipeActions)
          ? props.item.swipeActions.filter(Boolean)
          : [];
      });

      const hasSwipeActions = computed(() => swipeActions.value.length > 0);

      const swipeActionPalette = [
        { backgroundColor: "#357cdf", color: "#fff" },
        { backgroundColor: "#94bfff", color: "#000000" },
        { backgroundColor: "#e0edff", color: "#000000" },
      ];

      const actionPaneWidth = computed(() => {
        if (!hasSwipeActions.value) return 0;
        return 66 * swipeActions.value.length;
      });

      // 计算属性
      const hasButtons = computed(() => {
        return (
          props.item?.buttons &&
          Array.isArray(props.item.buttons) &&
          props.item.buttons.length > 0
        );
      });

      const isMultipleButtons = computed(() => {
        return props.item?.buttons && props.item.buttons.length > 1;
      });

      const isSingleButton = computed(() => {
        return props.item?.buttons && props.item.buttons.length === 1;
      });

      const cardContentStyle = computed(() => ({
        transform: `translateX(-${swipeOffset.value}px)`,
      }));

      const actionPaneStyle = computed(() => ({
        width: `${actionPaneWidth.value}px`,
      }));

      const swipeIndicatorStyle = computed(() => {
        const palette = getSwipeActionStyle(0) || {};
        return {
          backgroundColor: palette.backgroundColor || "#357cdf",
          opacity: swipeOffset.value > 0 ? 0 : 1,
        };
      });

      const getSwipeActionStyle = (index) => {
        const paletteIndex = Math.min(
          Number(index || 0),
          swipeActionPalette.length - 1
        );
        return swipeActionPalette[paletteIndex] || swipeActionPalette[0];
      };

      const closeSwipe = () => {
        swipeOffset.value = 0;
        swipeOpening.value = false;
      };

      const openSwipe = () => {
        if (!hasSwipeActions.value) return;
        swipeOffset.value = actionPaneWidth.value;
        swipeOpening.value = true;
      };

      // 处理卡片点击
      const handleCardClick = () => {
        if (isSwiping.value) {
          isSwiping.value = false;
          return;
        }

        if (swipeOpening.value) {
          closeSwipe();
          return;
        }

        // 如果菜单打开，先关闭菜单
        if (showButtonMenu.value) {
          showButtonMenu.value = false;
          return;
        }

        emit("click");
      };

      // 处理设置按钮点击
      const handleSettingClick = () => {
        if (isSingleButton.value) {
          // 只有一个按钮，直接执行
          handleButtonClick(props.item.buttons[0], 0);
        } else if (isMultipleButtons.value) {
          // 先记录当前状态
          const wasOpen = showButtonMenu.value;

          // 关闭其他卡片的菜单
          document.dispatchEvent(new CustomEvent("closeAllCardMenus"));

          // 切换当前菜单状态
          showButtonMenu.value = !wasOpen;
        }
      };

      // 关闭菜单
      const closeMenu = () => {
        showButtonMenu.value = false;
      };

      const handleTouchStart = (event) => {
        if (!hasSwipeActions.value) return;
        const touch = event.touches && event.touches[0];
        if (!touch) return;
        touchStartX.value = touch.clientX;
        touchStartY.value = touch.clientY;
        isSwiping.value = false;
        document.dispatchEvent(new CustomEvent("closeAllCardSwipes"));
      };

      const handleTouchMove = (event) => {
        if (!hasSwipeActions.value) return;
        const touch = event.touches && event.touches[0];
        if (!touch) return;

        const deltaX = touch.clientX - touchStartX.value;
        const deltaY = touch.clientY - touchStartY.value;

        if (Math.abs(deltaY) > Math.abs(deltaX) || Math.abs(deltaX) < 8) {
          return;
        }

        isSwiping.value = true;
        const nextOffset = swipeOpening.value
          ? actionPaneWidth.value - deltaX
          : -deltaX;
        swipeOffset.value = Math.max(
          0,
          Math.min(actionPaneWidth.value, nextOffset)
        );
      };

      const handleTouchEnd = () => {
        if (!hasSwipeActions.value) return;
        if (swipeOffset.value > actionPaneWidth.value / 2) {
          openSwipe();
        } else {
          closeSwipe();
        }

        setTimeout(() => {
          isSwiping.value = false;
        }, 0);
      };

      const handleCloseAllSwipes = () => {
        closeSwipe();
      };

      // 处理按钮点击
      const handleButtonClick = (btn, index) => {
        showButtonMenu.value = false;
        closeSwipe();

        // 执行按钮的回调
        if (btn.onclick && typeof btn.onclick === "function") {
          btn.onclick();
        }

        // 触发组件事件
        emit("buttonClick", { button: btn, index, item: props.item });
      };

      // 监听全局关闭事件
      onMounted(() => {
        document.addEventListener("closeAllCardMenus", closeMenu);
        document.addEventListener("closeAllCardSwipes", handleCloseAllSwipes);
      });

      onBeforeUnmount(() => {
        document.removeEventListener("closeAllCardMenus", closeMenu);
        document.removeEventListener(
          "closeAllCardSwipes",
          handleCloseAllSwipes
        );
      });

      // H5环境下的清理逻辑
      // 注意：Vue 3的beforeUnmount在某些H5环境下可能不可用
      // 这里暂时省略，依赖页面刷新时的自动清理

      return {
        showButtonMenu,
        swipeActions,
        hasSwipeActions,
        hasButtons,
        isMultipleButtons,
        isSingleButton,
        cardContentStyle,
        actionPaneStyle,
        swipeIndicatorStyle,
        getSwipeActionStyle,
        handleCardClick,
        handleSettingClick,
        handleTouchStart,
        handleTouchMove,
        handleTouchEnd,
        handleButtonClick,
      };
    },
    template: `
			<div class="ss-card" :class="{ 'ss-card--swipeable': hasSwipeActions }">
				<div
					v-if="hasSwipeActions"
					class="ss-card-swipe__indicator"
					:style="swipeIndicatorStyle"
				></div>
				<div v-if="hasSwipeActions" class="ss-card-swipe__actions" :style="actionPaneStyle">
					<!-- 功能说明：卡片左滑露出记录操作按钮（如“变动”），替代齿轮菜单入口 by xu 2026-03-06 -->
					<button
						v-for="(btn, index) in swipeActions"
						:key="index"
						class="ss-card-swipe__action"
						:style="getSwipeActionStyle(index)"
						@click.stop="handleButtonClick(btn, index)"
					>
						{{ btn.title || btn.text || btn.desc }}
					</button>
				</div>

				<div
					class="ss-card-swipe__content"
					:style="cardContentStyle"
					@touchstart="handleTouchStart"
					@touchmove="handleTouchMove"
					@touchend="handleTouchEnd"
				>
					<div class="ss-card-main" @click="handleCardClick">
						<!-- 右上角设置按钮 - 只有buttons且长度>0时显示 -->
						<div
							v-if="hasButtons && !hasSwipeActions"
							class="card-setting-header"
							@click.stop="handleSettingClick"
						>
							<div class="setting-icon">
								<Icon name="icon-chilun" size="32" color="#999"/>
							</div>

							<!-- 按钮弹窗菜单 - 只有多个按钮时显示 -->
							<div
								v-if="showButtonMenu && isMultipleButtons"
								class="button-menu"
								@click.stop
							>
								<div
									v-for="(btn, index) in item.buttons"
									:key="index"
									class="menu-item"
									@click="handleButtonClick(btn, index)"
								>
									<Icon v-if="btn.icon" :name="btn.icon" size="28" color="inherit"/>
									<span class="menu-text">{{ btn.title }}</span>
								</div>
							</div>
						</div>

						<!-- 卡片内容 -->
						<slot></slot>
					</div>
				</div>
			</div>
		`,
  };

  // ss-search-button 搜索按钮组件 - 从小程序转换
  const SsSearchButton = {
    name: "SsSearchButton",
    props: {
      // 按钮文本
      text: {
        type: String,
        default: "增加",
      },
      // 是否禁用
      disabled: {
        type: Boolean,
        default: false,
      },
      // 按钮高度
      height: {
        type: [String, Number],
        default: "36px",
      },
      // 前置图标名称
      preIcon: {
        type: String,
        default: "",
      },
      // 后置图标名称
      suffixIcon: {
        type: String,
        default: "",
      },
      // 图标大小
      iconSize: {
        type: [String, Number],
        default: "32",
      },
      // 图标颜色
      iconColor: {
        type: String,
        default: "#585d6e",
      },
      // 自定义按钮样式
      customStyle: {
        type: Object,
        default: () => ({}),
      },
      // 跳转链接（兼容原JSP用法）
      href: {
        type: String,
        default: "",
      },
      // 选项列表
      options: {
        type: Array,
        default: () => [],
      },
    },
    emits: ["click", "optionClick"],
    setup(props, { emit }) {
      // 状态
      const showOptionsMenu = ref(false);

      // 计算属性
      const hasOptions = computed(() => {
        return (
          props.options &&
          Array.isArray(props.options) &&
          props.options.length > 0
        );
      });

      const hasMultipleOptions = computed(() => {
        return props.options && props.options.length > 1;
      });

      const isSingleOption = computed(() => {
        return props.options && props.options.length === 1;
      });

      // 按钮样式
      const buttonStyle = computed(() => ({
        height:
          typeof props.height === "number" ? `${props.height}px` : props.height,
        ...props.customStyle,
      }));

      // 处理按钮点击
      const handleClick = () => {
        if (!hasOptions.value) {
          // 没有选项，直接触发点击事件
          emit("click");
        } else if (isSingleOption.value) {
          // 单个选项，直接执行
          handleOptionClick(props.options[0], 0);
        } else if (hasMultipleOptions.value) {
          // 先记录当前状态
          const wasOpen = showOptionsMenu.value;

          // 关闭其他按钮的菜单
          document.dispatchEvent(new CustomEvent("closeAllButtonMenus"));

          // 切换当前菜单状态
          showOptionsMenu.value = !wasOpen;
        }
      };

      // 处理选项点击
      const handleOptionClick = (option, index) => {
        showOptionsMenu.value = false;

        // 执行选项的回调
        if (option.onclick && typeof option.onclick === "function") {
          option.onclick();
        }

        // 触发组件事件
        emit("optionClick", { option, index });
      };

      // 关闭菜单
      const closeMenu = () => {
        showOptionsMenu.value = false;
      };

      // 监听全局关闭事件
      onMounted(() => {
        document.addEventListener("closeAllButtonMenus", closeMenu);
      });

      return {
        showOptionsMenu,
        hasOptions,
        hasMultipleOptions,
        isSingleOption,
        buttonStyle,
        handleClick,
        handleOptionClick,
      };
    },
    template: `
			<div class="ss-search-button-container" :class="{ open: showOptionsMenu }">
				<button
					class="ss-search-button"
					:style="buttonStyle"
					@click="handleClick"
					:disabled="disabled"
				>
					<!-- 前置图标插槽 -->
					<div v-if="preIcon" class="ss-search-button__pre-icon">
						<Icon :name="preIcon" :size="iconSize" :color="iconColor"/>
					</div>

					<!-- 按钮文本 -->
					<span class="ss-search-button__text">{{ text }}</span>

					<!-- 后置图标插槽 -->
					<div v-if="suffixIcon" class="ss-search-button__suffix-icon">
						<Icon :name="suffixIcon" :size="iconSize" :color="iconColor"/>
					</div>
				</button>

				<!-- 选项弹窗菜单 -->
				<div
					v-if="showOptionsMenu && hasMultipleOptions"
					class="options-menu"
					@click.stop
				>
					<div
						v-for="(option, index) in options"
						:key="index"
						class="option-item"
						@click="handleOptionClick(option, index)"
					>
						<Icon v-if="option.icon" :name="option.icon" size="28" color="inherit"/>
						<span class="option-text">{{ option.text }}</span>
					</div>
				</div>
			</div>
		`,
  };

  let activeSsSelectCloser = null;

  // ss-select 下拉选择组件 - 从小程序转换
  const SsSelect = {
    name: "SsSelect",
    props: {
      // 选项数组
      options: {
        type: Array,
        default: () => [],
      },
      // 字段映射
      mapping: {
        type: Object,
        default: () => ({ text: "n", value: "v" }),
      },
      // 默认值
      modelValue: {
        type: [String, Number],
        default: "",
      },
      // 占位符
      placeholder: {
        type: String,
        default: "请选择",
      },
      // 校验配置
      validation: {
        type: Object,
        default: () => ({ enable: false, message: "" }),
      },
      // 是否禁用
      disabled: {
        type: Boolean,
        default: false,
      },
      // 是否支持搜索
      searchable: {
        type: Boolean,
        default: false,
      },
      // 是否支持清空
      clearable: {
        type: Boolean,
        default: false,
      },
      // 加载状态
      loading: {
        type: Boolean,
        default: false,
      },
      // 宽度设置
      width: {
        type: String,
        default: "100%",
      },
      minWidth: {
        type: String,
        default: "unset",
      },
      // 功能说明：对齐PC端 ss-objp，用 codebook 在组件内部拉取下拉选项 by xu 2026-02-28
      cb: {
        type: String,
        default: "",
      },
      url: {
        type: String,
        default: "/service?ssServ=loadObjpOpt&objectpickerdropdown1=1",
      },
      inp: {
        type: [Boolean, String],
        default: false,
      },
      filter: {
        type: [Object, String],
        default: null,
      },
      autoSelectFirst: {
        type: Boolean,
        default: false,
      },
    },
    emits: ["update:modelValue", "change", "search", "clear", "loaded"],
    setup(props, { emit }) {
      const isOpen = ref(false);
      const containerRef = ref(null);
      const inputRef = ref(null);
      const selectedValue = ref(props.modelValue);
      const searchKeyword = ref("");
      const filterKeyword = ref("");
      const remoteOptions = ref([]);
      const remoteLoading = ref(false);
      const autoMinWidth = ref("");
      const forceShowAll = ref(false);

      const canInput = computed(() => {
        return props.inp === true || props.inp === "true";
      });

      // 功能说明：ss-select 未显式传宽度时，按 placeholder/选项文本计算稳定最小宽度，避免回显和下拉宽度随选中项抖动 by xu 2026-03-06
      const normalizeCssSize = (value) => {
        if (value === undefined || value === null) return "";
        if (typeof value === "number") return `${value}px`;
        const text = String(value).trim();
        return text;
      };

      const hasExplicitWidth = computed(() => {
        const widthText = normalizeCssSize(props.width);
        if (!widthText) return false;
        return widthText !== "100%" && widthText !== "auto";
      });

      const parseFilterObj = () => {
        if (!props.filter) return {};
        if (typeof props.filter === "object") return props.filter;
        if (typeof props.filter === "string") {
          try {
            const obj = JSON.parse(props.filter);
            return obj && typeof obj === "object" ? obj : {};
          } catch (_) {
            return {};
          }
        }
        return {};
      };

      const normalizeResultToOptions = (respData) => {
        const raw = respData || {};
        if (Array.isArray(raw.resultList)) {
          return raw.resultList.map((it) => {
            if (it && typeof it === "object") return it;
            return { n: String(it || ""), v: String(it || "") };
          });
        }
        if (raw.result && typeof raw.result === "object") {
          return Object.keys(raw.result).map((k) => ({
            n: raw.result[k],
            v: k,
          }));
        }
        if (Array.isArray(raw.objectList)) {
          return raw.objectList.map((it) => {
            if (it && typeof it === "object") return it;
            return { n: String(it || ""), v: String(it || "") };
          });
        }
        return [];
      };

      const needRemoteData = computed(() => !!String(props.cb || "").trim());

      const optionsList = computed(() => {
        if (Array.isArray(props.options) && props.options.length > 0) {
          return props.options;
        }
        if (needRemoteData.value) {
          return remoteOptions.value;
        }
        return [];
      });

      const getOptionText = (option) => {
        if (!option || typeof option !== "object") return "";
        const text = option[props.mapping.text];
        return text === undefined || text === null ? "" : String(text);
      };

      const findSelectedOption = () => {
        return optionsList.value.find(
          (option) => option?.[props.mapping.value] === selectedValue.value
        );
      };

      const syncInputFromSelection = () => {
        if (!canInput.value) return;
        const selectedOption = findSelectedOption();
        searchKeyword.value = selectedOption
          ? getOptionText(selectedOption)
          : "";
        filterKeyword.value = "";
        forceShowAll.value = false;
      };

      const filteredOptions = computed(() => {
        const list = Array.isArray(optionsList.value) ? optionsList.value : [];
        if (!canInput.value || forceShowAll.value) {
          return list;
        }
        const keyword = String(filterKeyword.value || "")
          .trim()
          .toLowerCase();
        if (!keyword) {
          return list;
        }
        return list.filter((option) =>
          getOptionText(option).toLowerCase().includes(keyword)
        );
      });

      const measureStableMinWidth = () => {
        if (hasExplicitWidth.value) {
          autoMinWidth.value = "";
          return;
        }

        const texts = [props.placeholder]
          .concat(
            (Array.isArray(optionsList.value) ? optionsList.value : []).map(
              (option) => option?.[props.mapping.text]
            )
          )
          .map((item) =>
            item === undefined || item === null ? "" : String(item).trim()
          )
          .filter(Boolean);

        if (!texts.length || typeof document === "undefined") {
          autoMinWidth.value = "";
          return;
        }

        const measureNode =
          containerRef.value?.querySelector?.(".select-text") ||
          containerRef.value;
        const computedStyle = measureNode
          ? window.getComputedStyle(measureNode)
          : null;
        const fontSize = computedStyle?.fontSize || "16px";
        const fontWeight = computedStyle?.fontWeight || "400";
        const fontFamily = computedStyle?.fontFamily || "sans-serif";
        const canvas = document.createElement("canvas");
        const context = canvas.getContext("2d");
        if (!context) return;

        context.font = `${fontWeight} ${fontSize} ${fontFamily}`;
        const widestText = texts.reduce((maxWidth, text) => {
          return Math.max(maxWidth, context.measureText(text).width);
        }, 0);

        const reservedWidth = 52;
        autoMinWidth.value = `${Math.max(
          88,
          Math.ceil(widestText + reservedWidth)
        )}px`;
      };

      const maybeAutoSelectFirst = (opts) => {
        if (!props.autoSelectFirst) return;
        if (!Array.isArray(opts) || opts.length === 0) return;
        if (
          selectedValue.value !== undefined &&
          selectedValue.value !== null &&
          selectedValue.value !== ""
        ) {
          return;
        }
        const first = opts[0];
        if (!first || typeof first !== "object") return;
        const value = first[props.mapping.value];
        if (value === undefined || value === null || value === "") return;
        selectedValue.value = value;
        emit("update:modelValue", value);
        emit("change", value);
        syncInputFromSelection();
      };

      const loadRemoteOptions = async () => {
        if (!needRemoteData.value) return;
        if (!window.request || typeof window.request.post !== "function")
          return;

        remoteLoading.value = true;
        try {
          const filterObj = parseFilterObj();
          const objpParam = {
            input: String(canInput.value),
            codebook: String(props.cb || ""),
            ...filterObj,
          };
          const postData = {
            objectpickerparam: JSON.stringify(objpParam),
            objectpickertype: 1,
            objectpickersearchAll: 1,
          };

          // 功能说明：xyByBj 人员码本按班级级联时，补充固定 cascadingLevel 和上级班级值 by xu 2026-03-18
          if (String(props.cb || "") === "xyByBj" && filterObj.bjid) {
            objpParam.cascadingLevel = "bjid,ryid";
            postData.objectpickerparam = JSON.stringify(objpParam);
            postData.upperValue = filterObj.bjid;
          }

          const resp = await window.request.post(
            props.url || "/service?ssServ=loadObjpOpt&objectpickerdropdown1=1",
            postData,
            { loading: false, formData: true }
          );
          const opts = normalizeResultToOptions(
            resp && resp.data ? resp.data : null
          );
          remoteOptions.value = opts;
          emit("loaded", opts);
          maybeAutoSelectFirst(opts);
          if (!isOpen.value) {
            syncInputFromSelection();
          }
        } catch (e) {
          remoteOptions.value = [];
          emit("loaded", []);
          console.error("[ss-select] loadRemoteOptions failed", props.cb, e);
        } finally {
          remoteLoading.value = false;
        }
      };

      const finalLoading = computed(() => {
        return !!props.loading || remoteLoading.value;
      });

      const selectContainerStyle = computed(() => {
        const style = {};
        const widthText = normalizeCssSize(props.width);
        const minWidthText = normalizeCssSize(props.minWidth);

        // 功能说明：ss-select 在 td 等窄容器中限制最大宽度，避免自动 minWidth 把父布局撑开 by xu 2026-03-07
        style.maxWidth = "100%";

        if (hasExplicitWidth.value) {
          style.width = widthText;
        }

        if (minWidthText && minWidthText !== "unset") {
          style.minWidth = `min(${minWidthText}, 100%)`;
        } else if (autoMinWidth.value) {
          style.minWidth = `min(${autoMinWidth.value}, 100%)`;
        }

        return style;
      });

      const displayText = computed(() => {
        if (!selectedValue.value) return props.placeholder;
        const selectedOption = findSelectedOption();
        return selectedOption
          ? getOptionText(selectedOption)
          : props.placeholder;
      });

      const enterInputFilterMode = () => {
        if (!canInput.value) return;
        const selectedOption = findSelectedOption();
        const selectedText = selectedOption
          ? getOptionText(selectedOption)
          : "";

        if (!filterKeyword.value && searchKeyword.value === selectedText) {
          searchKeyword.value = "";
        }

        filterKeyword.value = searchKeyword.value;
        forceShowAll.value = false;
      };

      const openDropdown = (showAll = false) => {
        if (props.disabled) return;
        if (activeSsSelectCloser && activeSsSelectCloser !== closeDropdown) {
          activeSsSelectCloser({ restoreInput: true });
        }
        forceShowAll.value = showAll;
        isOpen.value = true;
        activeSsSelectCloser = closeDropdown;
      };

      const closeDropdown = ({ restoreInput = true } = {}) => {
        isOpen.value = false;
        forceShowAll.value = false;
        if (activeSsSelectCloser === closeDropdown) {
          activeSsSelectCloser = null;
        }
        if (restoreInput) {
          syncInputFromSelection();
        }
      };

      const toggleDropdown = () => {
        if (props.disabled) return;
        if (isOpen.value) {
          closeDropdown();
        } else {
          openDropdown(false);
        }
      };

      const handleContainerClick = () => {
        if (props.disabled || canInput.value) return;
        toggleDropdown();
      };

      const handleTextClick = () => {
        if (props.disabled) return;
        if (!canInput.value) {
          toggleDropdown();
          return;
        }
        enterInputFilterMode();
        openDropdown(false);
        if (inputRef.value) {
          inputRef.value.focus();
        }
      };

      const handleInputFocus = () => {
        if (props.disabled || !canInput.value) return;
        enterInputFilterMode();
        openDropdown(false);
      };

      const handleInput = (event) => {
        const value = event?.target?.value || "";
        searchKeyword.value = value;
        filterKeyword.value = value;
        forceShowAll.value = false;
        isOpen.value = true;
        emit("search", value);
      };

      const handleArrowClick = () => {
        if (props.disabled) return;
        if (!canInput.value) {
          toggleDropdown();
          return;
        }
        filterKeyword.value = "";
        forceShowAll.value = true;
        isOpen.value = true;
      };

      const selectOption = (option) => {
        const value = option[props.mapping.value];
        selectedValue.value = value;
        searchKeyword.value = getOptionText(option);
        filterKeyword.value = "";
        forceShowAll.value = false;
        isOpen.value = false;

        emit("update:modelValue", value);
        emit("change", value);
      };

      const handleClickOutside = (event) => {
        if (!containerRef.value?.contains(event.target)) {
          closeDropdown();
        }
      };

      watch(
        () => props.modelValue,
        (newValue) => {
          selectedValue.value = newValue;
          if (!isOpen.value) {
            syncInputFromSelection();
          }
        },
        { immediate: true }
      );

      watch(
        () => props.options,
        (newVal) => {
          if (needRemoteData.value) return;
          emit("loaded", Array.isArray(newVal) ? newVal : []);
          if (!isOpen.value) {
            syncInputFromSelection();
          }
        }
      );

      watch(
        () => [props.cb, props.url, props.filter],
        () => {
          if (!needRemoteData.value) return;
          loadRemoteOptions();
        }
      );

      watch(
        () => [
          props.placeholder,
          props.width,
          props.minWidth,
          optionsList.value
            .map((option) => option?.[props.mapping.text])
            .join("||"),
        ],
        () => {
          measureStableMinWidth();
        },
        { immediate: true }
      );

      watch(
        () => optionsList.value,
        () => {
          if (!isOpen.value) {
            syncInputFromSelection();
          }
        },
        { deep: true }
      );

      onMounted(() => {
        document.addEventListener("click", handleClickOutside);
        loadRemoteOptions();
        if (!needRemoteData.value && Array.isArray(props.options)) {
          emit("loaded", props.options);
        }
        syncInputFromSelection();
        measureStableMinWidth();
      });

      onBeforeUnmount(() => {
        if (activeSsSelectCloser === closeDropdown) {
          activeSsSelectCloser = null;
        }
        document.removeEventListener("click", handleClickOutside);
      });

      return {
        containerRef,
        inputRef,
        isOpen,
        selectedValue,
        searchKeyword,
        canInput,
        filteredOptions,
        finalLoading,
        displayText,
        selectContainerStyle,
        handleContainerClick,
        handleTextClick,
        handleInputFocus,
        handleInput,
        handleArrowClick,
        selectOption,
      };
    },
    template: `
			<div
				ref="containerRef"
				class="ss-select-container"
				:class="{ open: isOpen }"
				:style="selectContainerStyle"
				@click.stop="handleContainerClick"
			>
				<div class="ss-select" :class="{ disabled: disabled }">
					<div class="select-text" @click.stop="handleTextClick">
						<input
							v-if="canInput"
							ref="inputRef"
							:type="'text'"
							:value="searchKeyword"
							:placeholder="placeholder"
							:disabled="disabled"
							@focus.stop="handleInputFocus"
							@click.stop="handleTextClick"
							@input="handleInput"
							style="width: 100%; border: none; outline: none; background: transparent; color: inherit; font-size: inherit; padding: 0; margin: 0;"
						/>
						<span v-else :class="{ placeholder: !selectedValue }">{{ displayText }}</span>
					</div>
					<div class="select-arrow" :class="{ rotate: isOpen }" @click.stop="handleArrowClick">
						<Icon name="icon-xiangxiajiantou" size="32" :color="disabled ? '#ccc' : '#999'"/>
					</div>
				</div>

				<div class="ss-options" v-show="isOpen">
					<div
						v-if="finalLoading"
						class="option-item loading-item"
					>
						<span class="loading-text">加载中...</span>
					</div>
					<div
						v-else-if="filteredOptions.length === 0"
						class="option-item no-options"
					>
						无选项
					</div>
					<div
						v-else
						v-for="(option, index) in filteredOptions"
						:key="index"
						class="option-item"
						:class="{ selected: option[mapping.value] === selectedValue }"
						@click.stop="selectOption(option)"
					>
						{{ option[mapping.text] }}
					</div>
				</div>
			</div>
		`,
  };

  // ss-bottom 底部按钮组件
  const SsBottom = {
    name: "SsBottom",
    props: {
      // 是否显示审核意见
      showShyj: {
        type: Boolean,
        default: false,
      },
      // 审核意见标题
      shyjTitle: {
        type: String,
        default: "审核意见",
      },
      // 审核意见占位符
      shyjPlaceholder: {
        type: String,
        default: "请输入审核意见",
      },
      divider: {
        type: Boolean,
        default: true,
      },
      // 按钮配置
      buttons: {
        type: Array,
        default: () => [
          { text: "取消", action: "cancel" },
          { text: "保存并提交", action: "submit" },
        ],
      },
    },
    emits: ["button-click", "update:shyjValue"],
    setup(props, { emit }) {
      const reason = ref("");
      const activeButtonIndex = ref(-1);

      // 处理按钮点击
      const handleButtonClick = (button, index) => {
        emit("button-click", {
          action: button.action,
          button: button,
          index: index,
          shyjValue: reason.value, // 传递审核意见
        });
      };

      // 监听审核意见变化
      const handleShyjInput = (event) => {
        const value = event.target.value;
        reason.value = value;
        emit("update:shyjValue", value);
      };

      // 处理按钮按下
      const handleButtonMouseDown = (index) => {
        activeButtonIndex.value = index;
      };

      // 处理按钮释放
      const handleButtonMouseUp = () => {
        activeButtonIndex.value = -1;
      };

      // 处理鼠标离开
      const handleMouseLeave = () => {
        activeButtonIndex.value = -1;
      };

      // 计算按钮样式
      const getButtonStyle = (button) => {
        const styles = {};

        // 如果有背景颜色配置
        if (button.backgroundColor) {
          styles.backgroundColor = button.backgroundColor;
        }

        // 如果有字体颜色配置
        if (button.color) {
          styles.color = button.color;
        }

        return styles;
      };

      // 计算按钮点击样式
      const getButtonActiveStyle = (button) => {
        const styles = {};

        // 如果有点击背景色配置，使用点击背景色
        if (button.clickBgColor) {
          styles.backgroundColor = button.clickBgColor;
        } else if (button.backgroundColor) {
          // 如果没有点击背景色，但有背景色，点击时使用背景色
          styles.backgroundColor = button.backgroundColor;
        }

        // 如果有点击字体色配置，使用点击字体色
        if (button.clickColor) {
          styles.color = button.clickColor;
        } else if (button.color) {
          // 如果没有点击字体色，但有字体色，点击时使用字体色
          styles.color = button.color;
        }

        return styles;
      };

      return {
        reason,
        activeButtonIndex,
        handleButtonClick,
        handleShyjInput,
        handleButtonMouseDown,
        handleButtonMouseUp,
        handleMouseLeave,
        getButtonStyle,
        getButtonActiveStyle,
      };
    },
    template: `
			<div class="ss-bottom">
				<!-- 审核意见区域 -->
				<div v-if="showShyj" class="ss-bottom__opinion">
					<table class="ss-bottom__opinion-table">
						<tr>
							<th class="ss-bottom__opinion-label">{{ shyjTitle }}</th>
							<td class="ss-bottom__opinion-input">
								<ss-input
									:placeholder="shyjPlaceholder"
									v-model="reason"
									@input="handleShyjInput"
								/>
							</td>
						</tr>
					</table>
				</div>
				<!-- 按钮区域 -->
				<div class="ss-bottom__buttons" :class="{ 'ss-bottom__buttons--with-border': !showShyj }">
					<template v-for="(button, index) in buttons" :key="index">
						<div
							class="ss-bottom__button"
							:class="{
								'ss-bottom__button--custom': button.backgroundColor || button.color || button.clickBgColor || button.clickColor,
								'ss-bottom__button--active': activeButtonIndex === index
							}"
							:style="activeButtonIndex === index ? getButtonActiveStyle(button) : getButtonStyle(button)"
							@click="handleButtonClick(button, index)"
							@mousedown="handleButtonMouseDown(index)"
							@mouseup="handleButtonMouseUp"
							@mouseleave="handleMouseLeave"
							@touchstart="handleButtonMouseDown(index)"
							@touchend="handleButtonMouseUp"
						>
							{{ button.text }}
						</div>
						<!-- 分割线，最后一个按钮不显示 -->
						<div v-if="index < buttons.length - 1 && divider" class="ss-bottom__divider"></div>
					</template>
				</div>
			</div>
		`,
  };

  // ===== SsVerify 审核节点链组件 =====
  const SsVerify = {
    name: "SsVerify",
    props: {
      verifyList: {
        type: Array,
        required: true,
      },
    },
    setup(props) {
      const toggleOpen = (item) => {
        item.open = !item.open;
        // 切换后重新计算连线高度
        setTimeout(() => {
          calculateLineHeight();
        }, 50);
      };

      // 计算连线高度的函数
      const calculateLineHeight = () => {
        const lastOpenGroup = document.querySelector(".group-item-last-open");
        console.log("lastOpenGroup", lastOpenGroup);
        if (lastOpenGroup) {
          // 使用原生JavaScript代替jQuery
          const nodes = lastOpenGroup.querySelectorAll(
            ".verify-node-container"
          );
          if (nodes.length) {
            let totalHeight = 0;

            if (nodes.length === 1) {
              // 只有一个节点时，连线伸到节点的中间位置
              const nodeHeight = nodes[0].offsetHeight;
              const nodeTop = nodes[0].offsetTop;
              totalHeight = nodeTop + nodeHeight / 2 - 15; // 减去圆点半径5px
            } else {
              // 多个节点时，连线延伸到最后一个节点的中间位置
              const lastNode = nodes[nodes.length - 1];
              const lastNodeTop = lastNode.offsetTop;
              const lastNodeHeight = lastNode.offsetHeight;
              totalHeight = lastNodeTop + lastNodeHeight / 2 - 15; // 减去圆点半径5px
            }

            console.log("节点信息:", {
              节点总数: nodes.length,
              计算后的高度: totalHeight,
              最后节点top: nodes[nodes.length - 1]?.offsetTop,
              最后节点高度: nodes[nodes.length - 1]?.offsetHeight,
            });

            lastOpenGroup.style.setProperty(
              "--group-line-height",
              `${totalHeight}px`
            );
          }
        }
      };

      onMounted(() => {
        setTimeout(() => {
          calculateLineHeight();
        }, 100);
      });

      return {
        toggleOpen,
      };
    },
    render() {
      const { h } = Vue;
      return h(
        "div",
        { class: "verify-nodes" },
        this.verifyList.map((item, i) =>
          h(
            "div",
            {
              key: i,
              class: {
                "group-item": true,
                "group-item-last-open":
                  i === this.verifyList.length - 1 && item.open,
              },
            },
            [
              h(
                "div",
                {
                  class: "group-item-title",
                  onClick: () => this.toggleOpen(item),
                },
                [
                  h("div", { class: "icon" }, [
                    h("i", {
                      class: item.open
                        ? "common-icon-folder-open common-icon"
                        : "common-icon-folder-close common-icon",
                    }),
                    h(
                      "div",
                      {
                        class: "num",
                        style: { top: item.open ? "60%" : "55%" },
                      },
                      item.children?.length || 0
                    ),
                  ]),
                  h("div", { class: "name" }, item.groupName),
                ]
              ),
              item.open && item.children?.length > 0
                ? h(
                    "div",
                    { class: "group-item-children" },
                    item.children.map((citem, j) =>
                      h(SsVerifyNode, {
                        key: j,
                        item: citem,
                        // isGroup: i + 1 !== this.verifyList.length,
                        isGroup: true,
                      })
                    )
                  )
                : null,
            ]
          )
        )
      );
    },
  };

  // ===== SsVerifyNode 审核节点组件 =====
  const SsVerifyNode = {
    name: "SsVerifyNode",
    props: {
      item: {
        type: Object,
        required: true,
      },
      isGroup: {
        type: Boolean,
        default: false,
      },
    },
    render() {
      const { h } = Vue;
      return h("div", { class: "verify-node-container" }, [
        h("div", { class: "info" }, [
          h("div", { class: "avatar" }, [
            h("img", {
              src: this.item.thumb,
              style: {
                width: "50px",
                height: "50px",
                borderRadius: "50%",
              },
            }),
          ]),
          h("div", { class: "desc" }, [
            h("div", this.item.name),
            h("div", this.item.role),
          ]),
          h("div", { class: "link" }, [
            h("div", [
              this.item.video
                ? h("i", { class: "common-icon-video common-icon" })
                : null,
              this.item.link
                ? h("i", { class: "common-icon-paper-clip common-icon" })
                : null,
            ]),
          ]),
        ]),
        h(
          "div",
          {
            class: {
              description: true,
              link: this.isGroup,
            },
            attrs: { "data-num": "3" },
          },
          [h("div", this.item.description)]
        ),
        h("div", { class: "time" }, this.item.time),
      ]);
    },
  };

  // ===== SsOnoffButton 开关按钮 =====
  const SsOnoffButton = {
    name: "SsOnoffButton",
    props: {
      // 字段名称，用于表单校验
      name: { type: String, required: true },
      // 显示标签
      label: { type: String, required: true },
      // 按钮的值
      value: { type: [String, Number], required: true },
      // 宽度设置
      width: { type: String, default: "" },
      // v-model 绑定的值
      modelValue: { type: [String, Number, Array], default: "" },
      // 是否多选模式
      multiple: { type: Boolean, default: false },
      // 是否禁用
      disabled: { type: Boolean, default: false },
    },
    emits: ["update:modelValue", "change"],
    setup(props, { emit }) {
      // 解析 modelValue，支持逗号分隔的字符串和数组
      const parseModelValue = (val) => {
        if (!val) return [];

        // 如果是数组，直接返回字符串数组
        if (Array.isArray(val)) {
          return val.map((v) => v.toString());
        }

        // 如果是字符串，按逗号分割
        const cleanValue = val.toString().replace(/^,+/, ""); // 去掉开头的逗号
        if (cleanValue.includes("|")) {
          return cleanValue.split("|");
        }
        if (cleanValue.includes(",")) {
          return cleanValue.split(",");
        }
        return cleanValue ? [cleanValue] : [];
      };

      // 判断当前按钮是否选中
      const isChecked = computed(() => {
        if (props.multiple) {
          const currentValue = parseModelValue(props.modelValue);
          return currentValue.includes(props.value.toString());
        }
        return props.modelValue === props.value;
      });

      // 切换选中状态
      const toggleSelect = () => {
        // 如果禁用，不执行任何操作
        if (props.disabled) return;

        if (props.multiple) {
          // 多选模式
          const currentValue = parseModelValue(props.modelValue);
          const index = currentValue.indexOf(props.value.toString());
          let newValue;

          if (index === -1) {
            // 添加选项
            newValue = [...currentValue, props.value.toString()];
          } else {
            // 移除选项
            newValue = currentValue.filter((v) => v !== props.value.toString());
          }

          // 发送更新事件，使用逗号分隔的字符串格式
          const emitValue = newValue.join(",");
          emit("update:modelValue", emitValue);
          emit("change", emitValue, newValue);
        } else {
          // 单选模式
          emit("update:modelValue", props.value);
          emit("change", props.value);
        }
      };

      return { isChecked, toggleSelect };
    },
    template: `
			  <div class="ss-onoff-button" :class="{ checked: isChecked, disabled: disabled }" @click="toggleSelect">
			    <span class="button-label">{{ label }}</span>
			    <div class="button-mark">
			      <span class="form-icon" :class="isChecked ? 'form-icon-onoffbutton-checked' : 'form-icon-onoffbutton-unchecked'"></span>
			    </div>
			  </div>
			`,
  };

  // ===== SsDatetimePicker 日期时间选择（使用 Vant 4） =====
  const SsDatetimePicker = {
    name: "SsDatetimePicker",
    props: {
      mode: { type: String, default: "date" }, // date | time | datetime
      placeholder: { type: String, default: "请选择日期" },
      modelValue: { type: String, default: "" },
      minDate: { type: String, default: "" },
      maxDate: { type: String, default: "" },
      // 字段名称 - 用于ssVm校验
      name: { type: String, default: "" },
      // 是否禁用
      disabled: { type: Boolean, default: false },
    },
    emits: [
      "update:modelValue",
      "change",
      "confirm",
      "cancel",
      "open",
      "close",
    ],
    setup(props, { emit }) {
      const showPicker = ref(false);
      const showTimePicker = ref(false);
      const currentStep = ref("date"); // 'date' | 'time'

      // 功能说明：统一在组件层处理 iframe 场景底部按钮显隐，避免每个页面重复绑定 by xu 2026-02-28
      const notifyParentBottomVisible = (visible) => {
        try {
          const fn = window.parent && window.parent.__mpObjInpSetBottomVisible;
          if (typeof fn === "function") fn(visible !== false);
        } catch (_) {}
      };

      // 功能说明：监听弹层显隐，点遮罩/取消关闭时也恢复父层底部按钮 by xu 2026-03-01
      watch(
        [showPicker, showTimePicker],
        ([dateOpen, timeOpen], [prevDateOpen, prevTimeOpen]) => {
          const hasOpen = !!(dateOpen || timeOpen);
          const hadOpen = !!(prevDateOpen || prevTimeOpen);

          if (!hadOpen && hasOpen) {
            emit("open");
            notifyParentBottomVisible(false);
            return;
          }

          if (hadOpen && !hasOpen) {
            emit("close");
            notifyParentBottomVisible(true);
          }
        }
      );

      // Vant DatePicker 需要数组格式 [year, month, day]
      const currentDateArray = ref([]);
      const currentTimeArray = ref(["12", "00"]); // [hour, minute]
      const tempDateStr = ref(""); // 临时存储选择的日期

      // 格式化显示文本
      const displayText = computed(() => {
        if (!props.modelValue) return props.placeholder;
        try {
          const d = new Date(props.modelValue);
          if (isNaN(d.getTime())) return props.modelValue;

          const year = d.getFullYear();
          const month = String(d.getMonth() + 1).padStart(2, "0");
          const day = String(d.getDate()).padStart(2, "0");
          const hours = String(d.getHours()).padStart(2, "0");
          const minutes = String(d.getMinutes()).padStart(2, "0");

          if (props.mode === "time") {
            return `${hours}:${minutes}`;
          } else if (props.mode === "datetime") {
            return `${year}-${month}-${day} ${hours}:${minutes}`;
          }
          return `${year}-${month}-${day}`;
        } catch (e) {
          return props.modelValue;
        }
      });

      // 监听 modelValue 变化，转换为数组格式
      watch(
        () => props.modelValue,
        (newVal) => {
          console.log("📅 modelValue 变化:", newVal);
          if (newVal) {
            try {
              const d = new Date(newVal);
              if (!isNaN(d.getTime())) {
                currentDateArray.value = [
                  d.getFullYear(),
                  d.getMonth() + 1,
                  d.getDate(),
                ];
                console.log("📅 转换为数组:", currentDateArray.value);
              }
            } catch (e) {
              console.warn("Invalid date:", newVal);
            }
          } else {
            const today = new Date();
            currentDateArray.value = [
              today.getFullYear(),
              today.getMonth() + 1,
              today.getDate(),
            ];
          }
        },
        { immediate: true }
      );

      // 确认选择 - 根据模式处理不同的数据
      const onConfirm = (value) => {
        console.log(
          "📅 Vant Picker confirm 原始值:",
          value,
          "mode:",
          props.mode
        );
        try {
          // Vant 返回的是对象，包含 selectedValues 数组
          const selectedValues = value.selectedValues || value;
          console.log("📅 selectedValues:", selectedValues);

          if (props.mode === "time") {
            // 时间模式：处理时分
            if (Array.isArray(selectedValues) && selectedValues.length >= 2) {
              const [hour, minute] = selectedValues;
              const timeStr = `${hour.padStart(2, "0")}:${minute.padStart(
                2,
                "0"
              )}`;
              console.log("🕐 转换后的时间字符串:", timeStr);
              emit("update:modelValue", timeStr);
              emit("change", timeStr);
              emit("confirm", timeStr);
              emit("close");
              notifyParentBottomVisible(true);
              showPicker.value = false;
            }
          } else if (
            Array.isArray(selectedValues) &&
            selectedValues.length >= 3
          ) {
            // 日期模式：处理年月日
            const [year, month, day] = selectedValues;
            const dateStr = `${year}-${month.padStart(2, "0")}-${day.padStart(
              2,
              "0"
            )}`;

            if (props.mode === "datetime") {
              // datetime 模式：先存储日期，然后打开时间选择器
              tempDateStr.value = dateStr;
              showPicker.value = false;
              showTimePicker.value = true;
              currentStep.value = "time";
            } else {
              // date 模式：直接完成
              emit("update:modelValue", dateStr);
              emit("change", dateStr);
              emit("confirm", dateStr);
              emit("close");
              notifyParentBottomVisible(true);
              showPicker.value = false;
            }
          }
        } catch (e) {
          console.error("Picker conversion error:", e);
        }
      };

      // 时间选择确认
      const onTimeConfirm = (value) => {
        try {
          const selectedValues = value.selectedValues || value;
          if (Array.isArray(selectedValues) && selectedValues.length >= 2) {
            const [hour, minute] = selectedValues;
            const datetimeStr = `${tempDateStr.value} ${hour.padStart(
              2,
              "0"
            )}:${minute.padStart(2, "0")}`;
            emit("update:modelValue", datetimeStr);
            emit("change", datetimeStr);
            emit("confirm", datetimeStr);
            emit("close");
            notifyParentBottomVisible(true);
          }
        } catch (e) {
          console.error("Time conversion error:", e);
        }
        showTimePicker.value = false;
        currentStep.value = "date";
      };

      // 取消选择
      const onCancel = () => {
        emit("cancel");
        showPicker.value = false;
      };

      const onTimeCancel = () => {
        emit("cancel");
        showTimePicker.value = false;
      };

      // 打开选择器
      const openPicker = () => {
        // 如果禁用，不打开选择器
        if (props.disabled) return;
        showPicker.value = true;
      };

      // 计算最小最大日期
      const minDateObj = computed(() => {
        return props.minDate ? new Date(props.minDate) : undefined;
      });

      const maxDateObj = computed(() => {
        return props.maxDate ? new Date(props.maxDate) : undefined;
      });

      return {
        showPicker,
        showTimePicker,
        currentDateArray,
        currentTimeArray,
        displayText,
        openPicker,
        onConfirm,
        onTimeConfirm,
        onTimeCancel,
        onCancel,
        minDateObj,
        maxDateObj,
      };
    },
    template: `
			  <div class="ss-datetime-picker ss-mobile-component" :class="{ disabled: disabled }">
			    <!-- 隐藏的input用于ssVm校验 -->
			    <input type="hidden" :name="name" :value="modelValue" />

			    <div class="datetime-picker-display" @click="openPicker">
			      <span class="datetime-picker-value" :class="{ placeholder: !modelValue }">{{ displayText }}</span>
			    </div>

			    <!-- 日期选择器 -->
			    <van-popup v-model:show="showPicker" position="bottom" :style="{ zIndex: 10000 }">
			      <van-date-picker
			        v-if="mode === 'date' || mode === 'datetime'"
			        v-model="currentDateArray"
			        :min-date="minDateObj"
			        :max-date="maxDateObj"
			        @confirm="onConfirm"
			        @cancel="onCancel"
			        title="选择日期"
			      />
			      <van-time-picker
			        v-if="mode === 'time'"
			        v-model="currentTimeArray"
			        @confirm="onConfirm"
			        @cancel="onCancel"
			        title="选择时间"
			      />
			    </van-popup>

			    <!-- 时间选择器（datetime 模式的第二步） -->
			    <van-popup v-model:show="showTimePicker" position="bottom" :style="{ zIndex: 10000 }">
			      <van-time-picker
			        v-model="currentTimeArray"
			        @confirm="onTimeConfirm"
			        @cancel="onTimeCancel"
			        title="选择时间"
			      />
			    </van-popup>
			  </div>
			`,
  };

  const SsConfirm = {
    name: "SsConfirm",
    props: {
      modelValue: { type: Boolean, default: false },
      title: { type: String, default: "确认" },
      content: { type: String, default: "" },
      maskClosable: { type: Boolean, default: true },
    },
    emits: ["update:modelValue", "confirm", "cancel", "close"],
    setup(props, { emit }) {
      // 监听弹窗显示状态，控制 body 滚动
      watch(
        () => props.modelValue,
        (newVal) => {
          console.log("🔔 SsConfirm modelValue 变化:", newVal);
          if (newVal) {
            document.body.classList.add("modal-open");
          } else {
            document.body.classList.remove("modal-open");
          }
        },
        { immediate: true }
      ); // 添加 immediate 选项

      const close = () => {
        console.log("🚪 关闭确认弹窗");
        emit("update:modelValue", false);
        emit("close");
      };
      const onMask = () => {
        console.log("👆 点击了遮罩层");
        if (props.maskClosable) close();
      };
      const onCancel = () => {
        console.log("❌ 点击了取消按钮");
        emit("cancel");
        close();
      };
      const onConfirm = () => {
        console.log("✅ 点击了确认按钮，触发 confirm 事件");
        emit("confirm");
        close();
      };
      return { onMask, onCancel, onConfirm };
    },
    template: `
			  <div v-if="modelValue" class="ss-confirm">
			    <div class="confirm-mask" @click="onMask"></div>
			    <div class="confirm-content">
			      <div class="confirm-header" v-if="title">
			        <div class="header-title">{{ title }}</div>
			      </div>
			      <div class="header-line" v-if="title"></div>
			      <div class="confirm-body">
			        <div v-if="content" class="confirm-content-text" v-html="content"></div>
			        <div class="confirm-slot-content"><slot /></div>
			      </div>
			      <div class="confirm-bottom">
			        <button class="confirm-btn confirm-btn-cancel" @click.stop="onCancel">取消</button>
			        <button class="confirm-btn confirm-btn-confirm" @click.stop="onConfirm">确认</button>
			      </div>
			    </div>
			  </div>
			`,
  };

  // ===== SsImageCropper 纯裁剪组件 =====
  const SsImageCropper = {
    name: "SsImageCropper",
    props: {
      // 是否显示裁剪器
      show: { type: Boolean, default: false },
      // 图片源（base64 或 URL）
      src: { type: String, required: true },
      // 图片形状：circle圆形 | square方形
      shape: { type: String, required: true },
      // 裁剪比例（宽/高）
      aspectRatio: { type: Number, default: 1 },
      // 输出图片宽度
      outputWidth: { type: Number, default: 300 },
      // 输出图片高度
      outputHeight: { type: Number, default: 300 },
    },
    emits: ["update:show", "confirm", "cancel"],
    setup(props, { emit }) {
      const cropperInstance = ref(null);

      // 监听 show 变化，初始化或销毁 Cropper
      watch(
        () => props.show,
        (newVal) => {
          if (newVal) {
            // 等待 DOM 更新后初始化
            Vue.nextTick(() => {
              initCropper();
            });
          } else {
            destroyCropper();
          }
        }
      );

      // 监听 src 变化，重新初始化 Cropper
      watch(
        () => props.src,
        () => {
          if (props.show) {
            Vue.nextTick(() => {
              initCropper();
            });
          }
        }
      );

      // 初始化 Cropper
      const initCropper = () => {
        const imageElement = document.getElementById("ss-image-cropper-img");
        if (!imageElement || !window.Cropper) return;

        // 销毁旧实例
        destroyCropper();

        // 根据 shape 属性添加类名
        const container = document.querySelector(".ss-image-cropper-container");
        if (container) {
          if (props.shape === "circle") {
            container.classList.add("crop-shape-circle");
            container.classList.remove("crop-shape-square");
          } else {
            container.classList.add("crop-shape-square");
            container.classList.remove("crop-shape-circle");
          }
        }

        cropperInstance.value = new window.Cropper(imageElement, {
          aspectRatio: props.aspectRatio,
          viewMode: 1,
          dragMode: "move",
          autoCropArea: 0.8,
          restore: false,
          guides: false, // 关闭辅助线
          center: false, // 关闭中心指示器
          highlight: false,
          cropBoxMovable: true,
          cropBoxResizable: true,
          toggleDragModeOnDblclick: false,
          minContainerWidth: window.innerWidth,
          minContainerHeight: window.innerHeight - 50,
        });
      };

      // 销毁 Cropper
      const destroyCropper = () => {
        if (cropperInstance.value) {
          cropperInstance.value.destroy();
          cropperInstance.value = null;
        }
      };

      // 取消裁剪
      const handleCancel = () => {
        emit("update:show", false);
        emit("cancel");
      };

      // 确认裁剪
      const handleConfirm = () => {
        if (!cropperInstance.value) return;

        const canvas = cropperInstance.value.getCroppedCanvas({
          width: props.outputWidth,
          height: props.outputHeight,
          imageSmoothingEnabled: true,
          imageSmoothingQuality: "high",
          fillColor: "#fff",
        });

        canvas.toBlob(
          (blob) => {
            emit("update:show", false);
            emit("confirm", blob);
          },
          "image/jpeg",
          0.9
        );
      };

      // 处理底部按钮事件
      const handleCropAction = (data) => {
        if (data.action === "cancel") {
          handleCancel();
        } else if (data.action === "confirm") {
          handleConfirm();
        }
      };

      return {
        handleCropAction,
      };
    },
    template: `
			<div v-if="show" class="ss-image-cropper-container">
				<!-- 左上角尺寸显示 -->
				<div class="crop-size-display">
					长: {{ outputWidth }}px<br>宽: {{ outputHeight }}px
				</div>

				<div class="ss-crop-image-container">
					<img id="ss-image-cropper-img" :src="src" />
				</div>

				<!-- 使用 ss-bottom 组件 -->
				<ss-bottom
					:show-shyj="false"
					:buttons="[
						{ text: '取消', action: 'cancel' },
						{ text: '保存并提交', action: 'confirm' }
					]"
					@button-click="handleCropAction"
				/>
			</div>
		`,
  };

  // ===== SsUploadImage 图片上传裁剪组件（支持单图/多图） =====
  const SsUploadImage = {
    name: "SsUploadImage",
    props: {
      // v-model 绑定的值（单图：String，多图：Array）
      modelValue: { type: [String, Array], default: "" },
      // 最大上传数量（默认1张，多图时设置大于1）
      max: { type: Number, default: 1 },
      // 是否禁用
      disabled: { type: Boolean, default: false },
      // 图片宽度（像素） - 必填
      width: { type: [Number, String], required: true },
      // 图片高度（像素） - 必填
      height: { type: [Number, String], required: true },
      // 图片形状：circle圆形 | square方形 - 必填
      shape: { type: String, required: true },
      // 裁剪比例（宽/高）
      aspectRatio: { type: Number, default: undefined },
      // 输出图片宽度
      outputWidth: { type: Number, default: 300 },
      // 输出图片高度
      outputHeight: { type: Number, default: 300 },
    },
    emits: ["update:modelValue", "updated"],
    setup(props, { emit }) {
      const showCropper = ref(false);
      const tempImageSrc = ref("");

      // 图片列表（统一用数组管理）
      const imageList = ref([]);

      // 监听 modelValue 变化，同步到 imageList
      watch(
        () => props.modelValue,
        (newVal) => {
          if (props.max === 1) {
            // 单图模式
            imageList.value = newVal ? [newVal] : [];
          } else {
            // 多图模式
            imageList.value = Array.isArray(newVal)
              ? [...newVal]
              : newVal
              ? [newVal]
              : [];
          }
        },
        { immediate: true }
      );

      // 是否可以继续添加图片
      const canAddMore = computed(() => {
        return imageList.value.length < props.max;
      });

      // 容器样式
      const itemStyle = computed(() => ({
        width:
          typeof props.width === "number" ? `${props.width}px` : props.width,
        height:
          typeof props.height === "number" ? `${props.height}px` : props.height,
        borderRadius: props.shape === "circle" ? "50%" : "8px",
      }));

      // 获取图片 URL（用于显示）
      const getImageUrl = (path) => {
        if (!path) return "/static/images/yishuzhao_nv.svg";
        if (path.startsWith("http") || path.startsWith("blob:")) {
          return path;
        }
        return window.SS.utils?.getImageUrl?.(path) || path;
      };

      // 选择图片
      const selectImage = () => {
        if (props.disabled || !canAddMore.value) return;
        const input = document.createElement("input");
        input.type = "file";
        input.accept = "image/*";
        input.onchange = (e) => {
          const file = e.target.files[0];
          if (file) {
            const reader = new FileReader();
            reader.onload = (event) => {
              tempImageSrc.value = event.target.result;
              showCropper.value = true;
            };
            reader.readAsDataURL(file);
          }
        };
        input.click();
      };

      // 删除图片
      const deleteImage = (index) => {
        const newList = imageList.value.filter((_, i) => i !== index);
        updateModelValue(newList);
      };

      // 确认裁剪
      const handleCropConfirm = async (blob) => {
        try {
          const serverPath = await uploadFile(blob, "image", "image.jpg");
          const newList = [...imageList.value, serverPath];
          updateModelValue(newList);
          emit("updated", serverPath);
        } catch (error) {
          console.error("上传失败:", error);
        }
      };

      // 取消裁剪
      const handleCropCancel = () => {
        tempImageSrc.value = "";
      };

      // 更新 modelValue
      const updateModelValue = (list) => {
        if (props.max === 1) {
          // 单图模式：返回 String
          emit("update:modelValue", list[0] || "");
        } else {
          // 多图模式：返回 Array
          emit("update:modelValue", list);
        }
      };

      return {
        imageList,
        canAddMore,
        itemStyle,
        showCropper,
        tempImageSrc,
        getImageUrl,
        selectImage,
        deleteImage,
        handleCropConfirm,
        handleCropCancel,
      };
    },
    template: `
			<div class="ss-upload-image-multi">
				<!-- 图片列表 -->
				<div class="image-list">
					<!-- 已上传的图片 -->
					<div
						v-for="(img, index) in imageList"
						:key="index"
						class="image-item"
						:style="itemStyle"
					>
						<img :src="getImageUrl(img)" class="image-display" />
						<!-- 删除按钮 -->
						<div v-if="!disabled" class="image-delete" @click.stop="deleteImage(index)">
							<Icon name="icon-guanbi" size="32" color="#fff" />
						</div>
					</div>

					<!-- 添加按钮 -->
					<div
						v-if="canAddMore && !disabled"
						class="image-item image-add"
						:style="itemStyle"
						@click="selectImage"
					>
						<Icon name="icon-xiangji" size="48" color="#ccc" />
						<div class="add-text">{{ imageList.length }}/{{ max }}</div>
					</div>
				</div>

				<!-- 裁剪组件 -->
				<ss-image-cropper
					v-model:show="showCropper"
					:src="tempImageSrc"
					:shape="shape"
					:aspect-ratio="aspectRatio"
					:output-width="outputWidth"
					:output-height="outputHeight"
					@confirm="handleCropConfirm"
					@cancel="handleCropCancel"
				/>
			</div>
		`,
  };

  // ===== SsCarCard 车辆卡片组件 =====
  const SsCarCard = {
    name: "SsCarCard",
    props: {
      // 车辆数据
      carData: {
        type: Object,
        default: () => ({}),
      },
      // 车辆状态：'available' | 'reserved' | 'disabled'
      status: {
        type: String,
        default: "available",
        validator: (value) =>
          ["available", "reserved", "disabled"].includes(value),
      },
    },
    emits: ["click", "select"],
    setup(props, { emit }) {
      // 计算状态样式类
      const statusClass = computed(() => {
        return `status-${props.status}`;
      });

      // 获取图片URL
      const getImageUrl = (path) => {
        if (!path) return "/static/images/default-car.png";
        if (path.startsWith("http") || path.startsWith("blob:")) {
          return path;
        }
        return window.SS.utils?.getImageUrl?.(path) || path;
      };

      // 处理卡片点击
      const handleCardClick = () => {
        if (props.status === "disabled") {
          return; // 禁用状态不响应点击
        }

        emit("click", props.carData);
        emit("select", props.carData);
      };

      return {
        statusClass,
        getImageUrl,
        handleCardClick,
      };
    },
    template: `
			<div class="car-card" :class="statusClass" @click="handleCardClick">
				<!-- 第一行：车辆名称 -->
				<div class="car-title">
					{{ carData.name || '别克GL8' }}
				</div>

				<!-- 第二行：左右结构 -->
				<div class="car-info">
					<!-- 左边：车辆图片 -->
					<div class="car-image-container">
						<img class="car-image" :src="getImageUrl(carData.image)" />
					</div>

					<!-- 右边：车辆信息 -->
					<div class="car-details">
						<div class="detail-item car-name" v-if="carData.wph">
							{{ carData.wph }}
						</div>
						<div class="detail-item seats" v-for="(wp, index) in carData.wpcsList" :key="index">
							{{ wp.mc }} : {{ wp.sz || wp.zf }}
						</div>
						<div class="detail-item car-type">
							{{ carData.type || '商务车' }}
						</div>
					</div>
				</div>
			</div>
		`,
  };

  // ===== SsSubTab 移动端Tab组件 =====
  const SsSubTab = {
    name: "SsSubTab",
    props: {
      // Tab列表数据
      tabList: {
        type: Array,
        required: true,
      },
      // 当前激活的Tab索引
      activeIndex: {
        type: Number,
        default: 0,
      },
      // 基础URL参数（会传递给每个iframe）
      baseParams: {
        type: Object,
        default: () => ({}),
      },
    },
    emits: ["tab-change"],
    setup(props, { emit }) {
      const currentTab = ref(props.activeIndex);
      const currentTabUrl = ref("");

      // 加载Tab对应的URL
      const loadTabUrl = (index) => {
        const tab = props.tabList[index];
        if (!tab || !tab.dest) return;

        // 构建iframe URL：mp_ + dest + .html
        const fileName = `mp_${tab.dest}.html`;

        // 构建完整URL，包含所有参数
        const tabService =
          tab.service ||
          tab.servName ||
          props.baseParams.service ||
          props.baseParams.ssServ ||
          "";
        const tabDest =
          tab.dest || props.baseParams.dest || props.baseParams.ssDest || "";
        const tabParam = tab.param || props.baseParams.param || "";
        const params = new URLSearchParams({
          ...props.baseParams,
          service: tabService,
          dest: tabDest,
          ssServ: tabService,
          ssDest: tabDest,
          param: tabParam,
        });

        currentTabUrl.value = `/page/${fileName}?${params.toString()}`;
        console.log(
          "🔄 切换到Tab:",
          tab.desc || tab.title,
          "加载页面:",
          currentTabUrl.value
        );
      };

      // 监听 activeIndex 变化
      watch(
        () => props.activeIndex,
        (newIndex) => {
          currentTab.value = newIndex;
          loadTabUrl(newIndex);
        },
        { immediate: true }
      );

      // 监听 tabList 变化
      watch(
        () => props.tabList,
        () => {
          if (props.tabList.length > 0 && currentTab.value === 0) {
            loadTabUrl(0);
          }
        },
        { immediate: true }
      );

      // 切换Tab
      const handleTabClick = (index) => {
        if (currentTab.value === index) return;

        currentTab.value = index;
        loadTabUrl(index);
        emit("tab-change", { index, tab: props.tabList[index] });
      };

      return {
        currentTab,
        currentTabUrl,
        handleTabClick,
      };
    },
    template: `
			<div class="ss-sub-tab">
				<!-- Tab 栏 -->
				<div class="ss-sub-tab__bar" v-if="tabList.length > 0">
					<div
						v-for="(tab, index) in tabList"
						:key="index"
						class="ss-sub-tab__item"
						:class="{ 'ss-sub-tab__item--active': currentTab === index }"
						@click="handleTabClick(index)"
					>
						{{ tab.desc || tab.title }}
					</div>
				</div>

				<!-- 内容区域 -->
				<div class="ss-sub-tab__content" v-if="currentTabUrl">
					<iframe :src="currentTabUrl" frameborder="0"></iframe>
				</div>
			</div>
		`,
  };

  // ===== SsUploadFile 文件上传组件（支持单文件/多文件） =====
  const SsUploadFile = {
    name: "SsUploadFile",
    props: {
      // v-model 绑定的值（单文件：String，多文件：Array）
      modelValue: { type: [String, Array], default: "" },
      // 最大上传数量（默认1个）
      max: { type: Number, default: 1 },
      // 是否禁用
      disabled: { type: Boolean, default: false },
      // 允许的文件类型（默认常见文件类型）
      accept: {
        type: String,
        default: ".pdf,.doc,.docx,.xls,.xlsx,.txt,.zip,.rar",
      },
      // 最大文件大小（MB，默认5MB）
      maxSize: { type: Number, default: 5 },
    },
    emits: ["update:modelValue", "uploaded"],
    setup(props, { emit }) {
      // 文件列表（统一用数组管理）
      const fileList = ref([]);

      // 监听 modelValue 变化，同步到 fileList
      watch(
        () => props.modelValue,
        (newVal) => {
          if (props.max === 1) {
            // 单文件模式
            fileList.value = newVal
              ? [{ path: newVal, name: getFileName(newVal) }]
              : [];
          } else {
            // 多文件模式
            if (Array.isArray(newVal)) {
              fileList.value = newVal.map((path) => ({
                path,
                name: getFileName(path),
              }));
            } else {
              fileList.value = newVal
                ? [{ path: newVal, name: getFileName(newVal) }]
                : [];
            }
          }
        },
        { immediate: true }
      );

      // 是否可以继续添加文件
      const canAddMore = computed(() => {
        return fileList.value.length < props.max;
      });

      // 从路径中提取文件名
      const getFileName = (path) => {
        if (!path) return "";
        const parts = path.split("/");
        return parts[parts.length - 1];
      };

      // 获取文件图标
      const getFileIcon = (fileName) => {
        const ext = fileName.split(".").pop().toLowerCase();
        const iconMap = {
          pdf: "icon-pdf",
          doc: "icon-word",
          docx: "icon-word",
          xls: "icon-excel",
          xlsx: "icon-excel",
          txt: "icon-txt",
          zip: "icon-zip",
          rar: "icon-zip",
        };
        return iconMap[ext] || "icon-wenjian";
      };

      // 选择文件
      const selectFile = () => {
        if (props.disabled || !canAddMore.value) return;
        const input = document.createElement("input");
        input.type = "file";
        input.accept = props.accept;
        input.onchange = async (e) => {
          const file = e.target.files[0];
          if (!file) return;

          // 检查文件大小
          const fileSizeMB = file.size / 1024 / 1024;
          if (fileSizeMB > props.maxSize) {
            window.showToast?.(`文件大小不能超过${props.maxSize}MB`, "error");
            return;
          }

          // 上传文件
          try {
            const serverPath = await uploadFile(file, "file", file.name);
            const newList = [
              ...fileList.value,
              { path: serverPath, name: file.name },
            ];
            updateModelValue(newList);
            emit("uploaded", serverPath);
          } catch (error) {
            console.error("文件上传失败:", error);
          }
        };
        input.click();
      };

      // 删除文件
      const deleteFile = (index) => {
        const newList = fileList.value.filter((_, i) => i !== index);
        updateModelValue(newList);
      };

      // 下载文件
      const downloadFile = (file) => {
        const url =
          window.SS.utils?.getFileUrl?.(file.path) ||
          window.getFileUrl?.(file.path) ||
          file.path;
        window.open(url, "_blank");
      };

      // 更新 modelValue
      const updateModelValue = (list) => {
        const paths = list.map((f) => f.path);
        if (props.max === 1) {
          // 单文件模式：返回 String
          emit("update:modelValue", paths[0] || "");
        } else {
          // 多文件模式：返回 Array
          emit("update:modelValue", paths);
        }
      };

      return {
        fileList,
        canAddMore,
        getFileIcon,
        selectFile,
        deleteFile,
        downloadFile,
      };
    },
    template: `
			<div class="ss-upload-file">
				<!-- 文件列表 -->
				<div v-if="fileList.length > 0" class="file-list">
					<!-- 已上传的文件 -->
					<div
						v-for="(file, index) in fileList"
						:key="index"
						class="file-item"
					>
						<div class="file-icon">
							<Icon :name="getFileIcon(file.name)" size="40" color="#40ac6d" />
						</div>
						<div class="file-info" @click="downloadFile(file)">
							<span class="file-name">{{ file.name }}</span>
						</div>
						<!-- 文件操作按钮 -->
						<div class="file-actions">
							<div class="file-download" @click="downloadFile(file)">
								<Icon name="icon-xiazai" size="32" color="#40ac6d" />
							</div>
							<div v-if="!disabled" class="file-delete" @click.stop="deleteFile(index)">
								<Icon name="icon-guanbi" size="32" color="#ff3b30" />
							</div>
						</div>
					</div>
				</div>

				<!-- 添加按钮 -->
				<div
					v-if="canAddMore && !disabled"
					class="file-add-button"
					@click="selectFile"
				>
					<div class="add-icon">
						<Icon name="icon-tianjia" size="64" color="#999" />
					</div>
					<span class="add-text">上传文件 ({{ fileList.length }}/{{ max }})</span>
					<span class="file-tip">支持格式: {{ accept }}，最大{{ maxSize }}MB</span>
				</div>
			</div>
		`,
  };

  /**
   * 功能说明：H5移动端富文本组件（对齐PC字段协议 mswj/fjid/path 回显） by xu 2026-03-01
   *
   * 约定：
   * - v-model 绑定文件路径字段（如 mswj）
   * - 组件内部维护编辑内容，并输出隐藏字段：xxEdit / xxwj / ueditorpath / fjid
   * - 回显通过 url + path 拉取 HTML 内容，不直接依赖 modelValue 的 HTML 字符串
   *
   * @component SsEditor
   * @prop {String} modelValue 文件路径（如 mswj）
   * @prop {String} name 字段名（默认 mswj）
   * @prop {String} url 回显读取接口地址
   * @prop {Number|String} height 编辑器高度
   * @prop {String} placeholder 占位文案
   * @prop {Boolean} readonly 是否只读
   * @prop {String} uploadUrl 上传接口地址
   * @prop {Object} param 附件参数（button.cmsAddUrl / button.cmsUpdUrl / mode）
   * @emits update:modelValue 更新文件路径
   * @emits ready 编辑器就绪
   * @emits change 内容变化
   */
  const SsEditor = {
    name: "SsEditor",
    props: {
      modelValue: { type: String, default: "" },
      name: { type: String, default: "mswj" },
      url: { type: String, default: "" },
      height: { type: [Number, String], default: 280 },
      placeholder: { type: String, default: "请输入内容" },
      readonly: { type: Boolean, default: false },
      uploadUrl: { type: String, default: "/service?ssServ=ulByHttp" },
      param: { type: Object, default: () => ({}) },
    },
    emits: ["update:modelValue", "ready", "change"],
    setup(props, { emit }) {
      const editorElementId = `ss-editor-${Date.now()}-${Math.floor(
        Math.random() * 10000
      )}`;
      const editorContent = ref("");
      const editorInstance = ref(null);
      const fjid = ref(props.param?.button?.val || "");
      const fjName = props.param?.button?.desc || "附件";
      const mode = props.param?.mode;

      /**
       * 功能说明：确保附件 fjid 存在，不存在时先通过 cmsAddUrl 创建 by xu 2026-03-01
       * @returns {Promise<string>} fjid
       */
      const ensureFjid = async () => {
        if (fjid.value) return fjid.value;
        if (!props.param?.button?.cmsAddUrl) return "";
        return new Promise((resolve) => {
          $.ajax({
            type: "post",
            url: props.param.button.cmsAddUrl,
            async: false,
            data: {
              name: "fjid",
              ssNrObjName: "sh",
              ssNrObjId: "",
            },
            success: (_fjid) => {
              fjid.value = _fjid || "";
              resolve(fjid.value);
            },
            error: () => resolve(""),
          });
        });
      };

      /**
       * 功能说明：打开附件管理弹窗（与PC端行为一致） by xu 2026-03-01
       * @returns {Promise<void>}
       */
      const openAttachmentDialog = async () => {
        if (!props.param?.button?.cmsUpdUrl) {
          console.warn("未配置附件编辑地址 cmsUpdUrl");
          return;
        }
        const currentFjid = await ensureFjid();
        if (!currentFjid) return;

        const query =
          `&nrid=T-${currentFjid}` +
          `&objectId=${currentFjid}` +
          `&objectName=${encodeURIComponent(fjName)}` +
          `&callback=${window["fjidCallbackName"] || ""}`;

        if (window.SS && typeof window.SS.openDialog === "function") {
          window.SS.openDialog({
            src: props.param.button.cmsUpdUrl + query,
            headerTitle: "编辑",
            width: 900,
            high: 664,
            zIndex: 51,
          });
        }
      };

      /**
       * 功能说明：初始化 Jodit 编辑器并绑定工具栏/上传/change 事件 by xu 2026-03-01
       * @returns {void}
       */
      const buildEditor = () => {
        if (!window.Jodit || !window.Jodit.make) {
          console.error("Jodit 未加载，无法初始化 ss-editor");
          return;
        }

        const editorUploadUrl = props.uploadUrl.includes("?")
          ? `${props.uploadUrl}&type=img`
          : `${props.uploadUrl}?type=img`;

        const instance = window.Jodit.make(`#${editorElementId}`, {
          height: props.height,
          placeholder: props.placeholder,
          readonly: props.readonly,
          language: "zh_cn",
          showXPathInStatusbar: false,
          showCharsCounter: false,
          showWordsCounter: false,
          allowResizeY: false,
          toolbarSticky: false,
          statusbar: false,
          uploader: {
            url: editorUploadUrl,
            format: "json",
            method: "POST",
            filesVariableName: (i) => `imgs[${i}]`,
            isSuccess: (resp) => resp?.code === 0 || !!resp?.data,
            getMessage: (resp) => resp?.msg || "上传失败",
            process: (resp) => resp?.data?.url || resp?.data?.path || "",
            contentType: () => false,
          },
          controls: {
            customLinkButton: {
              name: "link",
              tooltip: "附件",
              exec: () => {
                openAttachmentDialog();
              },
            },
          },
          buttons: [
            "fullsize",
            "bold",
            "italic",
            "underline",
            "|",
            "font",
            "fontsize",
            "|",
            "left",
            "center",
            "right",
            "|",
            "ul",
            "ol",
            "|",
            "image",
            "table",
            "customLinkButton",
            "|",
            "undo",
            "redo",
          ],
          buttonsMD: [
            "bold",
            "italic",
            "underline",
            "|",
            "image",
            "customLinkButton",
            "|",
            "dots",
          ],
          buttonsSM: [
            "bold",
            "italic",
            "|",
            "image",
            "customLinkButton",
            "|",
            "dots",
          ],
          buttonsXS: ["bold", "|", "dots"],
        });

        instance.value = editorContent.value || "";
        instance.events.on("change", () => {
          editorContent.value = instance.value || "";
          emit("change", editorContent.value);
        });

        editorInstance.value = instance;
        emit("ready", instance);
      };

      /**
       * 功能说明：按路径加载富文本HTML内容并回填编辑器 by xu 2026-03-01
       * @returns {Promise<void>}
       */
      const loadContentByPath = async () => {
        if (!props.url || !props.modelValue) return;
        try {
          const params = new URLSearchParams();
          if (mode) params.append("mode", mode);
          params.append("path", props.modelValue);

          const response = await window.axios.post(props.url, params, {
            headers: { "Content-Type": "application/x-www-form-urlencoded" },
          });

          const content = response?.data?.content || "";
          if (content) {
            editorContent.value = content;
            if (editorInstance.value) {
              editorInstance.value.value = content;
            }
          }

          const filePath = response?.data?.path;
          if (filePath) {
            emit("update:modelValue", filePath);
          }
        } catch (error) {
          console.error("ss-editor 回显内容加载失败:", error);
        }
      };

      onMounted(async () => {
        buildEditor();
        await loadContentByPath();
      });

      watch(
        () => props.readonly,
        (newVal) => {
          if (
            editorInstance.value &&
            typeof editorInstance.value.setReadOnly === "function"
          ) {
            editorInstance.value.setReadOnly(newVal);
          }
        }
      );

      onBeforeUnmount(() => {
        if (
          editorInstance.value &&
          typeof editorInstance.value.destruct === "function"
        ) {
          editorInstance.value.destruct();
        }
      });

      return {
        editorElementId,
        editorContent,
        fjid,
      };
    },
    template: `
			<div class="ss-editor-container">
				<input v-if="fjid" type="hidden" name="fjid" :value="fjid" />
				<input type="hidden" :name="name.replace(/wj$/, '') + 'Edit'" :value="editorContent" />
				<input type="hidden" :name="name.replace(/wj$/, '') + 'wj'" :value="modelValue" />
				<input type="hidden" name="ueditorpath" value="mswj" />
				<textarea :id="editorElementId"></textarea>
			</div>
		`,
  };

  window.SS.dom.initializeFormApp = function (config) {
    const { el, ...vueOptions } = config;
    const app = createApp({
      ...vueOptions,
    });

    // 注册组件
    // app.component("SsLoginIcon", SsLoginIcon);
    // app.component("SsMark", SsMark);
    // app.component("SsFullStyleHeader", SsFullStyleHeader);
    // app.component("SsDialog", SsDialog);
    app.component("SsInput", SsInput);
    app.component("SsBottom", SsBottom);
    app.component("SsCard", SsCard);
    app.component("SsSearchButton", SsSearchButton);
    app.component("SsSelect", SsSelect);
    app.component("Icon", Icon);
    // 注册 Vant 组件
    const vantLib = window.vant || window.Vant;
    console.log("🔍 检查 Vant:", {
      hasVant: !!window.vant,
      hasVantCap: !!window.Vant,
      vantLib: !!vantLib,
      vantKeys: vantLib ? Object.keys(vantLib).slice(0, 20) : [],
      hasPopup: vantLib?.Popup,
      hasDatetimePicker: vantLib?.DatetimePicker,
      hasDatePicker: vantLib?.DatePicker,
      hasTimePicker: vantLib?.TimePicker,
      allKeys: vantLib ? Object.keys(vantLib) : [],
    });

    if (vantLib) {
      try {
        // 使用 Vant 的 use 方法注册所有组件
        app.use(vantLib);
        console.log("✅ Vant 全部组件注册成功");
      } catch (error) {
        console.error("❌ Vant 组件注册失败:", error);
        // 降级方案：手动注册具体组件
        try {
          app.component("van-popup", vantLib.Popup);
          app.component("van-datetime-picker", vantLib.DatetimePicker);
          console.log("✅ Vant 手动注册成功");
        } catch (e) {
          console.error("❌ Vant 手动注册也失败:", e);
        }
      }
    } else {
      console.warn("⚠️ Vant 未加载");
    }

    // 注册组件 - 统一使用 kebab-case
    app.component("ss-verify", SsVerify);
    app.component("ss-verify-node", SsVerifyNode);

    app.component("ss-common-icon", SsCommonIcon);
    app.component("ss-onoff-button", SsOnoffButton);
    app.component("ss-datetime-picker", SsDatetimePicker);
    app.component("ss-confirm", SsConfirm);
    app.component("ss-image-cropper", SsImageCropper);
    app.component("ss-upload-image", SsUploadImage);
    app.component("ss-upload-file", SsUploadFile);
    app.component("ss-car-card", SsCarCard);
    app.component("ss-sub-tab", SsSubTab);
    app.component("ss-editor", SsEditor);
    app.component("SsEditor", SsEditor);

    // app.component("SsObjp", SsObjp);
    // app.component("SsHidden", SsHidden);
    // app.component("SsCcp", SsCcp);
    // app.component("SsDatePicker", SsDatePicker);
    // app.component("SsIcon", SsIcon);
    // app.component("SsCommonIcon", SsCommonIcon);
    // app.component("SsBreadcrumb", SsBreadcrumb);
    // app.component("SsEditor", SsEditor);
    // app.component("SsDialogIcon", SsDialogIcon);
    // app.component("SsBottomButton", SsBottomButton);
    // app.component("SsNavIcon", SsNavIcon);
    // app.component("SsHeaderIcon", SsHeaderIcon);
    // app.component("SsGolbalMenuIcon", SsGolbalMenuIcon);
    // app.component("SsCartListIcon", SsCartListIcon);
    // app.component("SsQuickIcon", SsQuickIcon);
    // app.component("SsFormIcon", SsFormIcon);
    // app.component("SsBottomDivIcon", SsBottomDivIcon);
    // app.component("SsEditorIcon", SsEditorIcon);
    // app.component("SsValidate", SsValidate);
    // app.component("SsOnoffbutton", SsOnoffbutton);
    // app.component("SsOnoffbuttonArray", SsOnoffbuttonArray);
    // app.component("SsTextarea", SsTextarea);
    // app.component("SsLoginInput", SsLoginInput);
    // app.component("SsLoginButton", SsLoginButton);
    // app.component("SsSearch", SsSearch);
    // app.component("SsCartItem", SsCartItem);
    // app.component("SsCartItem2", SsCartItem2);
    // app.component("SsListCard", SsListCard);
    // app.component("SsFolderCard", SsFolderCard);
    // app.component("SsFolderCartView", SsFolderCartView);
    // app.component("SsPage", SsPage);
    // app.component("SsRightInfo", SSRightInfo);
    // app.component("SsSuccessPopup", SsSuccessPopup);
    // app.component("SsErrorDialog", SsErrorDialog);
    // app.component("SsVerify", SsVerify);
    // app.component("SsVerifyNode", SsVerifyNode);
    // app.component("SsOrcImgBox", SsOrcImgBox);
    // app.component("ss-search-input", SsSearchInput);
    // app.component("ss-search-date-picker", SsSearchDatePicker);

    // app.component("ss-search-button", SsSearchButton);
    // app.component("ss-drop-button", SsDropButton);
    // app.component("ss-sub-tab", SsSubTab);
    // app.component("ss-img", SsImgUpload);
    // 设置为中文
    // app.use(ElementPlus, {
    //   locale: ElementPlusLocaleZhCn,
    // });
    // console.log(ElementPlus);
    // 确保 ElementPlusIconsVue
    // if (window.ElementPlusIconsVue) {
    //   // 注册 Element Plus 图标组件
    //   for (const [key, component] of Object.entries(
    //     window.ElementPlusIconsVue
    //   )) {
    //     console.log(key, component);
    //     app.component(key, component);
    //   }
    // }
    // 挂载首页的组件
    // for (const componentName in IndexComponents) {
    //   app.component(componentName, IndexComponents[componentName]);
    // }
    // 挂载echarts的组件
    // for (const componentName in EchartComponents) {
    //   app.component(componentName, EchartComponents[componentName]);
    // }
    // 挂载 Vue 应用
    const vm = app.mount(el);
    vm.data = vueOptions.data();

    return vm;
  };
})();
