diff --git a/packages/opendesign/src/components/cascader/OCascader.vue b/packages/opendesign/src/components/cascader/OCascader.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d911bb95368586e1c9a89235b58c0b2b01bbd168
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/OCascader.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+ -
+ {{ option.label }}
+
+
+
+
+
+
diff --git a/packages/opendesign/src/components/cascader/__demo__/CascaderBasic.vue b/packages/opendesign/src/components/cascader/__demo__/CascaderBasic.vue
new file mode 100644
index 0000000000000000000000000000000000000000..21a54495189a9c9233ff80bfe0e013a1e23ad892
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/__demo__/CascaderBasic.vue
@@ -0,0 +1,119 @@
+
+
+
+ 基础用法
+
+
diff --git a/packages/opendesign/src/components/cascader/__demo__/IndexCascader.vue b/packages/opendesign/src/components/cascader/__demo__/IndexCascader.vue
new file mode 100644
index 0000000000000000000000000000000000000000..51dd708aa207c272b9f2347ba2123af35344141e
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/__demo__/IndexCascader.vue
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/packages/opendesign/src/components/cascader/cascader.ts b/packages/opendesign/src/components/cascader/cascader.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ae9a58c7934551ab3545f96ac068ea1d6b6bcaad
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/cascader.ts
@@ -0,0 +1,149 @@
+import { isArray, isUndefined } from '../_shared/is';
+import { CascaderValueT } from './types';
+import { CascaderOptionT } from './types';
+
+interface CascaderNodeT {
+ value: string | number;
+ label?: string;
+ depth: number;
+ parent: CascaderNodeT | null;
+ children: CascaderNodeT[];
+ isLeaf: boolean;
+}
+
+interface ColumnInfoT {
+ value: string | number;
+ label?: string;
+ depth: number;
+ isLeaf: boolean;
+ isActive: boolean;
+}
+
+const DFS = (options: Array, parentNode: CascaderNodeT, depth: number) => {
+ for (let i = 0, len = options.length; i < len; i++) {
+ const item = options[i];
+ let node: CascaderNodeT = {
+ value: item.value,
+ label: item.label,
+ parent: parentNode,
+ depth: depth + 1,
+ children: [],
+ isLeaf: true,
+ };
+ parentNode.children.push(node);
+
+ if (item.children && item.children.length) {
+ node.isLeaf = false;
+ DFS(item.children, node, depth + 1);
+ }
+ }
+};
+
+export default class CascaderTree {
+ root: CascaderNodeT;
+ constructor() {
+ this.root = {
+ value: NaN,
+ label: '',
+ depth: 0,
+ parent: null,
+ children: [],
+ isLeaf: true,
+ };
+ }
+
+ updateTree(options: Array) {
+ this.root = {
+ value: NaN,
+ label: '',
+ depth: 0,
+ parent: null,
+ children: [],
+ isLeaf: true,
+ };
+ DFS(options, this.root, 0);
+ }
+
+ getNode(node: CascaderNodeT, val: string | number): CascaderNodeT | undefined {
+ if (node.value === val) {
+ return node;
+ }
+
+ const children: Array = node.children;
+
+ for (let i = 0, len = children.length; i < len; i++) {
+ const rlt = this.getNode(children[i], val);
+ if (rlt) {
+ return rlt;
+ }
+ }
+ }
+
+ getChild(node: CascaderNodeT, val: string | number): CascaderNodeT | undefined {
+ const children: Array = node.children;
+ return children.find((item) => item.value === val);
+ }
+
+ getPanelInfo(val: CascaderValueT | undefined) {
+ let rlt: Array> = [];
+
+ if (isUndefined(val)) {
+ return rlt;
+ }
+
+ if (!isArray(val)) {
+ let node = this.getNode(this.root, val);
+ if (isUndefined(node)) {
+ const columnInfo = this.getNextColumnInfo(this.root);
+ if (!isUndefined(columnInfo)) {
+ rlt = [columnInfo];
+ }
+ } else {
+ while (node.parent) {
+ const columnInfo = this.getNextColumnInfo(node.parent, node.value);
+ if (!isUndefined(columnInfo)) {
+ rlt.unshift(columnInfo);
+ }
+ node = node.parent;
+ }
+ }
+ } else {
+ let parent = this.root;
+
+ for (let i = 0, len = val.length; i < len; i++) {
+ const child = this.getChild(parent, val[i]);
+ if (isUndefined(child)) {
+ const columnInfo = this.getNextColumnInfo(this.root);
+ if (!isUndefined(columnInfo)) {
+ rlt = [columnInfo];
+ }
+ break;
+ } else {
+ const columnInfo = this.getNextColumnInfo(parent, val[i]);
+ rlt.push(columnInfo);
+ parent = child;
+ }
+ }
+ }
+
+ return rlt;
+ }
+
+ getNextColumnInfo(node: CascaderNodeT, activeVal?: string | number): Array {
+ return node.children.map((item) => {
+ const rlt = {
+ value: item.value,
+ label: item.label,
+ depth: item.depth,
+ isActive: false,
+ isLeaf: item.children && item.children.length ? false : true,
+ };
+
+ if (!isUndefined(activeVal)) {
+ rlt.isActive = item.value === activeVal;
+ }
+
+ return rlt;
+ });
+ }
+}
diff --git a/packages/opendesign/src/components/cascader/index.ts b/packages/opendesign/src/components/cascader/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2b67087af27e08fc1ee0784b94c0725fea6abf28
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/index.ts
@@ -0,0 +1,13 @@
+import type { App } from 'vue';
+
+import _OCascader from './OCascader.vue';
+
+const OCascader = Object.assign(_OCascader, {
+ install(app: App) {
+ app.component(_OCascader.name, _OCascader);
+ },
+});
+
+export * from './types';
+
+export { OCascader };
diff --git a/packages/opendesign/src/components/cascader/style/index.scss b/packages/opendesign/src/components/cascader/style/index.scss
new file mode 100644
index 0000000000000000000000000000000000000000..8f59c84af2885157e68f789163299bc9f9a59347
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/style/index.scss
@@ -0,0 +1,62 @@
+@use './var.scss';
+
+.o-cascader-panel {
+ position: relative;
+ display: inline-flex;
+ border-radius: var(--o-radius-control-l);
+ height: 200px;
+}
+
+.o-cascader-options {
+ list-style: none;
+ margin: 0;
+ padding: 4px 4px 4px 4px;
+ min-width: 144px;
+ max-width: 192px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+
+ & + .o-cascader-options {
+ border-left: 1px solid var(--cascader-options-bd-clor);
+ }
+
+ &:first-child {
+ padding-left: 0;
+ }
+
+ &:last-child {
+ padding-right: 0;
+ }
+}
+
+.o-cascader-option {
+ display: flex;
+ align-items: center;
+ padding: var(--cascader-option-padding);
+
+ color: var(--cascader-option-color);
+ border-radius: var(--cascader-option-radius);
+ background-color: var(--cascader-option-bg-color);
+ transition: background-color var(--o-duration-s) var(--o-easing-standard);
+ cursor: pointer;
+
+ &:not(.o-cascader-option-active):hover {
+ background-color: var(--cascader-option-bg-color-hover);
+ }
+}
+
+.o-cascader-option-label {
+ font-size: var(--cascader-option-text-size);
+ line-height: var(--cascader-option-text-height);
+}
+
+.o-cascader-option-arrow {
+ font-size: var(--cascader-option-icon-size);
+ margin-left: auto;
+}
+
+.o-cascader-option-active {
+ background-color: var(--cascader-option-bg-color-active);
+ font-weight: 500;
+}
diff --git a/packages/opendesign/src/components/cascader/style/index.ts b/packages/opendesign/src/components/cascader/style/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33107e5d2c59f2090e3e52d9cf07964e7175ca10
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/style/index.ts
@@ -0,0 +1,3 @@
+import '../../style';
+import '../../select/style';
+import './index.scss';
diff --git a/packages/opendesign/src/components/cascader/style/var.scss b/packages/opendesign/src/components/cascader/style/var.scss
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d84c776c555bd2231db41ace2c458eaa801fc634 100644
--- a/packages/opendesign/src/components/cascader/style/var.scss
+++ b/packages/opendesign/src/components/cascader/style/var.scss
@@ -0,0 +1,18 @@
+.o-cascader-options {
+ --cascader-options-bd-clor: var(--o-color-control1-light);
+}
+
+.o-cascader-option {
+ --cascader-option-color: var(--o-color-info1);
+ --cascader-option-text-size: var(--o-font_size-text1);
+ --cascader-option-text-height: var(--o-line_height-text1);
+
+ --cascader-option-padding: 8px 12px;
+ --cascader-option-radius: var(--o-radius-control-m);
+
+ --cascader-option-bg-color: transparent;
+ --cascader-option-bg-color-hover: var(--o-color-control2-light);
+ --cascader-option-bg-color-active: var(--o-color-control3-light);
+
+ --cascader-option-icon-size: var(--o-icon_size-xs);
+}
diff --git a/packages/opendesign/src/components/cascader/types.ts b/packages/opendesign/src/components/cascader/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..637fc7b4e66393f2b8a2537e1b659ee3dc404397
--- /dev/null
+++ b/packages/opendesign/src/components/cascader/types.ts
@@ -0,0 +1,96 @@
+import { ExtractPropTypes, PropType } from 'vue';
+import { PopupPositionT, PopupTriggerT } from '../popup';
+import type { RoundT, VariantT } from '../_shared/global';
+
+export type CascaderNodeValueT = string | number;
+export type CascaderNodePathT = Array;
+export type CascaderValueT = CascaderNodeValueT | CascaderNodePathT;
+
+export type CascaderOptionT = {
+ value: CascaderNodeValueT;
+ label?: string;
+ children?: CascaderOptionT[];
+};
+
+export const cascaderProps = {
+ /**
+ * 级联选择器的值
+ * v-model
+ */
+ modelValue: {
+ type: [String, Number, Array] as PropType,
+ },
+ /**
+ * 级联选择器选项值
+ *
+ * */
+ options: {
+ type: Array as PropType>,
+ },
+ /**
+ * 是否使用路径模式
+ *
+ * */
+ pathMode: {
+ type: Boolean,
+ default: false,
+ },
+ /**
+ * 圆角值
+ */
+ round: {
+ type: String as PropType,
+ },
+ /**
+ * 级联选择器类型
+ */
+ variant: {
+ type: String as PropType,
+ default: 'outline',
+ },
+ /**
+ * 提示文本
+ */
+ placeholder: {
+ type: String,
+ default: 'please select...',
+ },
+ /**
+ * 下拉选项触发方式
+ */
+ trigger: {
+ type: String as PropType,
+ default: 'click',
+ },
+ /**
+ * 下拉选项位置
+ */
+ optionPosition: {
+ type: String as PropType,
+ default: 'bl',
+ },
+ /**
+ * 下拉选项宽度自适应规则
+ * 'auto':自动 | 'min-width':最小宽度与选择框一致 | 'width': 宽度与选择框一致
+ */
+ optionWidthMode: {
+ type: String as PropType<'auto' | 'min-width' | 'width'>,
+ default: 'min-width',
+ },
+ /**
+ * 是否在结束选择时,卸载下拉选项
+ * v-model
+ */
+ unmountOnHide: {
+ type: Boolean,
+ default: true,
+ },
+ /**
+ * 过渡名称
+ */
+ transition: {
+ type: String,
+ },
+};
+
+export type CascaderPropsT = ExtractPropTypes;
diff --git a/packages/portal/src/router.ts b/packages/portal/src/router.ts
index 4848a27a4f82a518be22ee501fcb0d33a10a395b..eb5ee782df42285b6e56ab22ca8d65092d8f4373 100644
--- a/packages/portal/src/router.ts
+++ b/packages/portal/src/router.ts
@@ -68,12 +68,12 @@ export const routes = [
label: '下拉框',
component: () => import('@components/select/__demo__/IndexSelect.vue'),
},
- // {
- // path: '/cascader',
- // name: 'Cascader',
- // label: '级联选择器',
- // component: () => import('@components/cascader/__demo__/IndexCascader.vue'),
- // },
+ {
+ path: '/cascader',
+ name: 'Cascader',
+ label: '级联选择器',
+ component: () => import('@components/cascader/__demo__/IndexCascader.vue'),
+ },
{
path: '/radio',
name: 'Radio',