@svadmin/lite 0.13.0 → 0.14.0
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/components/LiteErrorSummary.svelte +34 -0
- package/dist/components/LiteErrorSummary.svelte.d.ts +14 -0
- package/dist/components/LiteFilterBuilder.svelte +95 -36
- package/dist/components/LiteGanttChart.svelte +43 -10
- package/dist/components/LiteGanttChart.svelte.d.ts +6 -0
- package/dist/components/LiteJsonSchemaForm.svelte +90 -44
- package/dist/components/LiteJsonSchemaForm.svelte.d.ts +1 -10
- package/dist/components/LiteKanbanBoard.svelte +56 -8
- package/dist/components/LiteKanbanBoard.svelte.d.ts +4 -0
- package/dist/components/LitePivotTable.svelte +88 -58
- package/dist/components/LitePivotTable.svelte.d.ts +13 -1
- package/dist/components/LiteSpreadsheetView.svelte +55 -9
- package/dist/components/LiteSpreadsheetView.svelte.d.ts +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/schema-form-data.d.ts +18 -0
- package/dist/schema-form-data.js +264 -0
- package/dist/server-adapter.js +141 -26
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
2
3
|
export interface KanbanCard {
|
|
3
4
|
id: string;
|
|
4
5
|
title: string;
|
|
@@ -17,6 +18,10 @@
|
|
|
17
18
|
columns: KanbanColumn[];
|
|
18
19
|
cards?: KanbanCard[];
|
|
19
20
|
formAction?: string;
|
|
21
|
+
loading?: boolean;
|
|
22
|
+
error?: string | undefined;
|
|
23
|
+
retryHref?: string;
|
|
24
|
+
ariaLabel?: string;
|
|
20
25
|
class?: string;
|
|
21
26
|
}
|
|
22
27
|
|
|
@@ -24,14 +29,54 @@
|
|
|
24
29
|
columns = [],
|
|
25
30
|
cards = [],
|
|
26
31
|
formAction = '',
|
|
32
|
+
loading = false,
|
|
33
|
+
error,
|
|
34
|
+
retryHref,
|
|
35
|
+
ariaLabel,
|
|
27
36
|
class: className = '',
|
|
28
37
|
}: Props = $props();
|
|
38
|
+
const i18n = useTranslation();
|
|
39
|
+
function text(value: unknown): value is string {
|
|
40
|
+
return typeof value === 'string' && value.trim().length > 0 && value.length <= 1000;
|
|
41
|
+
}
|
|
42
|
+
const validationError = $derived.by(() => {
|
|
43
|
+
if (!Array.isArray(columns) || !Array.isArray(cards)) return 'kanban.invalidData';
|
|
44
|
+
if (columns.length > 200 || cards.length > 1000) return 'kanban.limit';
|
|
45
|
+
if (columns.some(column => !column || !text(column.id) || !text(column.title))) return 'kanban.invalidData';
|
|
46
|
+
const ids = new Set(columns.map(column => column.id));
|
|
47
|
+
if (ids.size !== columns.length) return 'kanban.invalidData';
|
|
48
|
+
const cardIds = new Set<string>();
|
|
49
|
+
for (const card of cards) {
|
|
50
|
+
if (!card || !text(card.id) || !text(card.title) || !ids.has(card.columnId) || cardIds.has(card.id)
|
|
51
|
+
|| (card.description !== undefined && (typeof card.description !== 'string' || card.description.length > 10_000))
|
|
52
|
+
|| (card.tags !== undefined && (!Array.isArray(card.tags) || card.tags.length > 100 || !card.tags.every(text)))
|
|
53
|
+
|| (card.priority !== undefined && !['low', 'medium', 'high', 'urgent'].includes(card.priority))) return 'kanban.invalidData';
|
|
54
|
+
cardIds.add(card.id);
|
|
55
|
+
}
|
|
56
|
+
return '';
|
|
57
|
+
});
|
|
58
|
+
const displayError = $derived(error || (validationError ? i18n.t(validationError) : ''));
|
|
59
|
+
function localPath(value: string | undefined): string | undefined {
|
|
60
|
+
return value && (value.startsWith('/') || value.startsWith('?'))
|
|
61
|
+
&& !/^\/[\\/]/.test(value) && !value.includes('\\')
|
|
62
|
+
&& ![...value].some(char => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127) ? value : undefined;
|
|
63
|
+
}
|
|
64
|
+
const safeAction = $derived(localPath(formAction));
|
|
65
|
+
const safeRetry = $derived(localPath(retryHref));
|
|
29
66
|
</script>
|
|
30
67
|
|
|
31
|
-
<div class="sv-lite-kanban-board {className}">
|
|
68
|
+
<div class="sv-lite-kanban-board {className}" role="region" aria-label={ariaLabel ?? i18n.t('kanban.label')} aria-busy={loading}>
|
|
69
|
+
{#if loading}
|
|
70
|
+
<div role="status">{i18n.t('common.loading')}</div>
|
|
71
|
+
{:else if displayError}
|
|
72
|
+
<div role="alert">{displayError}</div>
|
|
73
|
+
{#if error && safeRetry}<a href={safeRetry}>{i18n.t('common.retry')}</a>{/if}
|
|
74
|
+
{:else if columns.length === 0}
|
|
75
|
+
<div role="status">{i18n.t('common.noData')}</div>
|
|
76
|
+
{:else}
|
|
32
77
|
{#each columns as col (col.id)}
|
|
33
78
|
{@const colCards = cards.filter((c) => c.columnId === col.id)}
|
|
34
|
-
<div class="sv-lite-kanban-column">
|
|
79
|
+
<div class="sv-lite-kanban-column" role="region" aria-label={col.title}>
|
|
35
80
|
<div class="sv-lite-column-header">
|
|
36
81
|
<span class="sv-lite-column-title">{col.title}</span>
|
|
37
82
|
<span class="sv-lite-column-count">({colCards.length})</span>
|
|
@@ -39,7 +84,7 @@
|
|
|
39
84
|
|
|
40
85
|
<div class="sv-lite-column-cards">
|
|
41
86
|
{#each colCards as card (card.id)}
|
|
42
|
-
<div class="sv-lite-kanban-card">
|
|
87
|
+
<div class="sv-lite-kanban-card" role="group" aria-label={card.title}>
|
|
43
88
|
<div class="sv-lite-card-title">{card.title}</div>
|
|
44
89
|
{#if card.description}
|
|
45
90
|
<div class="sv-lite-card-desc">{card.description}</div>
|
|
@@ -48,23 +93,26 @@
|
|
|
48
93
|
{#if card.priority}
|
|
49
94
|
<span class="sv-lite-priority-badge sv-lite-{card.priority}">{card.priority}</span>
|
|
50
95
|
{/if}
|
|
51
|
-
{#each card.tags ?? [] as tag (
|
|
96
|
+
{#each card.tags ?? [] as tag, index (index)}
|
|
52
97
|
<span class="sv-lite-tag-badge">#{tag}</span>
|
|
53
98
|
{/each}
|
|
54
99
|
</div>
|
|
55
100
|
</div>
|
|
101
|
+
{:else}
|
|
102
|
+
<div role="status">{i18n.t('common.noData')}</div>
|
|
56
103
|
{/each}
|
|
57
104
|
</div>
|
|
58
105
|
|
|
59
|
-
{#if
|
|
60
|
-
<form method="POST" action={
|
|
106
|
+
{#if safeAction && cards.length < 1000}
|
|
107
|
+
<form method="POST" action={safeAction} class="sv-lite-card-form" aria-label={i18n.t('kanban.addTo', { column: col.title })}>
|
|
61
108
|
<input type="hidden" name="columnId" value={col.id} />
|
|
62
|
-
<input type="text" name="title" placeholder=
|
|
63
|
-
<button type="submit" class="sv-lite-card-btn">
|
|
109
|
+
<input type="text" name="title" aria-label={i18n.t('kanban.cardTitle')} placeholder={i18n.t('kanban.cardTitle')} required maxlength={1000} class="sv-lite-card-input" />
|
|
110
|
+
<button type="submit" class="sv-lite-card-btn">{i18n.t('common.add')}</button>
|
|
64
111
|
</form>
|
|
65
112
|
{/if}
|
|
66
113
|
</div>
|
|
67
114
|
{/each}
|
|
115
|
+
{/if}
|
|
68
116
|
</div>
|
|
69
117
|
|
|
70
118
|
<style>
|
|
@@ -14,6 +14,10 @@ interface Props {
|
|
|
14
14
|
columns: KanbanColumn[];
|
|
15
15
|
cards?: KanbanCard[];
|
|
16
16
|
formAction?: string;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
error?: string | undefined;
|
|
19
|
+
retryHref?: string;
|
|
20
|
+
ariaLabel?: string;
|
|
17
21
|
class?: string;
|
|
18
22
|
}
|
|
19
23
|
declare const LiteKanbanBoard: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
|
|
2
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
+
import { buildPivot, buildPivotDrilldown, decodePivotResult, formatPivotValue } from '@svadmin/core/pivot';
|
|
4
|
+
export type AggregationFn = import('@svadmin/core/pivot').PivotAggregation;
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
5
7
|
data?: Record<string, unknown>[];
|
|
8
|
+
aggregateData?: unknown;
|
|
6
9
|
rowField: string;
|
|
7
10
|
rowLabel?: string;
|
|
8
11
|
columnField: string;
|
|
@@ -10,11 +13,23 @@
|
|
|
10
13
|
valueField: string;
|
|
11
14
|
valueLabel?: string;
|
|
12
15
|
aggregator?: AggregationFn;
|
|
16
|
+
formatValue?: (value: number) => string;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
error?: string | undefined;
|
|
19
|
+
retryHref?: string;
|
|
20
|
+
exportHref?: string;
|
|
21
|
+
drilldownHref?: (input: import('@svadmin/core/pivot').PivotDrilldown) => string | undefined;
|
|
22
|
+
resource?: string;
|
|
23
|
+
scopeKey?: string;
|
|
24
|
+
filters?: import('@svadmin/core').Filter[];
|
|
25
|
+
meta?: Record<string, unknown>;
|
|
26
|
+
ariaLabel?: string;
|
|
13
27
|
class?: string;
|
|
14
28
|
}
|
|
15
29
|
|
|
16
30
|
let {
|
|
17
31
|
data = [],
|
|
32
|
+
aggregateData,
|
|
18
33
|
rowField,
|
|
19
34
|
rowLabel = rowField,
|
|
20
35
|
columnField,
|
|
@@ -22,91 +37,106 @@
|
|
|
22
37
|
valueField,
|
|
23
38
|
valueLabel = valueField,
|
|
24
39
|
aggregator = 'sum',
|
|
40
|
+
formatValue,
|
|
41
|
+
loading = false,
|
|
42
|
+
error,
|
|
43
|
+
retryHref,
|
|
44
|
+
exportHref,
|
|
45
|
+
drilldownHref,
|
|
46
|
+
resource,
|
|
47
|
+
scopeKey,
|
|
48
|
+
filters,
|
|
49
|
+
meta,
|
|
50
|
+
ariaLabel,
|
|
25
51
|
class: className = '',
|
|
26
52
|
}: Props = $props();
|
|
27
53
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (aggregator === 'sum') return vals.reduce((a, b) => a + b, 0);
|
|
40
|
-
if (aggregator === 'avg') return vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
41
|
-
if (aggregator === 'min') return Math.min(...vals);
|
|
42
|
-
if (aggregator === 'max') return Math.max(...vals);
|
|
43
|
-
return 0;
|
|
54
|
+
const i18n = useTranslation();
|
|
55
|
+
const pivot = $derived(aggregateData !== undefined ? decodePivotResult(aggregateData)
|
|
56
|
+
: buildPivot(loading || error ? [] : data, { rowField, columnField, valueField, aggregator }));
|
|
57
|
+
const displayError = $derived(error || (pivot.error ? i18n.t(`pivot.${pivot.error}`) : ''));
|
|
58
|
+
// 重试仅接受站内绝对路径,避免在服务端页面输出可执行协议。
|
|
59
|
+
const safeRetryHref = $derived(safeHref(retryHref));
|
|
60
|
+
const safeExportHref = $derived(safeHref(exportHref));
|
|
61
|
+
function safeHref(value: string | undefined): string | undefined {
|
|
62
|
+
return value?.startsWith('/') && !/^\/[\\/]/.test(value)
|
|
63
|
+
&& !value.includes('\\')
|
|
64
|
+
&& ![...value].some(char => char.charCodeAt(0) <= 32 || char.charCodeAt(0) === 127) ? value : undefined;
|
|
44
65
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
function cellHref(rowKey: string, columnKey: string): string | undefined {
|
|
67
|
+
if (!drilldownHref || !pivot.cells.get(rowKey)?.has(columnKey)) return;
|
|
68
|
+
const row = pivot.rows.find(item => item.key === rowKey);
|
|
69
|
+
const column = pivot.columns.find(item => item.key === columnKey);
|
|
70
|
+
const query = { resource, rowField, columnField, valueField, aggregator,
|
|
71
|
+
...(filters === undefined ? {} : { filters }), ...(meta === undefined ? {} : { meta }) };
|
|
72
|
+
const input = buildPivotDrilldown(query, scopeKey, row, column);
|
|
73
|
+
try {
|
|
74
|
+
return safeHref(input ? drilldownHref(input) : undefined);
|
|
75
|
+
} catch {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
51
78
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const vals = data
|
|
55
|
-
.filter((d) => String(d[rowField] ?? '—') === r)
|
|
56
|
-
.map((d) => Number(d[valueField]) || 0);
|
|
57
|
-
return computeAgg(vals);
|
|
79
|
+
function display(value: number | null | undefined): string {
|
|
80
|
+
return formatPivotValue(value, formatValue ?? (v => v.toLocaleString(i18n.locale, { maximumFractionDigits: 2 })));
|
|
58
81
|
}
|
|
59
|
-
|
|
60
|
-
function getColTotal(c: string): number {
|
|
61
|
-
const vals = data
|
|
62
|
-
.filter((d) => String(d[columnField] ?? '—') === c)
|
|
63
|
-
.map((d) => Number(d[valueField]) || 0);
|
|
64
|
-
return computeAgg(vals);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const grandTotal = $derived(
|
|
68
|
-
computeAgg(data.map((d) => Number(d[valueField]) || 0))
|
|
69
|
-
);
|
|
70
82
|
</script>
|
|
71
83
|
|
|
72
|
-
<div class="sv-lite-pivot-container {className}">
|
|
84
|
+
<div class="sv-lite-pivot-container {className}" aria-busy={loading}>
|
|
73
85
|
<div class="sv-lite-pivot-header">
|
|
74
|
-
<strong>
|
|
75
|
-
<span class="sv-lite-pivot-sub">
|
|
86
|
+
<strong>{i18n.t('pivot.title')}</strong> ({rowLabel} × {columnLabel})
|
|
87
|
+
<span class="sv-lite-pivot-sub">{i18n.t(`pivot.${aggregator}`)} ({valueLabel})</span>
|
|
88
|
+
{#if safeExportHref}<a href={safeExportHref}>{i18n.t('common.export')}</a>{/if}
|
|
76
89
|
</div>
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
{#if loading}
|
|
92
|
+
<div role="status">{i18n.t('common.loading')}</div>
|
|
93
|
+
{:else if displayError}
|
|
94
|
+
<div role="alert">{displayError}</div>
|
|
95
|
+
{#if error && safeRetryHref}<a href={safeRetryHref}>{i18n.t('common.retry')}</a>{/if}
|
|
96
|
+
{:else if pivot.rows.length === 0}
|
|
97
|
+
<div role="status">{i18n.t('common.noData')}</div>
|
|
98
|
+
{:else}
|
|
99
|
+
<table class="sv-lite-pivot-table" aria-label={ariaLabel ?? `${i18n.t('pivot.title')}: ${rowLabel} / ${columnLabel} / ${valueLabel}`}>
|
|
79
100
|
<thead>
|
|
80
101
|
<tr>
|
|
81
|
-
<th class="sv-lite-th-dim">{rowLabel} \ {columnLabel}</th>
|
|
82
|
-
{#each
|
|
83
|
-
<th>{col}</th>
|
|
102
|
+
<th scope="col" class="sv-lite-th-dim">{rowLabel} \ {columnLabel}</th>
|
|
103
|
+
{#each pivot.columns as col (col.key)}
|
|
104
|
+
<th scope="col">{col.label}</th>
|
|
84
105
|
{/each}
|
|
85
|
-
<th class="sv-lite-th-total">
|
|
106
|
+
<th scope="col" class="sv-lite-th-total">{i18n.t('pivot.total')}</th>
|
|
86
107
|
</tr>
|
|
87
108
|
</thead>
|
|
88
109
|
<tbody>
|
|
89
|
-
{#each
|
|
110
|
+
{#each pivot.rows as row (row.key)}
|
|
90
111
|
<tr>
|
|
91
|
-
<
|
|
92
|
-
{#each
|
|
93
|
-
{@const
|
|
94
|
-
<td class="sv-lite-td-val">
|
|
112
|
+
<th scope="row" class="sv-lite-td-dim">{row.label}</th>
|
|
113
|
+
{#each pivot.columns as col (col.key)}
|
|
114
|
+
{@const href = cellHref(row.key, col.key)}
|
|
115
|
+
<td class="sv-lite-td-val">
|
|
116
|
+
{#if href}
|
|
117
|
+
<a href={href} aria-label={`${i18n.t('pivot.drilldown')}: ${row.label} / ${col.label}`}>
|
|
118
|
+
{display(pivot.cells.get(row.key)?.get(col.key))}
|
|
119
|
+
</a>
|
|
120
|
+
{:else}
|
|
121
|
+
{display(pivot.cells.get(row.key)?.get(col.key))}
|
|
122
|
+
{/if}
|
|
123
|
+
</td>
|
|
95
124
|
{/each}
|
|
96
|
-
<td class="sv-lite-td-rowtotal">{
|
|
125
|
+
<td class="sv-lite-td-rowtotal">{display(pivot.rowTotals.get(row.key))}</td>
|
|
97
126
|
</tr>
|
|
98
127
|
{/each}
|
|
99
128
|
</tbody>
|
|
100
129
|
<tfoot>
|
|
101
130
|
<tr>
|
|
102
|
-
<
|
|
103
|
-
{#each
|
|
104
|
-
<td class="sv-lite-td-val">{
|
|
131
|
+
<th scope="row" class="sv-lite-td-dim">{i18n.t('pivot.grandTotal')}</th>
|
|
132
|
+
{#each pivot.columns as col (col.key)}
|
|
133
|
+
<td class="sv-lite-td-val">{display(pivot.columnTotals.get(col.key))}</td>
|
|
105
134
|
{/each}
|
|
106
|
-
<td class="sv-lite-td-grandtotal">{grandTotal
|
|
135
|
+
<td class="sv-lite-td-grandtotal">{display(pivot.grandTotal)}</td>
|
|
107
136
|
</tr>
|
|
108
137
|
</tfoot>
|
|
109
138
|
</table>
|
|
139
|
+
{/if}
|
|
110
140
|
</div>
|
|
111
141
|
|
|
112
142
|
<style>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export type AggregationFn = '
|
|
1
|
+
export type AggregationFn = import('@svadmin/core/pivot').PivotAggregation;
|
|
2
2
|
interface Props {
|
|
3
3
|
data?: Record<string, unknown>[];
|
|
4
|
+
aggregateData?: unknown;
|
|
4
5
|
rowField: string;
|
|
5
6
|
rowLabel?: string;
|
|
6
7
|
columnField: string;
|
|
@@ -8,6 +9,17 @@ interface Props {
|
|
|
8
9
|
valueField: string;
|
|
9
10
|
valueLabel?: string;
|
|
10
11
|
aggregator?: AggregationFn;
|
|
12
|
+
formatValue?: (value: number) => string;
|
|
13
|
+
loading?: boolean;
|
|
14
|
+
error?: string | undefined;
|
|
15
|
+
retryHref?: string;
|
|
16
|
+
exportHref?: string;
|
|
17
|
+
drilldownHref?: (input: import('@svadmin/core/pivot').PivotDrilldown) => string | undefined;
|
|
18
|
+
resource?: string;
|
|
19
|
+
scopeKey?: string;
|
|
20
|
+
filters?: import('@svadmin/core').Filter[];
|
|
21
|
+
meta?: Record<string, unknown>;
|
|
22
|
+
ariaLabel?: string;
|
|
11
23
|
class?: string;
|
|
12
24
|
}
|
|
13
25
|
declare const LitePivotTable: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
+
const i18n = useTranslation();
|
|
4
|
+
|
|
2
5
|
export interface SheetData {
|
|
3
6
|
id: string;
|
|
4
7
|
name: string;
|
|
@@ -11,6 +14,8 @@
|
|
|
11
14
|
sheets?: SheetData[];
|
|
12
15
|
activeSheetId?: string;
|
|
13
16
|
formAction?: string;
|
|
17
|
+
readonly?: boolean;
|
|
18
|
+
limits?: Partial<{ maxRows: number; maxCols: number; maxCells: number; maxSheets: number; maxCellLength: number }>;
|
|
14
19
|
class?: string;
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -26,25 +31,63 @@
|
|
|
26
31
|
],
|
|
27
32
|
activeSheetId = 'sheet1',
|
|
28
33
|
formAction = '',
|
|
34
|
+
readonly = false,
|
|
35
|
+
limits: limitOverrides = {},
|
|
29
36
|
class: className = '',
|
|
30
37
|
}: Props = $props();
|
|
31
38
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
const limits = $derived.by(() => {
|
|
40
|
+
const bounded = (value: number | undefined, ceiling: number) =>
|
|
41
|
+
typeof value === 'number' && Number.isSafeInteger(value) && value > 0 ? Math.min(value, ceiling) : ceiling;
|
|
42
|
+
return {
|
|
43
|
+
maxRows: bounded(limitOverrides.maxRows, 1000),
|
|
44
|
+
maxCols: bounded(limitOverrides.maxCols, 100),
|
|
45
|
+
maxCells: bounded(limitOverrides.maxCells, 10_000),
|
|
46
|
+
maxSheets: bounded(limitOverrides.maxSheets, 32),
|
|
47
|
+
maxCellLength: bounded(limitOverrides.maxCellLength, 10_000),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
const validWorkbook = $derived(sheets.length <= limits.maxSheets && new Set(sheets.map(sheet => sheet.id)).size === sheets.length);
|
|
51
|
+
const currentSheet = $derived(validWorkbook ? (sheets.find(s => s.id === activeSheetId) ?? sheets[0]) : undefined);
|
|
52
|
+
const canRender = $derived.by(() => {
|
|
53
|
+
if (!currentSheet || !currentSheet.cells || typeof currentSheet.cells !== 'object'
|
|
54
|
+
|| !Number.isSafeInteger(currentSheet.rows) || currentSheet.rows < 1 || currentSheet.rows > limits.maxRows
|
|
55
|
+
|| !Number.isSafeInteger(currentSheet.cols) || currentSheet.cols < 1 || currentSheet.cols > limits.maxCols
|
|
56
|
+
|| currentSheet.rows * currentSheet.cols > limits.maxCells) return false;
|
|
57
|
+
let textLength = 0;
|
|
58
|
+
for (let row = 1; row <= currentSheet.rows; row++) {
|
|
59
|
+
for (let col = 0; col < currentSheet.cols; col++) {
|
|
60
|
+
const key = `${getColName(col)}${row}`;
|
|
61
|
+
const raw = Object.hasOwn(currentSheet.cells, key) ? currentSheet.cells[key] : '';
|
|
62
|
+
if (typeof raw !== 'string' || raw.length > limits.maxCellLength) return false;
|
|
63
|
+
textLength += raw.length;
|
|
64
|
+
if (textLength > 1_000_000) return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
});
|
|
35
69
|
|
|
36
70
|
function getColName(index: number): string {
|
|
37
|
-
|
|
71
|
+
let result = '';
|
|
72
|
+
for (let i = index; i >= 0; i = Math.floor(i / 26) - 1) {
|
|
73
|
+
result = String.fromCharCode(i % 26 + 65) + result;
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
38
76
|
}
|
|
39
77
|
</script>
|
|
40
78
|
|
|
41
79
|
<div class="sv-lite-sheet-container {className}">
|
|
42
80
|
<div class="sv-lite-sheet-header">
|
|
43
|
-
<strong>
|
|
81
|
+
<strong>{i18n.t('spreadsheet.title')}: {currentSheet?.name ?? ''}</strong>
|
|
44
82
|
</div>
|
|
45
83
|
|
|
46
|
-
{#if currentSheet}
|
|
84
|
+
{#if !validWorkbook || (currentSheet && !canRender)}
|
|
85
|
+
<p role="alert">{i18n.t('spreadsheet.invalid')}</p>
|
|
86
|
+
{:else if !currentSheet}
|
|
87
|
+
<p role="status">{i18n.t('spreadsheet.empty')}</p>
|
|
88
|
+
{:else}
|
|
47
89
|
<form method="POST" action={formAction}>
|
|
90
|
+
<input type="hidden" name="sheetId" value={currentSheet.id} />
|
|
48
91
|
<table class="sv-lite-sheet-table">
|
|
49
92
|
<thead>
|
|
50
93
|
<tr>
|
|
@@ -64,8 +107,11 @@
|
|
|
64
107
|
<td class="sv-lite-sheet-td">
|
|
65
108
|
<input
|
|
66
109
|
type="text"
|
|
110
|
+
{readonly}
|
|
111
|
+
aria-label={key}
|
|
112
|
+
maxlength={limits.maxCellLength}
|
|
67
113
|
name={`cell_${key}`}
|
|
68
|
-
value={currentSheet.cells[key] ?? ''}
|
|
114
|
+
value={Object.hasOwn(currentSheet.cells, key) ? currentSheet.cells[key] ?? '' : ''}
|
|
69
115
|
class="sv-lite-cell-input"
|
|
70
116
|
/>
|
|
71
117
|
</td>
|
|
@@ -75,9 +121,9 @@
|
|
|
75
121
|
</tbody>
|
|
76
122
|
</table>
|
|
77
123
|
|
|
78
|
-
{#if formAction}
|
|
124
|
+
{#if formAction && !readonly}
|
|
79
125
|
<div class="sv-lite-sheet-footer">
|
|
80
|
-
<button type="submit" class="sv-lite-sheet-save">
|
|
126
|
+
<button type="submit" class="sv-lite-sheet-save">{i18n.t('common.save')}</button>
|
|
81
127
|
</div>
|
|
82
128
|
{/if}
|
|
83
129
|
</form>
|
|
@@ -9,6 +9,14 @@ interface Props {
|
|
|
9
9
|
sheets?: SheetData[];
|
|
10
10
|
activeSheetId?: string;
|
|
11
11
|
formAction?: string;
|
|
12
|
+
readonly?: boolean;
|
|
13
|
+
limits?: Partial<{
|
|
14
|
+
maxRows: number;
|
|
15
|
+
maxCols: number;
|
|
16
|
+
maxCells: number;
|
|
17
|
+
maxSheets: number;
|
|
18
|
+
maxCellLength: number;
|
|
19
|
+
}>;
|
|
12
20
|
class?: string;
|
|
13
21
|
}
|
|
14
22
|
declare const LiteSpreadsheetView: import("svelte").Component<Props, {}, "">;
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export { default as LiteLogin } from './components/LiteLogin.svelte';
|
|
|
14
14
|
export { default as LiteShow } from './components/LiteShow.svelte';
|
|
15
15
|
export { default as LiteSearch } from './components/LiteSearch.svelte';
|
|
16
16
|
export { default as LiteAlert } from './components/LiteAlert.svelte';
|
|
17
|
+
export { default as LiteErrorSummary } from './components/LiteErrorSummary.svelte';
|
|
18
|
+
export type { LiteFormErrorEntry } from './components/LiteErrorSummary.svelte';
|
|
17
19
|
export { default as LitePermissionMatrix } from './components/LitePermissionMatrix.svelte';
|
|
18
20
|
export { default as LiteAuditLog } from './components/LiteAuditLog.svelte';
|
|
19
21
|
export { default as LiteArrayField } from './components/LiteArrayField.svelte';
|
|
@@ -57,6 +59,8 @@ export { default as LiteActivityFeed } from './components/LiteActivityFeed.svelt
|
|
|
57
59
|
export { default as LitePresenceAvatarGroup } from './components/LitePresenceAvatarGroup.svelte';
|
|
58
60
|
export { default as LitePrintableBill } from './components/LitePrintableBill.svelte';
|
|
59
61
|
export { default as LiteJsonSchemaForm } from './components/LiteJsonSchemaForm.svelte';
|
|
62
|
+
export { decodeSchemaFormArrayAction, decodeSchemaFormData, decodeSchemaFormSubmission, encodeSchemaFormArrayAction, schemaFormActionName, schemaFormFieldName, } from './schema-form-data';
|
|
63
|
+
export type { SchemaFormArrayAction, SchemaFormSchema } from './schema-form-data';
|
|
60
64
|
export { default as LiteMentionsInput } from './components/LiteMentionsInput.svelte';
|
|
61
65
|
export { default as LiteKanbanBoard } from './components/LiteKanbanBoard.svelte';
|
|
62
66
|
export { default as LitePivotTable } from './components/LitePivotTable.svelte';
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ export { default as LiteLogin } from './components/LiteLogin.svelte';
|
|
|
18
18
|
export { default as LiteShow } from './components/LiteShow.svelte';
|
|
19
19
|
export { default as LiteSearch } from './components/LiteSearch.svelte';
|
|
20
20
|
export { default as LiteAlert } from './components/LiteAlert.svelte';
|
|
21
|
+
export { default as LiteErrorSummary } from './components/LiteErrorSummary.svelte';
|
|
21
22
|
export { default as LitePermissionMatrix } from './components/LitePermissionMatrix.svelte';
|
|
22
23
|
export { default as LiteAuditLog } from './components/LiteAuditLog.svelte';
|
|
23
24
|
export { default as LiteArrayField } from './components/LiteArrayField.svelte';
|
|
@@ -57,6 +58,7 @@ export { default as LiteActivityFeed } from './components/LiteActivityFeed.svelt
|
|
|
57
58
|
export { default as LitePresenceAvatarGroup } from './components/LitePresenceAvatarGroup.svelte';
|
|
58
59
|
export { default as LitePrintableBill } from './components/LitePrintableBill.svelte';
|
|
59
60
|
export { default as LiteJsonSchemaForm } from './components/LiteJsonSchemaForm.svelte';
|
|
61
|
+
export { decodeSchemaFormArrayAction, decodeSchemaFormData, decodeSchemaFormSubmission, encodeSchemaFormArrayAction, schemaFormActionName, schemaFormFieldName, } from './schema-form-data';
|
|
60
62
|
export { default as LiteMentionsInput } from './components/LiteMentionsInput.svelte';
|
|
61
63
|
export { default as LiteKanbanBoard } from './components/LiteKanbanBoard.svelte';
|
|
62
64
|
export { default as LitePivotTable } from './components/LitePivotTable.svelte';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SchemaFormSchema } from '@svadmin/core/schema-form';
|
|
2
|
+
export type { SchemaFormSchema } from '@svadmin/core/schema-form';
|
|
3
|
+
export declare function schemaFormFieldName(path: readonly string[]): string;
|
|
4
|
+
export declare const schemaFormActionName = "schemaFormAction";
|
|
5
|
+
export type SchemaFormArrayAction = {
|
|
6
|
+
type: 'add' | 'remove';
|
|
7
|
+
path: string[];
|
|
8
|
+
index?: number;
|
|
9
|
+
};
|
|
10
|
+
export declare function encodeSchemaFormArrayAction(action: SchemaFormArrayAction): string;
|
|
11
|
+
export declare function decodeSchemaFormArrayAction(form: FormData): SchemaFormArrayAction | undefined;
|
|
12
|
+
/** 按可信 Schema 解码并验证支持的约束;不替代资源授权和业务命令校验。 */
|
|
13
|
+
export declare function decodeSchemaFormData(schema: SchemaFormSchema, form: FormData): Record<string, unknown>;
|
|
14
|
+
/** 数组动作只生成下一份草稿;只有 submit 分支才可进入业务写入。 */
|
|
15
|
+
export declare function decodeSchemaFormSubmission(schema: SchemaFormSchema, form: FormData): {
|
|
16
|
+
kind: 'draft' | 'submit';
|
|
17
|
+
data: Record<string, unknown>;
|
|
18
|
+
};
|