@aspire-ui/element-component-pro 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- .ecp-pro-table[data-v-f3d27813]{padding:16px;background:#fff;width:100%;box-sizing:border-box}.ecp-pro-table[data-v-f3d27813] .el-table{width:100%!important}.ecp-pro-table__header[data-v-f3d27813]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.ecp-pro-table__title-wrapper[data-v-f3d27813]{display:flex;align-items:center;gap:4px}.ecp-pro-table__title[data-v-f3d27813]{font-size:16px;font-weight:600}.ecp-pro-table__help[data-v-f3d27813]{color:#909399;cursor:help}.ecp-pro-table__toolbar[data-v-f3d27813]{display:flex;align-items:center;gap:8px}.ecp-pro-table__body[data-v-f3d27813]{width:100%}.ecp-pro-table__pagination[data-v-f3d27813]{margin-top:16px;display:flex;justify-content:flex-end}.ecp-pro-table__col-help[data-v-f3d27813]{margin-left:4px;color:#909399;cursor:help}.ecp-table-action[data-v-45a58e7c],.ecp-table-action__item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-table-action__icon[data-v-45a58e7c]{margin-right:4px}.ecp-table-action__more[data-v-45a58e7c]{display:inline-flex;align-items:center}.ecp-table-action__dropdown-item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-tree-select[data-v-f30bba11]{position:relative;width:100%}.ecp-tree-select__filter-inner[data-v-f30bba11]{margin-bottom:8px}.ecp-tree-select__dropdown[data-v-f30bba11]{position:absolute;top:100%;left:0;right:0;max-height:280px;overflow:auto;background:#fff;border:1px solid #dcdfe6;border-radius:4px;margin-top:4px;z-index:1000;padding:8px}.ecp-tree-select__loading[data-v-f30bba11]{padding:24px;text-align:center;color:#909399;font-size:14px}.ecp-pro-form-item__colon[data-v-48d7960b]{margin-right:2px}.ecp-pro-form-item__help-icon[data-v-48d7960b]{margin-left:4px;color:#909399;cursor:help;font-size:14px}.ecp-pro-form-item__help-icon[data-v-48d7960b]:hover{color:#409eff}.ecp-pro-form-item__help-item[data-v-48d7960b]{margin-bottom:4px}.ecp-pro-form-item__help-item[data-v-48d7960b]:last-child{margin-bottom:0}.ecp-form-actions[data-v-489c88d2]{text-align:right}.ecp-form-actions__advance[data-v-489c88d2]{margin-right:8px}.el-icon-d-arrow-left.up[data-v-489c88d2]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-489c88d2]{transform:rotate(-90deg)}.ecp-pro-form[data-v-7ee469dc]{padding:16px;position:relative}.ecp-pro-form__advance[data-v-7ee469dc]{margin-bottom:16px}.ecp-pro-form_col[data-v-7ee469dc]{position:relative;float:right}.el-icon-d-arrow-left.up[data-v-7ee469dc]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-7ee469dc]{transform:rotate(-90deg)}.ecp-form-actions__advance[data-v-7ee469dc]{position:absolute;bottom:0;left:50%;transform:translate(-50%,-50%)}.ecp-pro-descriptions[data-v-656036f6]{width:100%;box-sizing:border-box}.ecp-pro-descriptions__header[data-v-656036f6]{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:12px}.ecp-pro-descriptions__title-wrap[data-v-656036f6]{display:flex;align-items:center;gap:6px}.ecp-pro-descriptions__title[data-v-656036f6]{font-size:16px;font-weight:600;color:#303133}.ecp-pro-descriptions__help[data-v-656036f6],.ecp-pro-descriptions__toggle[data-v-656036f6]{color:#909399}.ecp-pro-descriptions__toggle .el-icon-arrow-down[data-v-656036f6]{margin-left:4px;transition:transform .2s ease}.ecp-pro-descriptions__toggle .el-icon-arrow-down.is-expanded[data-v-656036f6]{transform:rotate(180deg)}.ecp-pro-descriptions__body[data-v-656036f6]{display:grid;border-top:1px solid #ebeef5;border-left:1px solid #ebeef5;overflow:hidden}.ecp-pro-descriptions__body.is-collapsed[data-v-656036f6]{overflow:hidden}.ecp-pro-descriptions__body[data-v-656036f6]:not(.is-bordered){border-top:0;border-left:0;gap:12px 16px}.ecp-pro-descriptions__item[data-v-656036f6]{display:flex;min-width:0;border-right:1px solid #ebeef5;border-bottom:1px solid #ebeef5}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__item[data-v-656036f6]{border-right:0;border-bottom:0}.ecp-pro-descriptions__label[data-v-656036f6],.ecp-pro-descriptions__content[data-v-656036f6]{min-width:0;box-sizing:border-box;word-break:break-word}.ecp-pro-descriptions__label[data-v-656036f6]{flex:0 0 120px;padding:12px 16px;color:#606266;background:#fafafa}.ecp-pro-descriptions__content[data-v-656036f6]{flex:1;padding:12px 16px;color:#303133;background:#fff}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__label[data-v-656036f6]{flex-basis:auto;padding:0;margin-right:8px;background:transparent;font-weight:500}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__content[data-v-656036f6]{padding:0;background:transparent}.ecp-pro-descriptions__body.is-small .ecp-pro-descriptions__label[data-v-656036f6],.ecp-pro-descriptions__body.is-small .ecp-pro-descriptions__content[data-v-656036f6]{padding-top:8px;padding-bottom:8px;font-size:13px}@media (max-width: 767px){.ecp-pro-descriptions__item[data-v-656036f6]{flex-direction:column}.ecp-pro-descriptions__label[data-v-656036f6]{flex-basis:auto}}.ecp-pro-table-form__form[data-v-44aa7592] .el-form-item{margin-bottom:0}.ecp-pro-table-form__cell-item[data-v-44aa7592]{width:100%}.ecp-pro-table-form__cell-item[data-v-44aa7592] .el-form-item__content{margin-left:0!important;line-height:normal}.ecp-pro-table-form__fixed-label[data-v-44aa7592]{color:#303133;font-size:14px}.ecp-pro-table-form__req[data-v-44aa7592]{color:#f56c6c;margin-right:2px}.ecp-pro-table-form__th-text[data-v-44aa7592]{font-weight:500;color:#606266}.ecp-pro-table-form__action-title[data-v-44aa7592]{margin-right:8px;font-size:13px;color:#606266}.ecp-pro-table-form__add-btn[data-v-44aa7592]{padding:0;font-size:14px}.ecp-pro-table-form__del-btn[data-v-44aa7592]{padding:0;color:#909399}.ecp-pro-table-form__del-btn[data-v-44aa7592]:not(:disabled){color:#409eff}.ecp-pro-table-form .ecp-pro-table-form__header-cell{background:#f5f7fa!important}
1
+ .ecp-pro-table[data-v-f3d27813]{padding:16px;background:#fff;width:100%;box-sizing:border-box}.ecp-pro-table[data-v-f3d27813] .el-table{width:100%!important}.ecp-pro-table__header[data-v-f3d27813]{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.ecp-pro-table__title-wrapper[data-v-f3d27813]{display:flex;align-items:center;gap:4px}.ecp-pro-table__title[data-v-f3d27813]{font-size:16px;font-weight:600}.ecp-pro-table__help[data-v-f3d27813]{color:#909399;cursor:help}.ecp-pro-table__toolbar[data-v-f3d27813]{display:flex;align-items:center;gap:8px}.ecp-pro-table__body[data-v-f3d27813]{width:100%}.ecp-pro-table__pagination[data-v-f3d27813]{margin-top:16px;display:flex;justify-content:flex-end}.ecp-pro-table__col-help[data-v-f3d27813]{margin-left:4px;color:#909399;cursor:help}.ecp-table-action[data-v-45a58e7c],.ecp-table-action__item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-table-action__icon[data-v-45a58e7c]{margin-right:4px}.ecp-table-action__more[data-v-45a58e7c]{display:inline-flex;align-items:center}.ecp-table-action__dropdown-item[data-v-45a58e7c]{display:inline-flex;align-items:center;gap:4px}.ecp-tree-select[data-v-f30bba11]{position:relative;width:100%}.ecp-tree-select__filter-inner[data-v-f30bba11]{margin-bottom:8px}.ecp-tree-select__dropdown[data-v-f30bba11]{position:absolute;top:100%;left:0;right:0;max-height:280px;overflow:auto;background:#fff;border:1px solid #dcdfe6;border-radius:4px;margin-top:4px;z-index:1000;padding:8px}.ecp-tree-select__loading[data-v-f30bba11]{padding:24px;text-align:center;color:#909399;font-size:14px}.ecp-pro-form-item__colon[data-v-48d7960b]{margin-right:2px}.ecp-pro-form-item__help-icon[data-v-48d7960b]{margin-left:4px;color:#909399;cursor:help;font-size:14px}.ecp-pro-form-item__help-icon[data-v-48d7960b]:hover{color:#409eff}.ecp-pro-form-item__help-item[data-v-48d7960b]{margin-bottom:4px}.ecp-pro-form-item__help-item[data-v-48d7960b]:last-child{margin-bottom:0}.ecp-form-actions[data-v-489c88d2]{text-align:right}.ecp-form-actions__advance[data-v-489c88d2]{margin-right:8px}.el-icon-d-arrow-left.up[data-v-489c88d2]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-489c88d2]{transform:rotate(-90deg)}.ecp-pro-form[data-v-80bac8be]{padding:16px;position:relative}.ecp-pro-form__advance[data-v-80bac8be]{margin-bottom:16px}.ecp-pro-form_col[data-v-80bac8be]{position:relative;float:right}.el-icon-d-arrow-left.up[data-v-80bac8be]{transform:rotate(90deg)}.el-icon-d-arrow-left.down[data-v-80bac8be]{transform:rotate(-90deg)}.ecp-form-actions__advance[data-v-80bac8be]{position:absolute;bottom:0;left:50%;transform:translate(-50%,-50%)}.ecp-pro-descriptions[data-v-656036f6]{width:100%;box-sizing:border-box}.ecp-pro-descriptions__header[data-v-656036f6]{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:12px}.ecp-pro-descriptions__title-wrap[data-v-656036f6]{display:flex;align-items:center;gap:6px}.ecp-pro-descriptions__title[data-v-656036f6]{font-size:16px;font-weight:600;color:#303133}.ecp-pro-descriptions__help[data-v-656036f6],.ecp-pro-descriptions__toggle[data-v-656036f6]{color:#909399}.ecp-pro-descriptions__toggle .el-icon-arrow-down[data-v-656036f6]{margin-left:4px;transition:transform .2s ease}.ecp-pro-descriptions__toggle .el-icon-arrow-down.is-expanded[data-v-656036f6]{transform:rotate(180deg)}.ecp-pro-descriptions__body[data-v-656036f6]{display:grid;border-top:1px solid #ebeef5;border-left:1px solid #ebeef5;overflow:hidden}.ecp-pro-descriptions__body.is-collapsed[data-v-656036f6]{overflow:hidden}.ecp-pro-descriptions__body[data-v-656036f6]:not(.is-bordered){border-top:0;border-left:0;gap:12px 16px}.ecp-pro-descriptions__item[data-v-656036f6]{display:flex;min-width:0;border-right:1px solid #ebeef5;border-bottom:1px solid #ebeef5}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__item[data-v-656036f6]{border-right:0;border-bottom:0}.ecp-pro-descriptions__label[data-v-656036f6],.ecp-pro-descriptions__content[data-v-656036f6]{min-width:0;box-sizing:border-box;word-break:break-word}.ecp-pro-descriptions__label[data-v-656036f6]{flex:0 0 120px;padding:12px 16px;color:#606266;background:#fafafa}.ecp-pro-descriptions__content[data-v-656036f6]{flex:1;padding:12px 16px;color:#303133;background:#fff}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__label[data-v-656036f6]{flex-basis:auto;padding:0;margin-right:8px;background:transparent;font-weight:500}.ecp-pro-descriptions__body:not(.is-bordered) .ecp-pro-descriptions__content[data-v-656036f6]{padding:0;background:transparent}.ecp-pro-descriptions__body.is-small .ecp-pro-descriptions__label[data-v-656036f6],.ecp-pro-descriptions__body.is-small .ecp-pro-descriptions__content[data-v-656036f6]{padding-top:8px;padding-bottom:8px;font-size:13px}@media (max-width: 767px){.ecp-pro-descriptions__item[data-v-656036f6]{flex-direction:column}.ecp-pro-descriptions__label[data-v-656036f6]{flex-basis:auto}}.ecp-collapse-container[data-v-087dba3a]{background:#fff;border-radius:14px;border:1px solid #e8eef8;box-shadow:0 10px 24px #0f2d5e0f;overflow:hidden}.ecp-collapse-container.is-ghost[data-v-087dba3a]{border-color:transparent;box-shadow:none;background:transparent}.ecp-collapse-container__header[data-v-087dba3a]{min-height:56px;padding:0 20px;display:flex;align-items:center;justify-content:space-between;gap:16px;border-bottom:1px solid #eef3fb}.ecp-collapse-container.is-ghost .ecp-collapse-container__header[data-v-087dba3a]{padding-left:0;padding-right:0}.ecp-collapse-container__header-main[data-v-087dba3a]{min-width:0;flex:1;display:flex;align-items:center}.ecp-collapse-container__title-wrap[data-v-087dba3a]{min-width:0;display:inline-flex;align-items:center;gap:8px}.ecp-collapse-container__title[data-v-087dba3a]{color:#1f2d3d;font-size:16px;font-weight:600;line-height:1.4}.ecp-collapse-container__help[data-v-087dba3a]{color:#91a3b7;font-size:14px}.ecp-collapse-container__header-extra[data-v-087dba3a]{display:inline-flex;align-items:center;justify-content:flex-end;gap:12px;flex-shrink:0}.ecp-collapse-container__toggle[data-v-087dba3a]{padding:0;color:#2f6fd3}.ecp-collapse-container__toggle i[data-v-087dba3a]{margin-left:6px;transition:transform .2s ease}.ecp-collapse-container__toggle i.is-expanded[data-v-087dba3a]{transform:rotate(180deg)}.ecp-collapse-container__body[data-v-087dba3a]{padding:20px;box-sizing:border-box}.ecp-collapse-container.is-ghost .ecp-collapse-container__body[data-v-087dba3a]{padding-left:0;padding-right:0}.ecp-collapse-container.is-header-clickable .ecp-collapse-container__header-main[data-v-087dba3a]{cursor:pointer}@media (max-width: 768px){.ecp-collapse-container__header[data-v-087dba3a]{padding:14px 16px;align-items:flex-start;flex-direction:column}.ecp-collapse-container__header-extra[data-v-087dba3a]{width:100%;justify-content:space-between}.ecp-collapse-container__body[data-v-087dba3a]{padding:16px}}.ecp-pro-table-form__form[data-v-44aa7592] .el-form-item{margin-bottom:0}.ecp-pro-table-form__cell-item[data-v-44aa7592]{width:100%}.ecp-pro-table-form__cell-item[data-v-44aa7592] .el-form-item__content{margin-left:0!important;line-height:normal}.ecp-pro-table-form__fixed-label[data-v-44aa7592]{color:#303133;font-size:14px}.ecp-pro-table-form__req[data-v-44aa7592]{color:#f56c6c;margin-right:2px}.ecp-pro-table-form__th-text[data-v-44aa7592]{font-weight:500;color:#606266}.ecp-pro-table-form__action-title[data-v-44aa7592]{margin-right:8px;font-size:13px;color:#606266}.ecp-pro-table-form__add-btn[data-v-44aa7592]{padding:0;font-size:14px}.ecp-pro-table-form__del-btn[data-v-44aa7592]{padding:0;color:#909399}.ecp-pro-table-form__del-btn[data-v-44aa7592]:not(:disabled){color:#409eff}.ecp-pro-table-form .ecp-pro-table-form__header-cell{background:#f5f7fa!important}
@@ -28,12 +28,44 @@ export interface ScrollToFieldOptions {
28
28
  /** 行内对齐 */
29
29
  inline?: ScrollLogicalPosition;
30
30
  }
31
+ export interface ProFormFieldOption {
32
+ label: string;
33
+ value: unknown;
34
+ }
35
+ export interface GetFieldOptions {
36
+ (field: string): ProFormFieldOption[];
37
+ (field: string, raw: false): ProFormFieldOption[];
38
+ (field: string, raw: true): unknown[];
39
+ }
40
+ export interface CollapseContainerProps {
41
+ title?: string;
42
+ loading?: boolean;
43
+ /** 与 Vben Admin 保持一致,沿用 canExpan 命名 */
44
+ canExpan?: boolean;
45
+ /** canExpan 的别名,便于业务侧按语义使用 */
46
+ canExpand?: boolean;
47
+ helpMessage?: string | string[];
48
+ triggerWindowResize?: boolean;
49
+ expanded?: boolean;
50
+ defaultExpand?: boolean;
51
+ ghost?: boolean;
52
+ expandButtonText?: string;
53
+ collapseButtonText?: string;
54
+ /** 根容器透传属性,支持 style/class/id/data-* 等 */
55
+ wrapperProps?: Record<string, unknown>;
56
+ /** 头部额外 class */
57
+ headerClass?: string;
58
+ /** 头部额外样式 */
59
+ headerStyle?: Record<string, string | number>;
60
+ /** 内容区额外 class */
61
+ contentClass?: string;
62
+ /** 内容区额外样式 */
63
+ contentStyle?: Record<string, string | number>;
64
+ }
31
65
  /** ApiSelect 暴露的实例类型 */
32
66
  export interface ApiSelectInstance {
33
- options: Array<{
34
- label: string;
35
- value: unknown;
36
- }>;
67
+ options: ProFormFieldOption[];
68
+ rawOptions: unknown[];
37
69
  loading: boolean;
38
70
  fetchOptions: () => Promise<void>;
39
71
  }
@@ -53,11 +85,8 @@ export interface FormActionType {
53
85
  setProps: (props: Partial<ProFormProps>) => Promise<void>;
54
86
  /** 获取指定字段的组件实例 */
55
87
  getComponentInstance: (field: string) => ComponentPublicInstance | null;
56
- /** 获取 api-select 字段的 options */
57
- getFieldOptions: (field: string) => Array<{
58
- label: string;
59
- value: unknown;
60
- }>;
88
+ /** 获取 api-select 字段的 options,raw=true 时返回接口原始列表数据 */
89
+ getFieldOptions: GetFieldOptions;
61
90
  /** 获取 api-select 字段的加载状态 */
62
91
  isFieldLoading: (field: string) => boolean;
63
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspire-ui/element-component-pro",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Element UI 二次封装组件库,基于 Vue 2.7 + TypeScript + setup 语法糖,实现 VbenAdmin 风格的 Pro 组件",
5
5
  "type": "module",
6
6
  "main": "./dist/element-component-pro.umd.js",
@@ -0,0 +1,284 @@
1
+ <template>
2
+ <div
3
+ v-loading="effectiveProps.loading"
4
+ class="ecp-collapse-container"
5
+ :class="{
6
+ 'is-expanded': mergedExpanded,
7
+ 'is-ghost': effectiveProps.ghost,
8
+ 'is-header-clickable': canToggleByHeader,
9
+ }"
10
+ v-bind="wrapperProps"
11
+ >
12
+ <div v-if="showHeader" class="ecp-collapse-container__header" :class="effectiveProps.headerClass" :style="effectiveProps.headerStyle">
13
+ <div class="ecp-collapse-container__header-main" @click="handleHeaderClick">
14
+ <div v-if="$slots.title || effectiveProps.title" class="ecp-collapse-container__title-wrap">
15
+ <slot name="title">
16
+ <span class="ecp-collapse-container__title">{{ effectiveProps.title }}</span>
17
+ </slot>
18
+ <el-tooltip v-if="effectiveProps.helpMessage" placement="top" effect="dark">
19
+ <template slot="content">
20
+ <span v-if="Array.isArray(effectiveProps.helpMessage)">
21
+ <div v-for="(msg, index) in effectiveProps.helpMessage" :key="index">{{ msg }}</div>
22
+ </span>
23
+ <span v-else>{{ effectiveProps.helpMessage }}</span>
24
+ </template>
25
+ <i class="el-icon-question ecp-collapse-container__help" />
26
+ </el-tooltip>
27
+ </div>
28
+ </div>
29
+
30
+ <div v-if="$slots.action || resolvedCanExpan" class="ecp-collapse-container__header-extra" @click.stop>
31
+ <slot name="action" />
32
+ <el-button
33
+ v-if="resolvedCanExpan"
34
+ type="text"
35
+ class="ecp-collapse-container__toggle"
36
+ @click.stop="toggleExpand"
37
+ >
38
+ {{ mergedExpanded ? effectiveProps.collapseButtonText : effectiveProps.expandButtonText }}
39
+ <i class="el-icon-arrow-down" :class="{ 'is-expanded': mergedExpanded }" />
40
+ </el-button>
41
+ </div>
42
+ </div>
43
+
44
+ <el-collapse-transition>
45
+ <div
46
+ v-show="mergedExpanded"
47
+ class="ecp-collapse-container__body"
48
+ :class="effectiveProps.contentClass"
49
+ :style="effectiveProps.contentStyle"
50
+ >
51
+ <slot />
52
+ </div>
53
+ </el-collapse-transition>
54
+ </div>
55
+ </template>
56
+
57
+ <script setup lang="ts">
58
+ import { computed, ref, watch, useSlots } from 'vue'
59
+ import { useComponentSetting } from '../useComponentSetting'
60
+
61
+ interface CollapseContainerProps {
62
+ title?: string
63
+ loading?: boolean
64
+ canExpan?: boolean
65
+ canExpand?: boolean
66
+ helpMessage?: string | string[]
67
+ triggerWindowResize?: boolean
68
+ expanded?: boolean
69
+ defaultExpand?: boolean
70
+ ghost?: boolean
71
+ expandButtonText?: string
72
+ collapseButtonText?: string
73
+ wrapperProps?: Record<string, unknown>
74
+ headerClass?: string
75
+ headerStyle?: Record<string, string | number>
76
+ contentClass?: string
77
+ contentStyle?: Record<string, string | number>
78
+ }
79
+
80
+ const props = withDefaults(defineProps<CollapseContainerProps>(), {
81
+ loading: false,
82
+ canExpan: true,
83
+ canExpand: undefined,
84
+ triggerWindowResize: true,
85
+ expanded: undefined,
86
+ defaultExpand: true,
87
+ ghost: false,
88
+ expandButtonText: '展开',
89
+ collapseButtonText: '收起',
90
+ wrapperProps: () => ({}),
91
+ headerClass: '',
92
+ headerStyle: () => ({}),
93
+ contentClass: '',
94
+ contentStyle: () => ({}),
95
+ })
96
+
97
+ const emit = defineEmits<{
98
+ (e: 'update:expanded', value: boolean): void
99
+ (e: 'change', value: boolean): void
100
+ (e: 'collapse'): void
101
+ (e: 'expand'): void
102
+ }>()
103
+
104
+ const slots = useSlots()
105
+ const { mergeSettings } = useComponentSetting()
106
+
107
+ const innerExpanded = ref(props.expanded ?? props.defaultExpand)
108
+
109
+ const effectiveProps = computed(() => mergeSettings('CollapseContainer', { ...props }) as CollapseContainerProps)
110
+ const resolvedCanExpan = computed(() => effectiveProps.value.canExpand ?? effectiveProps.value.canExpan ?? true)
111
+ const isControlled = computed(() => effectiveProps.value.expanded !== undefined)
112
+ const mergedExpanded = computed(() => isControlled.value ? !!effectiveProps.value.expanded : innerExpanded.value)
113
+ const showHeader = computed(() => !!slots.title || !!slots.action || !!effectiveProps.value.title || !!effectiveProps.value.helpMessage || resolvedCanExpan.value)
114
+ const canToggleByHeader = computed(() => resolvedCanExpan.value)
115
+ const wrapperProps = computed(() => {
116
+ const incoming = effectiveProps.value.wrapperProps ?? {}
117
+ const wrapperClass = incoming.class
118
+ const wrapperStyle = incoming.style
119
+ const rest = { ...incoming }
120
+ delete rest.class
121
+ delete rest.style
122
+
123
+ return {
124
+ ...rest,
125
+ class: wrapperClass,
126
+ style: wrapperStyle,
127
+ }
128
+ })
129
+
130
+ const triggerWindowResize = () => {
131
+ if (!effectiveProps.value.triggerWindowResize || typeof window === 'undefined') return
132
+ window.setTimeout(() => {
133
+ window.dispatchEvent(new Event('resize'))
134
+ }, 220)
135
+ }
136
+
137
+ const setExpanded = (value: boolean) => {
138
+ if (!isControlled.value) {
139
+ innerExpanded.value = value
140
+ }
141
+ emit('update:expanded', value)
142
+ emit('change', value)
143
+ if (value) emit('expand')
144
+ else emit('collapse')
145
+ triggerWindowResize()
146
+ }
147
+
148
+ const toggleExpand = () => {
149
+ if (!resolvedCanExpan.value) return
150
+ setExpanded(!mergedExpanded.value)
151
+ }
152
+
153
+ const handleHeaderClick = () => {
154
+ if (!canToggleByHeader.value) return
155
+ toggleExpand()
156
+ }
157
+
158
+ watch(() => props.expanded, (value) => {
159
+ if (value !== undefined) {
160
+ innerExpanded.value = value
161
+ }
162
+ })
163
+
164
+ watch(() => props.defaultExpand, (value) => {
165
+ if (props.expanded === undefined) {
166
+ innerExpanded.value = value
167
+ }
168
+ })
169
+
170
+ defineExpose({
171
+ setExpanded,
172
+ toggleExpand,
173
+ })
174
+ </script>
175
+
176
+ <style scoped>
177
+ .ecp-collapse-container {
178
+ background: #fff;
179
+ border-radius: 14px;
180
+ border: 1px solid #e8eef8;
181
+ box-shadow: 0 10px 24px rgba(15, 45, 94, 0.06);
182
+ overflow: hidden;
183
+ }
184
+
185
+ .ecp-collapse-container.is-ghost {
186
+ border-color: transparent;
187
+ box-shadow: none;
188
+ background: transparent;
189
+ }
190
+
191
+ .ecp-collapse-container__header {
192
+ min-height: 56px;
193
+ padding: 0 20px;
194
+ display: flex;
195
+ align-items: center;
196
+ justify-content: space-between;
197
+ gap: 16px;
198
+ border-bottom: 1px solid #eef3fb;
199
+ }
200
+
201
+ .ecp-collapse-container.is-ghost .ecp-collapse-container__header {
202
+ padding-left: 0;
203
+ padding-right: 0;
204
+ }
205
+
206
+ .ecp-collapse-container__header-main {
207
+ min-width: 0;
208
+ flex: 1;
209
+ display: flex;
210
+ align-items: center;
211
+ }
212
+
213
+ .ecp-collapse-container__title-wrap {
214
+ min-width: 0;
215
+ display: inline-flex;
216
+ align-items: center;
217
+ gap: 8px;
218
+ }
219
+
220
+ .ecp-collapse-container__title {
221
+ color: #1f2d3d;
222
+ font-size: 16px;
223
+ font-weight: 600;
224
+ line-height: 1.4;
225
+ }
226
+
227
+ .ecp-collapse-container__help {
228
+ color: #91a3b7;
229
+ font-size: 14px;
230
+ }
231
+
232
+ .ecp-collapse-container__header-extra {
233
+ display: inline-flex;
234
+ align-items: center;
235
+ justify-content: flex-end;
236
+ gap: 12px;
237
+ flex-shrink: 0;
238
+ }
239
+
240
+ .ecp-collapse-container__toggle {
241
+ padding: 0;
242
+ color: #2f6fd3;
243
+ }
244
+
245
+ .ecp-collapse-container__toggle i {
246
+ margin-left: 6px;
247
+ transition: transform 0.2s ease;
248
+ }
249
+
250
+ .ecp-collapse-container__toggle i.is-expanded {
251
+ transform: rotate(180deg);
252
+ }
253
+
254
+ .ecp-collapse-container__body {
255
+ padding: 20px;
256
+ box-sizing: border-box;
257
+ }
258
+
259
+ .ecp-collapse-container.is-ghost .ecp-collapse-container__body {
260
+ padding-left: 0;
261
+ padding-right: 0;
262
+ }
263
+
264
+ .ecp-collapse-container.is-header-clickable .ecp-collapse-container__header-main {
265
+ cursor: pointer;
266
+ }
267
+
268
+ @media (max-width: 768px) {
269
+ .ecp-collapse-container__header {
270
+ padding: 14px 16px;
271
+ align-items: flex-start;
272
+ flex-direction: column;
273
+ }
274
+
275
+ .ecp-collapse-container__header-extra {
276
+ width: 100%;
277
+ justify-content: space-between;
278
+ }
279
+
280
+ .ecp-collapse-container__body {
281
+ padding: 16px;
282
+ }
283
+ }
284
+ </style>
@@ -0,0 +1,4 @@
1
+ import CollapseContainer from './CollapseContainer.vue'
2
+
3
+ export { CollapseContainer }
4
+ export default CollapseContainer
@@ -34,12 +34,32 @@ defineEmits<{ (e: 'input', value: unknown): void; (e: 'change', value: unknown):
34
34
 
35
35
  const loading = ref(false)
36
36
  const options = ref<Array<{ label: string; value: unknown }>>([])
37
- const cachedParams = ref<string>('')
37
+ const rawOptions = ref<unknown[]>([])
38
+
39
+ const sortStringify = (obj: unknown): string => {
40
+ if (obj === null || obj === undefined) return 'null'
41
+ return JSON.stringify(sortKeys(obj))
42
+ }
43
+
44
+ const sortKeys = (obj: unknown): unknown => {
45
+ if (Array.isArray(obj)) return obj.map(sortKeys)
46
+ if (obj !== null && typeof obj === 'object') {
47
+ return Object.keys(obj as Record<string, unknown>)
48
+ .sort()
49
+ .reduce<Record<string, unknown>>((acc, key) => {
50
+ acc[key] = sortKeys((obj as Record<string, unknown>)[key])
51
+ return acc
52
+ }, {})
53
+ }
54
+ return obj
55
+ }
56
+
57
+ const lastFetchedParamsKey = ref<string>(sortStringify(props.params ?? null))
38
58
 
39
59
  const onVisibleChange = (visible: boolean) => {
40
60
  if (props.lazy && visible) {
41
- const paramsKey = JSON.stringify(props.params ?? {})
42
- if (paramsKey !== cachedParams.value || options.value.length === 0) {
61
+ const paramsKey = sortStringify(props.params ?? null)
62
+ if (paramsKey !== lastFetchedParamsKey.value || options.value.length === 0) {
43
63
  fetchOptions()
44
64
  }
45
65
  }
@@ -55,6 +75,7 @@ const fetchOptions = async () => {
55
75
  : ((res as Record<string, unknown>)?.list as unknown[]) ??
56
76
  ((res as Record<string, unknown>)?.data as unknown[]) ??
57
77
  []
78
+ rawOptions.value = raw
58
79
  const labelKey = props.labelField ?? 'label'
59
80
  const valueKey = props.valueField ?? 'value'
60
81
  options.value = raw.map((item: unknown) => {
@@ -64,7 +85,7 @@ const fetchOptions = async () => {
64
85
  value: o[valueKey] ?? o.value,
65
86
  }
66
87
  })
67
- cachedParams.value = JSON.stringify(props.params ?? {})
88
+ lastFetchedParamsKey.value = sortStringify(props.params ?? null)
68
89
  } finally {
69
90
  loading.value = false
70
91
  }
@@ -72,6 +93,7 @@ const fetchOptions = async () => {
72
93
 
73
94
  defineExpose({
74
95
  options,
96
+ rawOptions,
75
97
  loading,
76
98
  fetchOptions,
77
99
  })
@@ -84,12 +106,16 @@ watch(() => props.api, () => {
84
106
  fetchOptions()
85
107
  } else {
86
108
  options.value = []
109
+ rawOptions.value = []
87
110
  }
88
111
  }, { deep: true }
89
112
  )
90
113
  watch(() => props.params, () => {
114
+ const newKey = sortStringify(props.params ?? null)
115
+ if (newKey === lastFetchedParamsKey.value) return
91
116
  if (props.lazy) {
92
117
  options.value = []
118
+ rawOptions.value = []
93
119
  } else {
94
120
  fetchOptions()
95
121
  }
@@ -422,15 +422,18 @@ const getComponentInstance = (field: string): ComponentPublicInstance | null =>
422
422
  return fieldInstanceMap.value.get(field) ?? null
423
423
  }
424
424
 
425
- /** 获取 api-select 字段的 options */
426
- const getFieldOptions = (field: string): Array<{ label: string; value: unknown }> => {
425
+ /** 获取 api-select 字段的 options,raw=true 时返回接口原始数据 */
426
+ function getFieldOptions(field: string): Array<{ label: string; value: unknown }>
427
+ function getFieldOptions(field: string, raw: false): Array<{ label: string; value: unknown }>
428
+ function getFieldOptions(field: string, raw: true): unknown[]
429
+ function getFieldOptions(field: string, raw = false): Array<{ label: string; value: unknown }> | unknown[] {
427
430
  const instance = fieldInstanceMap.value.get(field)
428
431
  if (!instance) return []
429
432
  const apiSelectInstance = instance as unknown as ApiSelectInstance
430
- if (apiSelectInstance?.options) {
431
- return apiSelectInstance.options
433
+ if (raw) {
434
+ return apiSelectInstance?.rawOptions ?? []
432
435
  }
433
- return []
436
+ return apiSelectInstance?.options ?? []
434
437
  }
435
438
 
436
439
  /** 获取 api-select 字段的加载状态 */
@@ -24,6 +24,9 @@ export interface UseFormReturn {
24
24
  appendSchemaByField: (schema: ProFormSchema, prefixField?: string, first?: boolean) => Promise<void>
25
25
  removeSchemaByField: (field: string | string[]) => Promise<void>
26
26
  setProps: (props: Partial<ProFormProps>) => Promise<void>
27
+ getComponentInstance: FormActionType['getComponentInstance']
28
+ getFieldOptions: FormActionType['getFieldOptions']
29
+ isFieldLoading: FormActionType['isFieldLoading']
27
30
  }
28
31
 
29
32
  export function useForm(props?: UseFormPropsReactive): [UseFormReturn['register'], Omit<UseFormReturn, 'register'>] {
@@ -92,6 +95,21 @@ export function useForm(props?: UseFormPropsReactive): [UseFormReturn['register'
92
95
  await formAction.value?.setProps(formProps)
93
96
  }
94
97
 
98
+ const getComponentInstance: FormActionType['getComponentInstance'] = (field) =>
99
+ formAction.value?.getComponentInstance(field) ?? null
100
+
101
+ function getFieldOptions(field: string): Array<{ label: string; value: unknown }>
102
+ function getFieldOptions(field: string, raw: false): Array<{ label: string; value: unknown }>
103
+ function getFieldOptions(field: string, raw: true): unknown[]
104
+ function getFieldOptions(field: string, raw = false): Array<{ label: string; value: unknown }> | unknown[] {
105
+ if (!formAction.value) return []
106
+ if (raw) return formAction.value.getFieldOptions(field, true)
107
+ return formAction.value.getFieldOptions(field)
108
+ }
109
+
110
+ const isFieldLoading: FormActionType['isFieldLoading'] = (field) =>
111
+ formAction.value?.isFieldLoading(field) ?? false
112
+
95
113
  const result: UseFormReturn = {
96
114
  register,
97
115
  formAction,
@@ -107,6 +125,9 @@ export function useForm(props?: UseFormPropsReactive): [UseFormReturn['register'
107
125
  appendSchemaByField,
108
126
  removeSchemaByField,
109
127
  setProps,
128
+ getComponentInstance,
129
+ getFieldOptions,
130
+ isFieldLoading,
110
131
  }
111
132
 
112
133
  return [register, result]
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ import type { VueConstructor } from 'vue'
2
2
  import ProTable, { TableAction } from './ProTable'
3
3
  import ProForm, { ProFormItem, FormActions, FormattedNumberInput } from './ProForm'
4
4
  import ProDescriptions from './ProDescriptions'
5
+ import CollapseContainer from './CollapseContainer'
5
6
  import { ProTableForm } from './ProTableForm'
6
7
  import { useForm } from './ProForm/useForm'
7
8
  import { useDescription } from './ProDescriptions/useDescription'
@@ -11,6 +12,7 @@ import { useComponentSetting } from './useComponentSetting'
11
12
  export { ProForm, ProFormItem, FormActions, FormattedNumberInput, useForm }
12
13
  export { ProTable, useProTable, TableAction }
13
14
  export { ProDescriptions, useDescription }
15
+ export { CollapseContainer }
14
16
  export { ProTableForm }
15
17
  export type {
16
18
  ProTableFormColumn,
@@ -34,6 +36,7 @@ const components = [
34
36
  { name: 'FormActions', component: FormActions },
35
37
  { name: 'FormattedNumberInput', component: FormattedNumberInput },
36
38
  { name: 'ProDescriptions', component: ProDescriptions },
39
+ { name: 'CollapseContainer', component: CollapseContainer },
37
40
  { name: 'ProTableForm', component: ProTableForm },
38
41
  ]
39
42
 
@@ -48,6 +51,7 @@ export default {
48
51
  ProTable,
49
52
  ProForm,
50
53
  ProDescriptions,
54
+ CollapseContainer,
51
55
  TableAction,
52
56
  FormattedNumberInput,
53
57
  ProTableForm,
@@ -32,9 +32,47 @@ export interface ScrollToFieldOptions {
32
32
  inline?: ScrollLogicalPosition
33
33
  }
34
34
 
35
+ export interface ProFormFieldOption {
36
+ label: string
37
+ value: unknown
38
+ }
39
+
40
+ export interface GetFieldOptions {
41
+ (field: string): ProFormFieldOption[]
42
+ (field: string, raw: false): ProFormFieldOption[]
43
+ (field: string, raw: true): unknown[]
44
+ }
45
+
46
+ export interface CollapseContainerProps {
47
+ title?: string
48
+ loading?: boolean
49
+ /** 与 Vben Admin 保持一致,沿用 canExpan 命名 */
50
+ canExpan?: boolean
51
+ /** canExpan 的别名,便于业务侧按语义使用 */
52
+ canExpand?: boolean
53
+ helpMessage?: string | string[]
54
+ triggerWindowResize?: boolean
55
+ expanded?: boolean
56
+ defaultExpand?: boolean
57
+ ghost?: boolean
58
+ expandButtonText?: string
59
+ collapseButtonText?: string
60
+ /** 根容器透传属性,支持 style/class/id/data-* 等 */
61
+ wrapperProps?: Record<string, unknown>
62
+ /** 头部额外 class */
63
+ headerClass?: string
64
+ /** 头部额外样式 */
65
+ headerStyle?: Record<string, string | number>
66
+ /** 内容区额外 class */
67
+ contentClass?: string
68
+ /** 内容区额外样式 */
69
+ contentStyle?: Record<string, string | number>
70
+ }
71
+
35
72
  /** ApiSelect 暴露的实例类型 */
36
73
  export interface ApiSelectInstance {
37
- options: Array<{ label: string; value: unknown }>
74
+ options: ProFormFieldOption[]
75
+ rawOptions: unknown[]
38
76
  loading: boolean
39
77
  fetchOptions: () => Promise<void>
40
78
  }
@@ -55,8 +93,8 @@ export interface FormActionType {
55
93
  setProps: (props: Partial<ProFormProps>) => Promise<void>
56
94
  /** 获取指定字段的组件实例 */
57
95
  getComponentInstance: (field: string) => ComponentPublicInstance | null
58
- /** 获取 api-select 字段的 options */
59
- getFieldOptions: (field: string) => Array<{ label: string; value: unknown }>
96
+ /** 获取 api-select 字段的 options,raw=true 时返回接口原始列表数据 */
97
+ getFieldOptions: GetFieldOptions
60
98
  /** 获取 api-select 字段的加载状态 */
61
99
  isFieldLoading: (field: string) => boolean
62
100
  }