@c2-markdown-ui/vue 0.0.0-stage → 0.4.4

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.
Files changed (61) hide show
  1. package/README.md +112 -2
  2. package/dist/MarkdownUI.vue.d.ts +15 -0
  3. package/dist/favicon.ico +0 -0
  4. package/dist/index.css +1 -0
  5. package/dist/index.d.ts +23 -0
  6. package/dist/index.js +10843 -0
  7. package/dist/index.umd.cjs +53 -0
  8. package/dist/widgets/Button.vue.d.ts +19 -0
  9. package/dist/widgets/ButtonGroup.vue.d.ts +13 -0
  10. package/dist/widgets/Cascader.vue.d.ts +48 -0
  11. package/dist/widgets/Chart.vue.d.ts +24 -0
  12. package/dist/widgets/DatePicker.vue.d.ts +42 -0
  13. package/dist/widgets/Form.vue.d.ts +59 -0
  14. package/dist/widgets/Incomplete.vue.d.ts +2 -0
  15. package/dist/widgets/MultipleChoiceQuestion.vue.d.ts +20 -0
  16. package/dist/widgets/NumberInput.vue.d.ts +37 -0
  17. package/dist/widgets/Quiz.vue.d.ts +39 -0
  18. package/dist/widgets/RadioGroup.vue.d.ts +43 -0
  19. package/dist/widgets/Select.vue.d.ts +27 -0
  20. package/dist/widgets/SelectMulti.vue.d.ts +38 -0
  21. package/dist/widgets/ShortAnswerQuestion.vue.d.ts +22 -0
  22. package/dist/widgets/Slider.vue.d.ts +21 -0
  23. package/dist/widgets/Switch.vue.d.ts +28 -0
  24. package/dist/widgets/Text.vue.d.ts +22 -0
  25. package/dist/widgets/TextInput.vue.d.ts +32 -0
  26. package/dist/widgets/TreeSelect.vue.d.ts +109 -0
  27. package/dist/widgets/Widget.vue.d.ts +8 -0
  28. package/dist/widgets/chartRemoteData.d.ts +17 -0
  29. package/dist/widgets/index.d.ts +20 -0
  30. package/dist/widgets/remoteChoices.d.ts +22 -0
  31. package/dist/widgets/treeSelectTypes.d.ts +43 -0
  32. package/package.json +46 -4
  33. package/src/App.vue +11 -0
  34. package/src/lib/MarkdownUI.vue +105 -0
  35. package/src/lib/index.ts +28 -0
  36. package/src/lib/widgets/Button.vue +47 -0
  37. package/src/lib/widgets/ButtonGroup.vue +43 -0
  38. package/src/lib/widgets/Cascader.vue +475 -0
  39. package/src/lib/widgets/Chart.vue +287 -0
  40. package/src/lib/widgets/DatePicker.vue +659 -0
  41. package/src/lib/widgets/Form.vue +479 -0
  42. package/src/lib/widgets/Incomplete.vue +56 -0
  43. package/src/lib/widgets/MultipleChoiceQuestion.vue +73 -0
  44. package/src/lib/widgets/NumberInput.vue +179 -0
  45. package/src/lib/widgets/Quiz.vue +205 -0
  46. package/src/lib/widgets/RadioGroup.vue +237 -0
  47. package/src/lib/widgets/Select.vue +279 -0
  48. package/src/lib/widgets/SelectMulti.vue +145 -0
  49. package/src/lib/widgets/ShortAnswerQuestion.vue +79 -0
  50. package/src/lib/widgets/Slider.vue +61 -0
  51. package/src/lib/widgets/Switch.vue +68 -0
  52. package/src/lib/widgets/Text.vue +59 -0
  53. package/src/lib/widgets/TextInput.vue +83 -0
  54. package/src/lib/widgets/TreeSelect.vue +872 -0
  55. package/src/lib/widgets/Widget.vue +185 -0
  56. package/src/lib/widgets/chartRemoteData.ts +84 -0
  57. package/src/lib/widgets/index.ts +20 -0
  58. package/src/lib/widgets/remoteChoices.ts +64 -0
  59. package/src/lib/widgets/treeSelectTypes.ts +49 -0
  60. package/src/lib/widgets.css +1754 -0
  61. package/src/main.ts +4 -0
