@c2-markdown-ui/svelte 0.0.0-stage → 0.4.3
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/README.md +91 -2
- package/package.json +36 -4
- package/src/lib/MarkdownUI.svelte +60 -0
- package/src/lib/Navbar.svelte +144 -0
- package/src/lib/WidgetEvents.svelte +139 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/widgets/Button.svelte +30 -0
- package/src/lib/widgets/ButtonGroup.svelte +37 -0
- package/src/lib/widgets/Cascader.svelte +486 -0
- package/src/lib/widgets/Chart.svelte +278 -0
- package/src/lib/widgets/DatePicker.svelte +609 -0
- package/src/lib/widgets/Form.svelte +571 -0
- package/src/lib/widgets/Incomplete.svelte +72 -0
- package/src/lib/widgets/MultipleChoiceQuestion.svelte +65 -0
- package/src/lib/widgets/NumberInput.svelte +149 -0
- package/src/lib/widgets/Quiz.svelte +301 -0
- package/src/lib/widgets/Radio.svelte +180 -0
- package/src/lib/widgets/Row.svelte +140 -0
- package/src/lib/widgets/Select.svelte +276 -0
- package/src/lib/widgets/SelectMulti.svelte +130 -0
- package/src/lib/widgets/ShortAnswerQuestion.svelte +57 -0
- package/src/lib/widgets/Slider.svelte +46 -0
- package/src/lib/widgets/Switch.svelte +60 -0
- package/src/lib/widgets/Text.svelte +58 -0
- package/src/lib/widgets/TextInput.svelte +58 -0
- package/src/lib/widgets/TreeSelect.svelte +941 -0
- package/src/lib/widgets/Widget.svelte +90 -0
- package/src/lib/widgets/chartRemoteData.ts +84 -0
- package/src/lib/widgets/index.ts +23 -0
- package/src/lib/widgets/remoteChoices.ts +90 -0
- package/src/lib/widgets.css +1754 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/** 级联选择器(参考 antd Cascader)
|
|
3
|
+
* 静态数据源两种格式(兼容共存,data 优先):
|
|
4
|
+
* 1. choices:扁平路径数组,每项用 / 分隔层级,每段支持 key:label
|
|
5
|
+
* 2. data:嵌套对象树(CascadeNode[]),如 [{value:'zj',label:'浙江',children:[...]}]
|
|
6
|
+
* url:远端数据源,fetch 拉取嵌套树,data/choices 作为初始/后备 */
|
|
7
|
+
interface CascadeNode {
|
|
8
|
+
value?: string | number;
|
|
9
|
+
/** value 的别名(key:label 风格数据源) */
|
|
10
|
+
key?: string | number;
|
|
11
|
+
label?: string | number;
|
|
12
|
+
children?: CascadeNode[];
|
|
13
|
+
/** lazy 模式:子级是否已请求过(请求返回空数组时为真叶子,标记避免重复请求) */
|
|
14
|
+
loaded?: boolean;
|
|
15
|
+
[extra: string]: any;
|
|
16
|
+
}
|
|
17
|
+
interface Props {
|
|
18
|
+
choices: string[];
|
|
19
|
+
/** 嵌套对象树数据源(与扁平路径 choices 兼容共存,优先于 choices) */
|
|
20
|
+
data?: CascadeNode[];
|
|
21
|
+
/** 远端数据源 URL:设置后运行时 fetch 拉取嵌套树,data/choices 作为初始/后备 */
|
|
22
|
+
url?: string;
|
|
23
|
+
/** 懒加载模式(url 存在时生效):首次只拉根级,点击父级后以 url?parent=<value> 逐级请求子级 */
|
|
24
|
+
lazy?: boolean;
|
|
25
|
+
label?: string;
|
|
26
|
+
default?: string;
|
|
27
|
+
placeholder?: string;
|
|
28
|
+
hint?: string;
|
|
29
|
+
required?: boolean;
|
|
30
|
+
/** 校验失败提示文案 */
|
|
31
|
+
message?: string;
|
|
32
|
+
/** 外部(form 提交时)传入的错误信息 */
|
|
33
|
+
error?: string;
|
|
34
|
+
/** 是否禁用 */
|
|
35
|
+
disabled?: boolean;
|
|
36
|
+
onchange: (value: string) => void;
|
|
37
|
+
}
|
|
38
|
+
let {
|
|
39
|
+
choices,
|
|
40
|
+
data,
|
|
41
|
+
url = '',
|
|
42
|
+
lazy = false,
|
|
43
|
+
label = '',
|
|
44
|
+
default: initial,
|
|
45
|
+
placeholder = '',
|
|
46
|
+
hint = '',
|
|
47
|
+
required = false,
|
|
48
|
+
message = '',
|
|
49
|
+
error = '',
|
|
50
|
+
disabled = false,
|
|
51
|
+
onchange
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
// ---------- 路径解析与树构建 ----------
|
|
55
|
+
interface CasNode {
|
|
56
|
+
/** 段 key(无 key:label 时即段文本) */
|
|
57
|
+
value: string;
|
|
58
|
+
/** 段显示文案 */
|
|
59
|
+
label: string;
|
|
60
|
+
children: CasNode[];
|
|
61
|
+
/** 是否叶子(完整路径;lazy 模式下无 children 视为待加载,非叶子) */
|
|
62
|
+
isLeaf: boolean;
|
|
63
|
+
/** lazy 模式:子级是否已请求过(请求返回空数组时为真叶子,标记避免重复请求) */
|
|
64
|
+
loaded?: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 解析一段文本:key:label → {value:key, label:label};无冒号则 value=label=段文本 */
|
|
68
|
+
function parseSeg(seg: string): { value: string; label: string } {
|
|
69
|
+
const idx = seg.indexOf(':');
|
|
70
|
+
if (idx > 0) {
|
|
71
|
+
const v = seg.slice(0, idx).trim();
|
|
72
|
+
const l = seg.slice(idx + 1).trim();
|
|
73
|
+
if (v && l) return { value: v, label: l };
|
|
74
|
+
}
|
|
75
|
+
return { value: seg.trim(), label: seg.trim() };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 嵌套对象树 → 内部 CasNode 树(label 缺省回退 value,children 缺省/空视为叶子)
|
|
79
|
+
* lazy 模式下无 children 的节点视为"待加载"(isLeaf=false),子级点击父级时请求;
|
|
80
|
+
* 节点上已有的 loaded 标记(lazy 子级请求返回空数组的真叶子)需保留,避免重复请求 */
|
|
81
|
+
function nodesToTree(nodes: CascadeNode[] | undefined, lazy = false): CasNode[] {
|
|
82
|
+
if (!Array.isArray(nodes)) return [];
|
|
83
|
+
return nodes.map(n => {
|
|
84
|
+
const v = n.value ?? n.key;
|
|
85
|
+
const children = n.children;
|
|
86
|
+
const hasChildren = Array.isArray(children) && children.length > 0;
|
|
87
|
+
const wasLoaded = n.loaded === true;
|
|
88
|
+
return {
|
|
89
|
+
value: String(v ?? ''),
|
|
90
|
+
label: String(n.label ?? v ?? ''),
|
|
91
|
+
children: hasChildren ? nodesToTree(children, lazy) : [],
|
|
92
|
+
isLeaf: lazy ? !hasChildren && wasLoaded : !hasChildren,
|
|
93
|
+
loaded: hasChildren || wasLoaded,
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 兼容常见远端嵌套树响应并归一化为 CascadeNode[]:
|
|
100
|
+
* - [{value,label,children}](直接树数组)
|
|
101
|
+
* - {data|options|choices|items|tree|treeData:[...]} 包裹
|
|
102
|
+
*/
|
|
103
|
+
function normalizeRemoteTree(json: any): CascadeNode[] {
|
|
104
|
+
let list = json;
|
|
105
|
+
if (!Array.isArray(json)) {
|
|
106
|
+
list = Array.isArray(json?.data) ? json.data
|
|
107
|
+
: Array.isArray(json?.options) ? json.options
|
|
108
|
+
: Array.isArray(json?.choices) ? json.choices
|
|
109
|
+
: Array.isArray(json?.items) ? json.items
|
|
110
|
+
: Array.isArray(json?.tree) ? json.tree
|
|
111
|
+
: Array.isArray(json?.treeData) ? json.treeData
|
|
112
|
+
: [];
|
|
113
|
+
}
|
|
114
|
+
const map = (arr: any[]): CascadeNode[] =>
|
|
115
|
+
(arr || [])
|
|
116
|
+
.map((it: any): CascadeNode | null => {
|
|
117
|
+
if (!it || typeof it !== 'object') return null;
|
|
118
|
+
const value = it.value ?? it.key;
|
|
119
|
+
if (value === undefined || value === null || value === '') return null;
|
|
120
|
+
const node: CascadeNode = { value: String(value) };
|
|
121
|
+
const label = it.label ?? it.name ?? it.text;
|
|
122
|
+
if (label !== undefined && label !== null) node.label = String(label);
|
|
123
|
+
if (Array.isArray(it.children) && it.children.length > 0) {
|
|
124
|
+
node.children = map(it.children);
|
|
125
|
+
}
|
|
126
|
+
return node;
|
|
127
|
+
})
|
|
128
|
+
.filter((n: CascadeNode | null): n is CascadeNode => n !== null);
|
|
129
|
+
return map(list);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------- 远端与静态数据源 ----------
|
|
133
|
+
/** 远端拉取的嵌套树(成功后覆盖本地 data/choices;lazy 模式下仅根级,子级逐步填充)
|
|
134
|
+
* 加载失败静默处理:回落本地 data/choices,均无数据时面板显示灰色"暂无数据" */
|
|
135
|
+
let remoteData = $state<CascadeNode[] | null>(null);
|
|
136
|
+
let loading = $state(false);
|
|
137
|
+
/** lazy 模式:正在加载子级的父级路径串(/ 连接),空 = 无进行中的子级请求 */
|
|
138
|
+
let loadingPath = $state('');
|
|
139
|
+
|
|
140
|
+
// 静态 tree:data 优先于 choices(兼容共存),远端拉取成功时以远端为准
|
|
141
|
+
const tree = $derived.by(() => {
|
|
142
|
+
if (remoteData) return nodesToTree(remoteData, lazy);
|
|
143
|
+
if (Array.isArray(data) && data.length > 0) return nodesToTree(data, lazy);
|
|
144
|
+
// 回退:扁平路径数组 → 多级树(公共前缀合并)
|
|
145
|
+
const root: CasNode[] = [];
|
|
146
|
+
for (const path of choices) {
|
|
147
|
+
if (!path) continue;
|
|
148
|
+
const segs = path.split('/').map(s => s.trim()).filter(Boolean);
|
|
149
|
+
if (segs.length === 0) continue;
|
|
150
|
+
let level = root;
|
|
151
|
+
for (let i = 0; i < segs.length; i++) {
|
|
152
|
+
const { value, label } = parseSeg(segs[i]);
|
|
153
|
+
let node = level.find(n => n.value === value);
|
|
154
|
+
if (!node) {
|
|
155
|
+
node = { value, label, children: [], isLeaf: i === segs.length - 1 };
|
|
156
|
+
level.push(node);
|
|
157
|
+
}
|
|
158
|
+
level = node.children;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return root;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/** 找到 value 对应的节点(顶层查找) */
|
|
165
|
+
function findTop(value: string): CasNode | undefined {
|
|
166
|
+
return tree.find(n => n.value === value);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** 默认路径(value 串,/ 分隔)→ 逐级 value 数组;支持 label 串回退(无 key:label 时 value=label 等价) */
|
|
170
|
+
function pathToValues(path: string | undefined): string[] {
|
|
171
|
+
if (!path) return [];
|
|
172
|
+
const segs = path.split('/').map(s => s.trim()).filter(Boolean);
|
|
173
|
+
const values: string[] = [];
|
|
174
|
+
let level = tree;
|
|
175
|
+
for (const seg of segs) {
|
|
176
|
+
const node = level.find(n => n.value === seg) ?? level.find(n => n.label === seg);
|
|
177
|
+
if (!node) return values;
|
|
178
|
+
values.push(node.value);
|
|
179
|
+
level = node.children;
|
|
180
|
+
}
|
|
181
|
+
return values;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ---------- 状态 ----------
|
|
185
|
+
/** 逐级选中的 value 数组(空 = 未选择) */
|
|
186
|
+
let selected = $state<string[]>([]);
|
|
187
|
+
/** 面板展开时逐级激活(高亮)的 value 数组 */
|
|
188
|
+
let active = $state<string[]>([]);
|
|
189
|
+
let open = $state(false);
|
|
190
|
+
|
|
191
|
+
// 初始化 default
|
|
192
|
+
selected = pathToValues(initial);
|
|
193
|
+
|
|
194
|
+
let rootEl: HTMLElement | undefined = $state();
|
|
195
|
+
|
|
196
|
+
$effect(() => {
|
|
197
|
+
function onDocClick(e: MouseEvent) {
|
|
198
|
+
if (rootEl && !rootEl.contains(e.target as Node)) open = false;
|
|
199
|
+
}
|
|
200
|
+
document.addEventListener('mousedown', onDocClick);
|
|
201
|
+
return () => document.removeEventListener('mousedown', onDocClick);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
$effect(() => {
|
|
205
|
+
if (!url) { remoteData = null; return; }
|
|
206
|
+
loading = true;
|
|
207
|
+
let cancelled = false;
|
|
208
|
+
(async () => {
|
|
209
|
+
try {
|
|
210
|
+
const res = await fetch(url);
|
|
211
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
212
|
+
const json: any = await res.json();
|
|
213
|
+
if (cancelled) return;
|
|
214
|
+
const list = normalizeRemoteTree(json);
|
|
215
|
+
if (list.length === 0) throw new Error('响应中没有可用选项');
|
|
216
|
+
remoteData = list;
|
|
217
|
+
// 用户尚未选择时回填默认路径(远端加载完成后触发;lazy 模式默认路径仅能回填根级)
|
|
218
|
+
if (selected.length === 0) selected = pathToValues(initial);
|
|
219
|
+
} catch {
|
|
220
|
+
// 加载失败:静默回落本地 data/choices
|
|
221
|
+
} finally {
|
|
222
|
+
if (!cancelled) loading = false;
|
|
223
|
+
}
|
|
224
|
+
})();
|
|
225
|
+
return () => { cancelled = true; };
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/** lazy 模式:按路径在原始树中找到节点并填充其子级(返回新树,目标节点打 loaded 标记) */
|
|
229
|
+
function fillChildren(treeNodes: CascadeNode[], path: string[], children: CascadeNode[]): CascadeNode[] {
|
|
230
|
+
return treeNodes.map(node => {
|
|
231
|
+
const v = String(node.value ?? node.key ?? '');
|
|
232
|
+
if (v !== path[0]) return node;
|
|
233
|
+
if (path.length === 1) {
|
|
234
|
+
// 目标节点:填充子级;空数组 = 真叶子(loaded 防止重复请求)
|
|
235
|
+
return { ...node, children, loaded: true };
|
|
236
|
+
}
|
|
237
|
+
return { ...node, children: fillChildren(node.children || [], path.slice(1), children) };
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** lazy 模式:请求某父级的子级(url?parent=<value>),成功后填充进树 */
|
|
242
|
+
async function loadChildren(node: CasNode, path: string[]) {
|
|
243
|
+
if (!url || !node || node.loaded) return;
|
|
244
|
+
loadingPath = path.join('/');
|
|
245
|
+
try {
|
|
246
|
+
const sep = url.includes('?') ? '&' : '?';
|
|
247
|
+
const res = await fetch(`${url}${sep}parent=${encodeURIComponent(node.value)}`);
|
|
248
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
249
|
+
const json: any = await res.json();
|
|
250
|
+
const list = normalizeRemoteTree(json);
|
|
251
|
+
// 递归更新 remoteData(不可变更新;children 为空时该节点确认为真叶子)
|
|
252
|
+
if (remoteData) remoteData = fillChildren(remoteData, path, list);
|
|
253
|
+
} catch {
|
|
254
|
+
// 子级加载失败:静默处理,子层级面板显示灰色"暂无数据"
|
|
255
|
+
} finally {
|
|
256
|
+
loadingPath = '';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 当前激活层级对应的子节点列表(level 0 用树根) */
|
|
261
|
+
function levelNodes(level: number): CasNode[] {
|
|
262
|
+
let nodes = tree;
|
|
263
|
+
for (let i = 0; i < level; i++) {
|
|
264
|
+
const v = active[i];
|
|
265
|
+
const node = nodes.find(n => n.value === v);
|
|
266
|
+
if (!node) return [];
|
|
267
|
+
nodes = node.children;
|
|
268
|
+
}
|
|
269
|
+
return nodes;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 展开层级总数(含当前激活最深一层;lazy 模式下未加载的节点也占一层,展示加载态) */
|
|
273
|
+
const levelCount = $derived.by(() => {
|
|
274
|
+
if (!open) return 0;
|
|
275
|
+
let count = 1;
|
|
276
|
+
let nodes = tree;
|
|
277
|
+
for (const v of active) {
|
|
278
|
+
const node = nodes.find(n => n.value === v);
|
|
279
|
+
if (!node || node.isLeaf) break;
|
|
280
|
+
nodes = node.children;
|
|
281
|
+
count++;
|
|
282
|
+
}
|
|
283
|
+
return count;
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
/** 选中节点 value 数组 → 展示文案串(label 用 / 连接) */
|
|
287
|
+
function displayText(values: string[]): string {
|
|
288
|
+
let nodes = tree;
|
|
289
|
+
const labels: string[] = [];
|
|
290
|
+
for (const v of values) {
|
|
291
|
+
const node = nodes.find(n => n.value === v);
|
|
292
|
+
if (!node) break;
|
|
293
|
+
labels.push(node.label);
|
|
294
|
+
nodes = node.children;
|
|
295
|
+
}
|
|
296
|
+
return labels.join(' / ');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const shownError = $derived(error || '');
|
|
300
|
+
|
|
301
|
+
function toggleOpen() {
|
|
302
|
+
if (disabled) return;
|
|
303
|
+
open = !open;
|
|
304
|
+
if (open) {
|
|
305
|
+
// 打开时激活层级同步到已选路径
|
|
306
|
+
active = selected.length > 0 ? selected.slice(0, -1) : [];
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** 点击某层选项:叶子 → 完成选择并关闭;待加载(lazy)→ 请求子级后展开 */
|
|
311
|
+
function pick(level: number, node: CasNode) {
|
|
312
|
+
if (disabled) return;
|
|
313
|
+
const next = active.slice(0, level);
|
|
314
|
+
next.push(node.value);
|
|
315
|
+
if (node.isLeaf) {
|
|
316
|
+
// 叶子节点:完成选择
|
|
317
|
+
selected = next;
|
|
318
|
+
active = [];
|
|
319
|
+
open = false;
|
|
320
|
+
onchange(next.join('/'));
|
|
321
|
+
} else {
|
|
322
|
+
// 展开下一层;lazy 模式下未加载时先请求子级
|
|
323
|
+
active = next;
|
|
324
|
+
if (lazy && url && !node.loaded) {
|
|
325
|
+
void loadChildren(node, next);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function clearValue(e: MouseEvent) {
|
|
331
|
+
e.stopPropagation();
|
|
332
|
+
selected = [];
|
|
333
|
+
active = [];
|
|
334
|
+
open = false;
|
|
335
|
+
onchange('');
|
|
336
|
+
}
|
|
337
|
+
</script>
|
|
338
|
+
|
|
339
|
+
<div class="selector widget-cascader" bind:this={rootEl}>
|
|
340
|
+
{#if label || hint || required}
|
|
341
|
+
<div class="widget-label-row">
|
|
342
|
+
{#if required}<span class="widget-required-mark" aria-hidden="true">*</span>{/if}
|
|
343
|
+
{#if label}<label>{label}</label>{/if}
|
|
344
|
+
{#if hint}<span class="widget-hint" data-hint={hint}>ⓘ</span>{/if}
|
|
345
|
+
</div>
|
|
346
|
+
{/if}
|
|
347
|
+
<div class="widget-field-content">
|
|
348
|
+
<button type="button" class="cas-input" class:widget-input-error={!!shownError} onclick={toggleOpen} aria-expanded={open} {disabled}>
|
|
349
|
+
<span class="cas-input-text" class:cas-placeholder={selected.length === 0}>
|
|
350
|
+
{loading ? '加载中…' : selected.length > 0 ? displayText(selected) : placeholder || '请选择'}
|
|
351
|
+
</span>
|
|
352
|
+
{#if !disabled && selected.length > 0}
|
|
353
|
+
<span class="cas-clear" role="button" tabindex="-1" aria-label="清空"
|
|
354
|
+
onclick={(e) => clearValue(e)}>×</span>
|
|
355
|
+
{:else if !disabled}
|
|
356
|
+
<span class="cas-arrow" class:cas-arrow-open={open} aria-hidden="true">▾</span>
|
|
357
|
+
{/if}
|
|
358
|
+
</button>
|
|
359
|
+
{#if shownError}<div class="widget-error-text" role="alert">{shownError}</div>{/if}
|
|
360
|
+
|
|
361
|
+
{#if open}
|
|
362
|
+
<div class="cas-panel">
|
|
363
|
+
{#if levelCount === 0}
|
|
364
|
+
<div class="cas-empty">{loading ? '加载中…' : '暂无数据'}</div>
|
|
365
|
+
{:else}
|
|
366
|
+
{#each Array(levelCount) as _, level}
|
|
367
|
+
<div class="cas-menu" role="menu">
|
|
368
|
+
{#each levelNodes(level) as node (node.value)}
|
|
369
|
+
<button
|
|
370
|
+
type="button"
|
|
371
|
+
class="cas-option"
|
|
372
|
+
class:cas-option-active={active[level] === node.value}
|
|
373
|
+
onclick={() => pick(level, node)}
|
|
374
|
+
>
|
|
375
|
+
<span class="cas-option-label">{node.label}</span>
|
|
376
|
+
{#if !node.isLeaf}
|
|
377
|
+
<span class="cas-option-arrow" aria-hidden="true">›</span>
|
|
378
|
+
{/if}
|
|
379
|
+
</button>
|
|
380
|
+
{/each}
|
|
381
|
+
{#if levelNodes(level).length === 0}
|
|
382
|
+
<div class="cas-empty">{loadingPath && active.join('/').startsWith(loadingPath) ? '加载中…' : '暂无数据'}</div>
|
|
383
|
+
{/if}
|
|
384
|
+
</div>
|
|
385
|
+
{/each}
|
|
386
|
+
{/if}
|
|
387
|
+
</div>
|
|
388
|
+
{/if}
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
|
|
392
|
+
<style>
|
|
393
|
+
.widget-cascader {
|
|
394
|
+
position: relative;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.cas-input {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
gap: 0.375rem;
|
|
401
|
+
width: 100%;
|
|
402
|
+
padding: 0.625rem 0.75rem;
|
|
403
|
+
border: 1px solid #d9d9d9;
|
|
404
|
+
border-radius: 6px;
|
|
405
|
+
font-size: 0.875rem;
|
|
406
|
+
background-color: #ffffff;
|
|
407
|
+
cursor: pointer;
|
|
408
|
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
|
409
|
+
text-align: left;
|
|
410
|
+
}
|
|
411
|
+
.cas-input:hover { border-color: #4096ff; }
|
|
412
|
+
.cas-input:focus {
|
|
413
|
+
outline: none;
|
|
414
|
+
border-color: #1677ff;
|
|
415
|
+
box-shadow: 0 0 0 2px rgba(5, 145, 255, 0.1);
|
|
416
|
+
}
|
|
417
|
+
/* 禁用态:置灰 + 禁止交互 */
|
|
418
|
+
.cas-input:disabled {
|
|
419
|
+
background-color: #f5f5f5;
|
|
420
|
+
color: rgba(0, 0, 0, 0.25);
|
|
421
|
+
border-color: #d9d9d9;
|
|
422
|
+
cursor: not-allowed;
|
|
423
|
+
}
|
|
424
|
+
.cas-input:disabled:hover { border-color: #d9d9d9; }
|
|
425
|
+
|
|
426
|
+
.cas-input-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1 1 auto; min-width: 0; }
|
|
427
|
+
.cas-placeholder { color: rgba(0, 0, 0, 0.45); }
|
|
428
|
+
.cas-arrow { color: rgba(0, 0, 0, 0.45); font-size: 0.75rem; transition: transform 0.2s ease; }
|
|
429
|
+
.cas-arrow-open { transform: rotate(180deg); }
|
|
430
|
+
.cas-clear {
|
|
431
|
+
padding: 0 0.25rem;
|
|
432
|
+
color: rgba(0, 0, 0, 0.45);
|
|
433
|
+
font-size: 1rem;
|
|
434
|
+
line-height: 1;
|
|
435
|
+
}
|
|
436
|
+
.cas-clear:hover { color: rgba(0, 0, 0, 0.88); }
|
|
437
|
+
|
|
438
|
+
/* 级联面板:横向多级菜单并排 */
|
|
439
|
+
.cas-panel {
|
|
440
|
+
position: absolute;
|
|
441
|
+
z-index: 50;
|
|
442
|
+
top: calc(100% + 4px);
|
|
443
|
+
left: 0;
|
|
444
|
+
display: flex;
|
|
445
|
+
background: #ffffff;
|
|
446
|
+
border: 1px solid #d9d9d9;
|
|
447
|
+
border-radius: 8px;
|
|
448
|
+
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
449
|
+
}
|
|
450
|
+
.cas-menu {
|
|
451
|
+
width: 160px;
|
|
452
|
+
max-height: 240px;
|
|
453
|
+
overflow-y: auto;
|
|
454
|
+
padding: 0.375rem;
|
|
455
|
+
}
|
|
456
|
+
.cas-menu + .cas-menu {
|
|
457
|
+
border-left: 1px solid #f0f0f0;
|
|
458
|
+
}
|
|
459
|
+
.cas-option {
|
|
460
|
+
display: flex;
|
|
461
|
+
align-items: center;
|
|
462
|
+
justify-content: space-between;
|
|
463
|
+
gap: 0.5rem;
|
|
464
|
+
width: 100%;
|
|
465
|
+
padding: 0.375rem 0.5rem;
|
|
466
|
+
border: none;
|
|
467
|
+
border-radius: 4px;
|
|
468
|
+
background: transparent;
|
|
469
|
+
font-size: 0.875rem;
|
|
470
|
+
color: rgba(0, 0, 0, 0.88);
|
|
471
|
+
cursor: pointer;
|
|
472
|
+
text-align: left;
|
|
473
|
+
transition: background 0.15s ease;
|
|
474
|
+
}
|
|
475
|
+
.cas-option:hover { background: #f5f5f5; }
|
|
476
|
+
.cas-option-active { background: #e6f4ff; color: #1677ff; font-weight: 500; }
|
|
477
|
+
.cas-option-active:hover { background: #bae0ff; }
|
|
478
|
+
.cas-option-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
479
|
+
.cas-option-arrow { color: rgba(0, 0, 0, 0.45); font-size: 0.875rem; flex: 0 0 auto; }
|
|
480
|
+
.cas-empty {
|
|
481
|
+
padding: 0.75rem 0.5rem;
|
|
482
|
+
font-size: 0.8125rem;
|
|
483
|
+
color: rgba(0, 0, 0, 0.45);
|
|
484
|
+
text-align: center;
|
|
485
|
+
}
|
|
486
|
+
</style>
|