@@ -0,0 +1,185 @@
1
+ <template>
2
+ <div class="widget">
3
+ <component
4
+ v-if="WidgetComponent"
5
+ :is="WidgetComponent"
6
+ v-bind="parsed"
7
+ :initial-data="initialData"
8
+ @change="(value) => dispatch({ id: props.id, value })"
9
+ @submit="(value) => dispatchSubmit(props.id, value)"
10
+ @fieldchange="(stats) => dispatchFieldChange(props.id, stats)"
11
+ />
12
+ <span v-else style="color: red">
13
+ Unknown widget "{{ parsed.type }}"
14
+ </span>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { computed, defineProps } from 'vue'
20
+ import Button from './Button.vue'
21
+ import ButtonGroup from './ButtonGroup.vue'
22
+ import Cascader from './Cascader.vue'
23
+ import TreeSelect from './TreeSelect.vue'
24
+ import Chart from './Chart.vue'
25
+ import Form from './Form.vue'
26
+ import Incomplete from './Incomplete.vue'
27
+ import Select from './Select.vue'
28
+ import SelectMulti from './SelectMulti.vue'
29
+ import Slider from './Slider.vue'
30
+ import Switch from './Switch.vue'
31
+ import TextInput from './TextInput.vue'
32
+ import MultipleChoiceQuestion from './MultipleChoiceQuestion.vue'
33
+ import ShortAnswerQuestion from './ShortAnswerQuestion.vue'
34
+ import Quiz from './Quiz.vue'
35
+ import RadioGroup from './RadioGroup.vue'
36
+ import Text from './Text.vue'
37
+ import NumberInput from './NumberInput.vue'
38
+ import DatePicker from './DatePicker.vue'
39
+
40
+ interface WidgetProps {
41
+ id: string
42
+ content: string
43
+ /** 外部传入的表单初始值(encodeURIComponent 后的 JSON 字符串),由 MarkdownUI 注入 */
44
+ data?: string
45
+ }
46
+
47
+ const props = defineProps<WidgetProps>()
48
+
49
+ const widgets = {
50
+ 'Button': Button,
51
+ 'ButtonGroup': ButtonGroup,
52
+ 'Cascader': Cascader,
53
+ 'TreeSelect': TreeSelect,
54
+ 'Chart': Chart,
55
+ 'Form': Form,
56
+ 'Incomplete': Incomplete,
57
+ // Support both PascalCase and DSL type strings for robustness
58
+ 'MultipleChoiceQuestion': MultipleChoiceQuestion,
59
+ 'multiple-choice-question': MultipleChoiceQuestion,
60
+ 'Select': Select,
61
+ 'SelectMulti': SelectMulti,
62
+ 'ShortAnswerQuestion': ShortAnswerQuestion,
63
+ 'short-answer-question': ShortAnswerQuestion,
64
+ 'Slider': Slider,
65
+ 'Switch': Switch,
66
+ 'TextInput': TextInput,
67
+ 'Quiz': Quiz,
68
+ 'quiz': Quiz,
69
+ 'RadioGroup': RadioGroup,
70
+ 'radio-group': RadioGroup,
71
+ 'Text': Text,
72
+ 'text': Text,
73
+ 'NumberInput': NumberInput,
74
+ 'number-input': NumberInput,
75
+ 'DatePicker': DatePicker,
76
+ 'date-picker': DatePicker
77
+ }
78
+
79
+ const typeMapping: Record<string, keyof typeof widgets> = {
80
+ 'button': 'Button',
81
+ 'button-group': 'ButtonGroup',
82
+ 'cascade': 'Cascader',
83
+ 'tree-select': 'TreeSelect',
84
+ 'chart-line': 'Chart',
85
+ 'chart-bar': 'Chart',
86
+ 'chart-pie': 'Chart',
87
+ 'chart-scatter': 'Chart',
88
+ 'form': 'Form',
89
+ 'incomplete': 'Incomplete',
90
+ 'multiple-choice-question': 'MultipleChoiceQuestion',
91
+ 'quiz': 'Quiz',
92
+ 'radio-group': 'RadioGroup',
93
+ 'select': 'Select',
94
+ 'select-multi': 'SelectMulti',
95
+ 'short-answer-question': 'ShortAnswerQuestion',
96
+ 'slider': 'Slider',
97
+ 'switch': 'Switch',
98
+ 'text': 'Text',
99
+ 'text-input': 'TextInput',
100
+ 'number-input': 'NumberInput',
101
+ 'date-picker': 'DatePicker'
102
+ }
103
+
104
+ let parsed = { type: 'incomplete' }
105
+
106
+ try {
107
+ parsed = JSON.parse(decodeURIComponent(props.content))
108
+ } catch (e) {
109
+ // suppress intermediate states
110
+ }
111
+
112
+ // 解析外部注入的表单初始值(容错:非法 JSON 时忽略)
113
+ const initialData = computed(() => {
114
+ if (!props.data) return undefined
115
+ try {
116
+ return JSON.parse(decodeURIComponent(props.data))
117
+ } catch {
118
+ return undefined
119
+ }
120
+ })
121
+
122
+ const WidgetComponent = computed(() => {
123
+ const rawType = parsed.type as string
124
+ const mapped = typeMapping[rawType] || rawType
125
+ let component = widgets[mapped as keyof typeof widgets]
126
+
127
+ // Fallback: try PascalCase from hyphen/underscore types
128
+ if (!component && typeof mapped === 'string') {
129
+ const pascal = mapped
130
+ .split(/[-_]/)
131
+ .map(s => s.charAt(0).toUpperCase() + s.slice(1))
132
+ .join('')
133
+ component = widgets[pascal as keyof typeof widgets]
134
+ }
135
+
136
+ return component
137
+ })
138
+
139
+ const dispatch = (detail: any) => {
140
+ const event = new CustomEvent('widget-event', {
141
+ detail: detail,
142
+ bubbles: true,
143
+ composed: true
144
+ })
145
+
146
+ // Find the custom element and bubble up to find the closest widget container
147
+ const customElement = document.querySelector(`markdown-ui-widget[id="${props.id}"]`)
148
+ if (customElement) {
149
+ const container = customElement.closest('.widget-container')
150
+ if (container) {
151
+ container.dispatchEvent(event)
152
+ }
153
+ }
154
+ }
155
+
156
+ /** form 提交事件:独立于 widget-event,通过 widget-submit 派发 */
157
+ const dispatchSubmit = (id: string, value: unknown) => {
158
+ const customElement = document.querySelector(`markdown-ui-widget[id="${id}"]`)
159
+ if (customElement) {
160
+ const container = customElement.closest('.widget-container')
161
+ if (container) {
162
+ container.dispatchEvent(new CustomEvent('widget-submit', {
163
+ detail: { id, value },
164
+ bubbles: true,
165
+ composed: true
166
+ }))
167
+ }
168
+ }
169
+ }
170
+
171
+ /** form 字段级统计事件:任一表单项被填写时通过 widget-form-change 派发 */
172
+ const dispatchFieldChange = (id: string, stats: unknown) => {
173
+ const customElement = document.querySelector(`markdown-ui-widget[id="${id}"]`)
174
+ if (customElement) {
175
+ const container = customElement.closest('.widget-container')
176
+ if (container) {
177
+ container.dispatchEvent(new CustomEvent('widget-form-change', {
178
+ detail: { id, stats },
179
+ bubbles: true,
180
+ composed: true
181
+ }))
182
+ }
183
+ }
184
+ }
185
+ </script>
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 图表远端数据源解析:
3
+ * - CSV 文本(与 DSL 内嵌 CSV 同格式:首行 header「标签列,系列1,系列2...」,其后数据行)
4
+ * - JSON:支持 { labels, datasets:[{label,data}] } 及常见包装({data|result} 内嵌)
5
+ */
6
+
7
+ export interface ChartDataset {
8
+ label: string;
9
+ data: number[];
10
+ }
11
+
12
+ export interface RemoteChartData {
13
+ labels: string[];
14
+ datasets: ChartDataset[];
15
+ }
16
+
17
+ /** 解析 CSV 文本为图表数据(header 至少 2 列,数据列须为数字) */
18
+ export function parseChartCsv(text: string): RemoteChartData | null {
19
+ const lines = text
20
+ .split('\n')
21
+ .map(l => l.trim())
22
+ .filter(l => l.length > 0);
23
+ if (lines.length === 0) return null;
24
+
25
+ const headers = lines[0].split(',').map(h => h.trim());
26
+ if (headers.length < 2) return null;
27
+
28
+ const labels: string[] = [];
29
+ const datasets: ChartDataset[] = headers.slice(1).map(label => ({ label, data: [] }));
30
+
31
+ for (let i = 1; i < lines.length; i++) {
32
+ const values = lines[i].split(',').map(v => v.trim());
33
+ if (values.length !== headers.length) return null;
34
+ labels.push(values[0]);
35
+ for (let j = 1; j < values.length; j++) {
36
+ const num = parseFloat(values[j]);
37
+ if (isNaN(num)) return null;
38
+ datasets[j - 1].data.push(num);
39
+ }
40
+ }
41
+ return { labels, datasets };
42
+ }
43
+
44
+ /** 归一化 JSON 响应为图表数据(labels + datasets) */
45
+ export function normalizeRemoteChartData(json: any): RemoteChartData | null {
46
+ // 解开常见包装:{data|result|chart} 内嵌
47
+ let payload = json;
48
+ for (const key of ['data', 'result', 'chart']) {
49
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)
50
+ && payload[key] && typeof payload[key] === 'object') {
51
+ payload = payload[key];
52
+ }
53
+ }
54
+
55
+ if (!payload || typeof payload !== 'object') return null;
56
+
57
+ // 直接是 { labels, datasets } 结构
58
+ let labels: any = payload.labels;
59
+ let datasets: any = payload.datasets;
60
+
61
+ // 数组形式:[{ label, value }] → 单系列
62
+ if (Array.isArray(payload) && payload.length > 0 && payload[0]?.label !== undefined && payload[0]?.value !== undefined) {
63
+ labels = payload.map((it: any) => it.label);
64
+ datasets = [{ label: 'Series', data: payload.map((it: any) => Number(it.value)) }];
65
+ }
66
+
67
+ if (!Array.isArray(labels) || !Array.isArray(datasets) || labels.length === 0 || datasets.length === 0) {
68
+ return null;
69
+ }
70
+
71
+ const normLabels = labels.map((l: any) => String(l));
72
+ const normDatasets: ChartDataset[] = [];
73
+ for (const ds of datasets) {
74
+ if (!ds || !Array.isArray(ds.data)) continue;
75
+ const data = ds.data.map((v: any) => {
76
+ const num = Number(v);
77
+ return isNaN(num) ? 0 : num;
78
+ });
79
+ normDatasets.push({ label: String(ds.label ?? 'Series'), data });
80
+ }
81
+ if (normDatasets.length === 0) return null;
82
+
83
+ return { labels: normLabels, datasets: normDatasets };
84
+ }
@@ -0,0 +1,20 @@
1
+ export { default as Button } from './Button.vue'
2
+ export { default as ButtonGroup } from './ButtonGroup.vue'
3
+ export { default as Cascader } from './Cascader.vue'
4
+ export { default as Chart } from './Chart.vue'
5
+ export { default as Form } from './Form.vue'
6
+ export { default as Incomplete } from './Incomplete.vue'
7
+ export { default as MultipleChoiceQuestion } from './MultipleChoiceQuestion.vue'
8
+ export { default as Select } from './Select.vue'
9
+ export { default as SelectMulti } from './SelectMulti.vue'
10
+ export { default as TreeSelect } from './TreeSelect.vue'
11
+ export { default as ShortAnswerQuestion } from './ShortAnswerQuestion.vue'
12
+ export { default as Slider } from './Slider.vue'
13
+ export { default as Switch } from './Switch.vue'
14
+ export { default as TextInput } from './TextInput.vue'
15
+ export { default as NumberInput } from './NumberInput.vue'
16
+ export { default as DatePicker } from './DatePicker.vue'
17
+ export { default as Text } from './Text.vue'
18
+ export { default as Quiz } from './Quiz.vue'
19
+ export { default as RadioGroup } from './RadioGroup.vue'
20
+ export { default as Widget } from './Widget.vue'
@@ -0,0 +1,64 @@
1
+ /** 远端选项:value 是表单实际取值,label 是展示文案 */
2
+ export interface RemoteChoice {
3
+ value: string;
4
+ label: string;
5
+ }
6
+
7
+ /**
8
+ * 兼容常见远端响应格式并归一化为 {value,label}:
9
+ * - ["测试"] → value=label="测试"
10
+ * - [{key:"test",label:"测试"}] → value="test"(表单取值),label="测试"(展示)
11
+ * - [{value:"test",label:"测试"}] → value="test",label="测试"
12
+ * - {data:[...]} / {options:[...]} / {choices:[...]} / {items:[...]} → 取内嵌数组
13
+ *
14
+ * 对象条目取值优先级:value > key > id > name;展示优先级:label > name > text > value。
15
+ */
16
+ export function normalizeRemoteChoices(json: any): RemoteChoice[] {
17
+ let list = json;
18
+ if (!Array.isArray(json)) {
19
+ list = Array.isArray(json?.data) ? json.data
20
+ : Array.isArray(json?.options) ? json.options
21
+ : Array.isArray(json?.choices) ? json.choices
22
+ : Array.isArray(json?.items) ? json.items
23
+ : [];
24
+ }
25
+ return list
26
+ .map((item: any): RemoteChoice | null => {
27
+ if (typeof item === 'string' || typeof item === 'number') {
28
+ const s = String(item);
29
+ return s ? { value: s, label: s } : null;
30
+ }
31
+ if (item && typeof item === 'object') {
32
+ const value = item.value ?? item.key ?? item.id ?? item.name;
33
+ if (value === undefined || value === null || value === '') return null;
34
+ const label = item.label ?? item.name ?? item.text ?? String(value);
35
+ return { value: String(value), label: String(label) };
36
+ }
37
+ return null;
38
+ })
39
+ .filter((c: RemoteChoice | null): c is RemoteChoice => c !== null);
40
+ }
41
+
42
+ /** 解析本地 choices 条目的 key:label 格式:"bj:北京" → {value:"bj",label:"北京"};不符规则时 value=label=原文 */
43
+ export function parseChoice(c: string): RemoteChoice {
44
+ const s = String(c ?? '');
45
+ const idx = s.indexOf(':');
46
+ if (idx > 0) {
47
+ const key = s.slice(0, idx);
48
+ const label = s.slice(idx + 1);
49
+ if (key && label && !/\s/.test(key) && !/^\d+$/.test(key) && !label.startsWith('//')) {
50
+ return { value: key, label };
51
+ }
52
+ }
53
+ return { value: s, label: s };
54
+ }
55
+
56
+ /**
57
+ * 把本地 choices / default 值(可能写的是展示文案或 key:label 原文)映射到远端 value 体系:
58
+ * 条目本身已是 value 优先,字符串则用 label 匹配远端条目回查 value。
59
+ */
60
+ export function resolveChoiceValue(c: string, remote: RemoteChoice[]): string {
61
+ const parsed = parseChoice(c).value;
62
+ const hit = remote.find(r => r.value === c || r.label === c || r.value === parsed);
63
+ return hit ? hit.value : c;
64
+ }
@@ -0,0 +1,49 @@
1
+ // 树形选择组件共享类型:
2
+ // 单独成文件是因为 TreeSelect.vue 的 <script setup>(主组件)与普通 <script>(TreeNode 子组件)
3
+ // 是两个独立作用域,无法共享局部 interface;放此处两边统一 import,避免与 DOM 全局 Node 类型冲突
4
+
5
+ /** 远端/静态 data 原始节点(容错:value/key、label/name 等字段映射) */
6
+ export interface TreeNodeRaw {
7
+ value?: string | number
8
+ key?: string | number
9
+ label?: string | number
10
+ children?: TreeNodeRaw[]
11
+ loaded?: boolean
12
+ [k: string]: any
13
+ }
14
+
15
+ /** 内部归一化树节点 */
16
+ export interface TreeSelectNode {
17
+ value: string
18
+ label: string
19
+ children: TreeSelectNode[]
20
+ loaded: boolean
21
+ depth: number
22
+ }
23
+
24
+ /** TreeSelect 组件 props */
25
+ export interface TreeSelectProps {
26
+ choices: string[]
27
+ data?: TreeNodeRaw[]
28
+ url?: string
29
+ lazy?: boolean
30
+ /** 多选模式(antd 默认单选,multiple 开启多选) */
31
+ multiple?: boolean
32
+ /** 父子节点选中不关联(antd checkStrictly) */
33
+ checkStrictly?: boolean
34
+ /** 选中值回显策略:all / parent / child(antd SHOW_ALL/SHOW_PARENT/SHOW_CHILD) */
35
+ showCheckedStrategy?: string
36
+ /** 触发器最多显示的标签数,超出 +N(antd maxTagCount) */
37
+ maxTagCount?: number
38
+ /** 树连接线(antd treeLine) */
39
+ treeLine?: boolean
40
+ label?: string
41
+ default?: string | string[]
42
+ placeholder?: string
43
+ hint?: string
44
+ required?: boolean
45
+ message?: string
46
+ error?: string
47
+ disabled?: boolean
48
+ searchable?: boolean
49
+ }