openxiangda 1.0.147 → 1.0.149
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 +10 -0
- package/lib/cli.js +191 -3
- package/openxiangda-skills/SKILL.md +2 -0
- package/openxiangda-skills/references/pages/page-sdk.md +19 -0
- package/openxiangda-skills/skills/openxiangda-core/SKILL.md +3 -0
- package/openxiangda-skills/skills/openxiangda-form/SKILL.md +24 -0
- package/package.json +2 -1
- package/packages/sdk/dist/{ProcessPreview-BoblxCUt.d.mts → ProcessPreview-BOCARAvP.d.mts} +9 -1
- package/packages/sdk/dist/{ProcessPreview-BoblxCUt.d.ts → ProcessPreview-BOCARAvP.d.ts} +9 -1
- package/packages/sdk/dist/components/index.cjs +33 -21
- package/packages/sdk/dist/components/index.cjs.map +1 -1
- package/packages/sdk/dist/components/index.d.mts +4 -4
- package/packages/sdk/dist/components/index.d.ts +4 -4
- package/packages/sdk/dist/components/index.mjs +33 -21
- package/packages/sdk/dist/components/index.mjs.map +1 -1
- package/packages/sdk/dist/{dataManagementApi-BCzfV88G.d.mts → dataManagementApi-CLMqf79O.d.mts} +1 -1
- package/packages/sdk/dist/{dataManagementApi-_FFNPv2e.d.ts → dataManagementApi-DhpRKmlp.d.ts} +1 -1
- package/packages/sdk/dist/runtime/index.cjs +375 -30
- package/packages/sdk/dist/runtime/index.cjs.map +1 -1
- package/packages/sdk/dist/runtime/index.d.mts +3 -3
- package/packages/sdk/dist/runtime/index.d.ts +3 -3
- package/packages/sdk/dist/runtime/index.mjs +377 -32
- package/packages/sdk/dist/runtime/index.mjs.map +1 -1
- package/packages/sdk/dist/runtime/react.cjs +12 -0
- package/packages/sdk/dist/runtime/react.cjs.map +1 -1
- package/packages/sdk/dist/runtime/react.d.mts +24 -2
- package/packages/sdk/dist/runtime/react.d.ts +24 -2
- package/packages/sdk/dist/runtime/react.mjs +12 -0
- package/packages/sdk/dist/runtime/react.mjs.map +1 -1
- package/templates/openxiangda-react-spa/AGENTS.md +8 -0
- package/templates/openxiangda-react-spa/src/pages/defaults/FilePreviewRoutePage.tsx +18 -33
- package/templates/sy-lowcode-app-workspace/AGENTS.md +1 -0
package/README.md
CHANGED
|
@@ -216,6 +216,16 @@ openxiangda workspace bind --profile dev --app-type APP_XXXX
|
|
|
216
216
|
|
|
217
217
|
表单页、流程表单页和自定义代码页都应在 `sy-lowcode-app-workspace` 中实现,由 `openxiangda workspace publish --profile <name>` 统一构建、上传 OSS 并注册到平台。`openxiangda form create`、`form publish`、`page publish` 只作为底层修复/诊断命令,不作为 AI 生成页面的主入口。
|
|
218
218
|
|
|
219
|
+
表单数据导出使用 `openxiangda form export`,不要手写下载接口 URL:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
openxiangda form export customer --mode xlsx --profile dev
|
|
223
|
+
openxiangda form export customer --mode xlsx-images --profile dev --all
|
|
224
|
+
openxiangda form export customer --mode package --profile dev --output ./exports/
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
`xlsx` 是普通 Excel;`xlsx-images` 会尽量把 `ImageField` 图片嵌入单元格;`package` 会下载 zip,包含 `data.xlsx`、`attachments/` 和 `manifest.json`。`--output` 指向目录时使用服务端文件名,指向文件路径时按该路径写入;`--json` 只输出 `{ file, filename, mime, bytes, mode, formUuid }` 摘要,不把二进制写到 stdout。
|
|
228
|
+
|
|
219
229
|
运行时页面读取当前用户信息时,优先使用 `sdk.user.getCurrent<PageUserRecord>()`。用户对象会返回常规组织成员关系 `departments`,也会返回系统维护的所属单位字段 `affiliatedDepartmentId` / `affiliatedDepartment`。`departments` 表示用户真实所在的部门、班级、专业等成员关系;`affiliatedDepartment` 表示业务上用于统计、筛选和展示的归属单位,通常是学院、单位或在源单位缺失时可用的具体部门节点,不用于替代权限部门成员关系。
|
|
220
230
|
|
|
221
231
|
```ts
|
package/lib/cli.js
CHANGED
|
@@ -141,6 +141,7 @@ Usage:
|
|
|
141
141
|
openxiangda form create <formCode> [--name text] [--type receipt] # low-level shell only
|
|
142
142
|
openxiangda form bind <formCode> --form-uuid <FORM_XXX>
|
|
143
143
|
openxiangda form pull <formCode|formUuid> [--json]
|
|
144
|
+
openxiangda form export <formCode|formUuid> --mode xlsx|xlsx-images|package [--output <file-or-dir>]
|
|
144
145
|
openxiangda form publish <formCode|formUuid> --bundle-url <url> # repair only
|
|
145
146
|
openxiangda form schema-plan <formCode|formUuid> --schema-json <file>|--components-json <file>
|
|
146
147
|
openxiangda page list [--profile name] [--json]
|
|
@@ -2077,7 +2078,7 @@ async function form(args) {
|
|
|
2077
2078
|
const [subcommand, ...rest] = args;
|
|
2078
2079
|
const { flags, positional } = parseArgs(rest);
|
|
2079
2080
|
if (wantsSubcommandHelp(subcommand, flags)) {
|
|
2080
|
-
print('用法: openxiangda form list|create|bind|pull|publish|schema-plan [--profile name] [--json]');
|
|
2081
|
+
print('用法: openxiangda form list|create|bind|pull|export|publish|schema-plan [--profile name] [--json]');
|
|
2081
2082
|
return;
|
|
2082
2083
|
}
|
|
2083
2084
|
const config = loadConfig();
|
|
@@ -2155,6 +2156,46 @@ async function form(args) {
|
|
|
2155
2156
|
return;
|
|
2156
2157
|
}
|
|
2157
2158
|
|
|
2159
|
+
if (subcommand === 'export') {
|
|
2160
|
+
const [formKey] = positional;
|
|
2161
|
+
if (!formKey) {
|
|
2162
|
+
fail('用法: openxiangda form export <formCode|formUuid> --mode xlsx|xlsx-images|package');
|
|
2163
|
+
}
|
|
2164
|
+
const target = getWorkspaceTarget(config, profileName, flags);
|
|
2165
|
+
const formUuid = resolveFormUuid(target.bound, formKey, flags);
|
|
2166
|
+
const mode = normalizeFormExportMode(flags.mode);
|
|
2167
|
+
const extension = mode === 'package' ? 'zip' : 'xlsx';
|
|
2168
|
+
const endpoint =
|
|
2169
|
+
mode === 'package' ? 'advancedExportPackage.zip' : 'advancedExport.xlsx';
|
|
2170
|
+
const apiPath = apiPathWithQuery(
|
|
2171
|
+
`/${encodeURIComponent(target.appType)}/v1/form/${endpoint}`,
|
|
2172
|
+
buildFormExportQuery(formUuid, mode, flags)
|
|
2173
|
+
);
|
|
2174
|
+
const download = await downloadWithAuth(config, target.profileName, apiPath, {
|
|
2175
|
+
timeoutMs: flags['timeout-ms'] || flags.timeout || 120000,
|
|
2176
|
+
});
|
|
2177
|
+
const fallbackFilename = `export_${formUuid}_${new Date()
|
|
2178
|
+
.toISOString()
|
|
2179
|
+
.replace(/[-:T.Z]/g, '')
|
|
2180
|
+
.slice(0, 14)}.${extension}`;
|
|
2181
|
+
const output = writeDownloadedFile(
|
|
2182
|
+
download,
|
|
2183
|
+
flags.output || '.',
|
|
2184
|
+
fallbackFilename
|
|
2185
|
+
);
|
|
2186
|
+
const result = {
|
|
2187
|
+
file: output.file,
|
|
2188
|
+
filename: output.filename,
|
|
2189
|
+
mime: download.mime,
|
|
2190
|
+
bytes: download.bytes,
|
|
2191
|
+
mode,
|
|
2192
|
+
formUuid,
|
|
2193
|
+
};
|
|
2194
|
+
if (flags.json) return writeJson(result);
|
|
2195
|
+
print(`已导出 ${mode}: ${output.file} (${download.bytes} bytes)`);
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2158
2199
|
if (subcommand === 'publish') {
|
|
2159
2200
|
const [formKey] = positional;
|
|
2160
2201
|
if (!formKey || !flags['bundle-url']) {
|
|
@@ -2205,7 +2246,7 @@ async function form(args) {
|
|
|
2205
2246
|
return;
|
|
2206
2247
|
}
|
|
2207
2248
|
|
|
2208
|
-
fail('用法: openxiangda form list|create|bind|pull|publish|schema-plan');
|
|
2249
|
+
fail('用法: openxiangda form list|create|bind|pull|export|publish|schema-plan');
|
|
2209
2250
|
}
|
|
2210
2251
|
|
|
2211
2252
|
async function page(args) {
|
|
@@ -5379,7 +5420,7 @@ async function commands(args) {
|
|
|
5379
5420
|
'env',
|
|
5380
5421
|
'workspace init|bind|plan|check|publish [--app-name] [--changed|--since|--form|--page|--only|--dry-run|--force|--resources|--skip-resources|--prune]',
|
|
5381
5422
|
'app list|create|snapshot',
|
|
5382
|
-
'form list|create|bind|pull|publish|schema-plan',
|
|
5423
|
+
'form list|create|bind|pull|export|publish|schema-plan',
|
|
5383
5424
|
'page list|publish|bind|releases|activate',
|
|
5384
5425
|
'menu list|create|update|sort|bind|delete',
|
|
5385
5426
|
'workflow compile|list|create|bind|pull|publish|delete|validate',
|
|
@@ -6079,6 +6120,61 @@ function resolveFormUuid(bound, formKey, flags = {}) {
|
|
|
6079
6120
|
return mapped || formKey;
|
|
6080
6121
|
}
|
|
6081
6122
|
|
|
6123
|
+
function normalizeFormExportMode(value) {
|
|
6124
|
+
const mode = String(value || 'xlsx').trim();
|
|
6125
|
+
if (['xlsx', 'xlsx-images', 'package'].includes(mode)) return mode;
|
|
6126
|
+
fail('--mode 只能是 xlsx、xlsx-images 或 package');
|
|
6127
|
+
}
|
|
6128
|
+
|
|
6129
|
+
function buildFormExportQuery(formUuid, mode, flags = {}) {
|
|
6130
|
+
const query = {
|
|
6131
|
+
formUuid,
|
|
6132
|
+
exportAll: flags.all ? 'y' : 'n',
|
|
6133
|
+
embedImages: mode === 'xlsx-images' || mode === 'package' ? 'y' : 'n',
|
|
6134
|
+
currentPage: flags.page,
|
|
6135
|
+
pageSize: flags['page-size'],
|
|
6136
|
+
conditionType: flags['condition-type'],
|
|
6137
|
+
searchKeyWord: flags['search-keyword'],
|
|
6138
|
+
instanceStatus: flags['instance-status'],
|
|
6139
|
+
exportFields: flags.fields,
|
|
6140
|
+
};
|
|
6141
|
+
if (flags['filters-json']) {
|
|
6142
|
+
query.filters = JSON.stringify(readJsonArg(flags['filters-json'], 'filters-json'));
|
|
6143
|
+
}
|
|
6144
|
+
if (flags['order-json']) {
|
|
6145
|
+
query.order = JSON.stringify(readJsonArg(flags['order-json'], 'order-json'));
|
|
6146
|
+
}
|
|
6147
|
+
return query;
|
|
6148
|
+
}
|
|
6149
|
+
|
|
6150
|
+
function writeDownloadedFile(download, outputFlag, fallbackFilename) {
|
|
6151
|
+
const filename = sanitizeDownloadFilename(download.filename || fallbackFilename);
|
|
6152
|
+
const target = String(outputFlag || '.');
|
|
6153
|
+
const absoluteTarget = path.resolve(process.cwd(), target);
|
|
6154
|
+
const targetIsDirectory =
|
|
6155
|
+
target.endsWith('/') ||
|
|
6156
|
+
target.endsWith(path.sep) ||
|
|
6157
|
+
target === '.' ||
|
|
6158
|
+
(fs.existsSync(absoluteTarget) && fs.statSync(absoluteTarget).isDirectory()) ||
|
|
6159
|
+
(!fs.existsSync(absoluteTarget) && !path.extname(absoluteTarget));
|
|
6160
|
+
const file = targetIsDirectory
|
|
6161
|
+
? path.join(absoluteTarget, filename)
|
|
6162
|
+
: absoluteTarget;
|
|
6163
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
6164
|
+
fs.writeFileSync(file, download.buffer);
|
|
6165
|
+
return {
|
|
6166
|
+
file,
|
|
6167
|
+
filename: path.basename(file),
|
|
6168
|
+
};
|
|
6169
|
+
}
|
|
6170
|
+
|
|
6171
|
+
function sanitizeDownloadFilename(value) {
|
|
6172
|
+
return String(value || 'download.bin')
|
|
6173
|
+
.replace(/[\\/]+/g, '_')
|
|
6174
|
+
.replace(/[\0\r\n]+/g, '')
|
|
6175
|
+
.trim() || 'download.bin';
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6082
6178
|
function readFormSchemaPlanBody(flags = {}) {
|
|
6083
6179
|
if (flags['schema-json']) {
|
|
6084
6180
|
return { schema: readJsonArg(flags['schema-json'], 'schema-json') };
|
|
@@ -12994,6 +13090,98 @@ async function retryTransientFetch(fn, options = {}) {
|
|
|
12994
13090
|
throw lastError;
|
|
12995
13091
|
}
|
|
12996
13092
|
|
|
13093
|
+
async function downloadWithAuth(config, profileName, apiPath, options = {}) {
|
|
13094
|
+
const resolved = getProfile(config, profileName);
|
|
13095
|
+
const profile = resolved.profile;
|
|
13096
|
+
if (!profile.token?.accessToken) {
|
|
13097
|
+
fail(`profile ${resolved.profileName} 未登录,请先执行 openxiangda login --profile ${resolved.profileName}`);
|
|
13098
|
+
}
|
|
13099
|
+
const requestOptions = {
|
|
13100
|
+
method: 'GET',
|
|
13101
|
+
timeoutMs: options.timeoutMs,
|
|
13102
|
+
retries: options.retries,
|
|
13103
|
+
};
|
|
13104
|
+
const send = () =>
|
|
13105
|
+
downloadBinary(profile.baseUrl, apiPath, profile.token.accessToken, requestOptions);
|
|
13106
|
+
try {
|
|
13107
|
+
return await retryTransientFetch(send, requestOptions);
|
|
13108
|
+
} catch (error) {
|
|
13109
|
+
if (!isUnauthorized(error) || !profile.token?.refreshToken) {
|
|
13110
|
+
throw error;
|
|
13111
|
+
}
|
|
13112
|
+
await refreshProfile(config, resolved.profileName);
|
|
13113
|
+
return await retryTransientFetch(send, requestOptions);
|
|
13114
|
+
}
|
|
13115
|
+
}
|
|
13116
|
+
|
|
13117
|
+
async function downloadBinary(baseUrl, apiPath, accessToken, options = {}) {
|
|
13118
|
+
if (typeof fetch !== 'function') {
|
|
13119
|
+
throw new Error('当前 Node.js 版本不支持 fetch,请使用 Node.js 18 或更高版本');
|
|
13120
|
+
}
|
|
13121
|
+
const url = `${baseUrl.replace(/\/+$/, '')}${apiPath}`;
|
|
13122
|
+
const timeoutMs = normalizeRuntimeUploadTimeoutMs(options.timeoutMs);
|
|
13123
|
+
const controller = new AbortController();
|
|
13124
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
13125
|
+
let response;
|
|
13126
|
+
try {
|
|
13127
|
+
response = await fetch(url, {
|
|
13128
|
+
method: options.method || 'GET',
|
|
13129
|
+
headers: {
|
|
13130
|
+
accept: '*/*',
|
|
13131
|
+
authorization: `Bearer ${accessToken}`,
|
|
13132
|
+
},
|
|
13133
|
+
signal: controller.signal,
|
|
13134
|
+
});
|
|
13135
|
+
} catch (error) {
|
|
13136
|
+
if (isAbortError(error)) {
|
|
13137
|
+
throw new Error(maskText(`HTTP download timed out after ${timeoutMs}ms: ${apiPath}`));
|
|
13138
|
+
}
|
|
13139
|
+
throw new Error(formatFetchError(error, apiPath));
|
|
13140
|
+
} finally {
|
|
13141
|
+
clearTimeout(timer);
|
|
13142
|
+
}
|
|
13143
|
+
|
|
13144
|
+
if (!response.ok) {
|
|
13145
|
+
const text = await response.text().catch(() => '');
|
|
13146
|
+
let payload = null;
|
|
13147
|
+
try {
|
|
13148
|
+
payload = text ? JSON.parse(text) : null;
|
|
13149
|
+
} catch {
|
|
13150
|
+
payload = { message: text };
|
|
13151
|
+
}
|
|
13152
|
+
const message = payload?.message || response.statusText || 'download failed';
|
|
13153
|
+
const error = new Error(maskText(`HTTP ${response.status}: ${message}`));
|
|
13154
|
+
error.status = response.status;
|
|
13155
|
+
error.payload = payload;
|
|
13156
|
+
throw error;
|
|
13157
|
+
}
|
|
13158
|
+
|
|
13159
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
13160
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
13161
|
+
const disposition = response.headers.get('content-disposition') || '';
|
|
13162
|
+
return {
|
|
13163
|
+
buffer,
|
|
13164
|
+
filename: parseContentDispositionFilename(disposition),
|
|
13165
|
+
mime: response.headers.get('content-type') || 'application/octet-stream',
|
|
13166
|
+
bytes: buffer.length,
|
|
13167
|
+
};
|
|
13168
|
+
}
|
|
13169
|
+
|
|
13170
|
+
function parseContentDispositionFilename(header) {
|
|
13171
|
+
const value = String(header || '');
|
|
13172
|
+
const encodedMatch = value.match(/filename\*=([^;]+)/i);
|
|
13173
|
+
if (encodedMatch) {
|
|
13174
|
+
const encoded = encodedMatch[1].trim().replace(/^UTF-8''/i, '');
|
|
13175
|
+
try {
|
|
13176
|
+
return sanitizeDownloadFilename(decodeURIComponent(encoded.replace(/^"|"$/g, '')));
|
|
13177
|
+
} catch {
|
|
13178
|
+
return sanitizeDownloadFilename(encoded);
|
|
13179
|
+
}
|
|
13180
|
+
}
|
|
13181
|
+
const plainMatch = value.match(/filename="?([^";]+)"?/i);
|
|
13182
|
+
return plainMatch ? sanitizeDownloadFilename(plainMatch[1]) : '';
|
|
13183
|
+
}
|
|
13184
|
+
|
|
12997
13185
|
async function requestWithAuth(config, profileName, apiPath, options = {}) {
|
|
12998
13186
|
const resolved = getProfile(config, profileName);
|
|
12999
13187
|
const profile = resolved.profile;
|
|
@@ -24,6 +24,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
24
24
|
| 创建应用 / 新建 app / scaffold / 初始化工作区 | `openxiangda-app` | `openxiangda workspace init <dir> --profile <name> --app-name "..."` |
|
|
25
25
|
| 绑定已有应用 / bind existing app | `openxiangda-app` | `openxiangda workspace bind --profile <name> --app-type APP_XXX` |
|
|
26
26
|
| 创建 / 修改表单字段 / 表单页 / schema | `openxiangda-form` | edit `src/forms/<code>/{schema.ts,page.tsx}` → `workspace publish --form <code>` |
|
|
27
|
+
| 导出表单数据 / 图片 / 附件 / Excel / zip | `openxiangda-form` | `openxiangda form export <formCode> --mode xlsx|xlsx-images|package --profile <name>` |
|
|
27
28
|
| 创建 / 修改自定义代码页 / portal / dashboard | `openxiangda-page` | edit `src/pages/<code>/` → `workspace publish --page <code>` |
|
|
28
29
|
| 审批流程 / workflow / 流程节点 / JS_CODE | `openxiangda-workflow-automation` | `openxiangda workflow validate / create / publish` |
|
|
29
30
|
| 自动化 / 定时任务 / 提交触发 / cron | `openxiangda-workflow-automation` | `openxiangda automation validate / create / publish / enable` |
|
|
@@ -57,6 +58,7 @@ OpenXiangda supports two workspace modes. Classic `sy-lowcode-app-workspace` pub
|
|
|
57
58
|
- ✅ For form-entry UX, pick components in this order: OpenXiangda platform form components → `antd` / `antd-mobile` wrappers → custom component only when neither exists.
|
|
58
59
|
- ✅ In React SPA apps, keep imports on public package entrypoints such as `openxiangda`, `openxiangda/runtime`, and `openxiangda/runtime/react`. The default template already groups large vendor chunks; if a bundle is too large, use route-level lazy loading and Vite `manualChunks`, not `openxiangda/packages/sdk/dist/...` internal paths.
|
|
59
60
|
- ✅ For image thumbnails or lightweight previews, use `imageCompression` on `ImageField` or `AttachmentField`. Keep the original `url` for the source file and use `thumbUrl` / `previewUrl` / `variants` for smaller images.
|
|
61
|
+
- ✅ For data delivery with files, use `openxiangda form export <formCode> --mode xlsx|xlsx-images|package --profile <name>`. `package` writes a zip with `data.xlsx`, `attachments/`, and `manifest.json`; do not hand-write export URLs or stream binary content to stdout.
|
|
60
62
|
- ✅ List pages, linked options, remote selectors, and report drill-downs must use paginated server-side queries with explicit searchable fields. Do not fetch a large page and filter in local state.
|
|
61
63
|
- ✅ Treat workflow forms as an exception. Ordinary ticket/order/task/asset lifecycles use status fields, state machines, action logs, permissions, and automations; workflows are for real approval tasks.
|
|
62
64
|
- ✅ Proactively submit platform feedback when you discover a defect, missing capability, unclear rule, repeated workaround, user-visible UX gap, or implementation uncertainty worth improving.
|
|
@@ -16,6 +16,7 @@ Guidelines:
|
|
|
16
16
|
- Use `sdk.function.invoke(code, { input })` for reusable backend business logic declared under `src/resources/functions/` and `src/functions/`. Do not implement multi-form orchestration, connector fan-out, notification orchestration, or permission-sensitive backend rules directly in a page component.
|
|
17
17
|
- Use `sdk.connector.invoke`, `sdk.connector.call("connector.api")`, or `sdk.connector.download` for external services. The SDK calls the platform runtime connector endpoint; it must not call third-party domains directly.
|
|
18
18
|
- Use `sdk.notification.sendByType` and `batchSendByType` for reusable business messages. Custom notification types must be declared in `src/resources/notifications/` and published with `openxiangda resource publish`.
|
|
19
|
+
- Use `sdk.createFileAccessTicket(bucketName, objectName, fileName, "preview")` when a custom React SPA page needs a user-openable attachment preview link. Open `response.result.previewPageUrl`; do not use `previewUrl` or `/service/file/preview-by-ticket/:ticket` as the page entry.
|
|
19
20
|
- Use `sdk.organization.departments.*` and `sdk.organization.accounts.*` only for admin-style app pages that intentionally manage platform departments/accounts. The current user needs `app:organization:manage` on the app. Do not call legacy `/user` or `/department` write endpoints from pages.
|
|
20
21
|
- Use `sdk.workCenter.listItems({ boxType })` and `sdk.workCenter.getStats()` for current-user work center lists and counts. `boxType` is one of `todo`, `done`, `cc`, or `initiated`. This is the end-user task/handled/cc/initiated surface; do not build todo pages by reading workflow operation logs, automation logs, or raw process-task tables.
|
|
21
22
|
- `sdk.form.create` returns identifiers and generated serial number values only. Treat `formInstId` / `formInstanceId` as the required success contract; `serialNumber` / `serialNumbers` are present only when the form has `SerialNumberField`. Do not expect the save response to contain the full row, formula results, or other server-generated field values. Call `sdk.form.getDetail` explicitly after create when those values are needed.
|
|
@@ -100,6 +101,24 @@ const response = await sdk.dataSource.run("tickets", {
|
|
|
100
101
|
|
|
101
102
|
Data view filters, having, and fields use output aliases such as `customerName` or `ticketCount`, not source references such as `customer.name`. For option-like fields, prefer scalar aliases such as `statusValue` in the view definition and page filters. If the page only needs one form, prefer `sdk.form.advancedSearch`. If the page only needs a simple one-form dropdown, prefer linkedForm options.
|
|
102
103
|
|
|
104
|
+
File preview ticket:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
const ticketResponse = await sdk.createFileAccessTicket(
|
|
108
|
+
"lowcode",
|
|
109
|
+
"attachments/contracts/demo.docx",
|
|
110
|
+
"demo.docx",
|
|
111
|
+
"preview",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const previewPageUrl = ticketResponse.result?.previewPageUrl
|
|
115
|
+
if (previewPageUrl) {
|
|
116
|
+
window.open(previewPageUrl, "_blank")
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`previewPageUrl` is the final browser page URL, normally `/view/:appType/file-preview?ticket=...`. `previewUrl` is the file content stream for the preview renderer and should not be used as the user-facing page entry.
|
|
121
|
+
|
|
103
122
|
App Function invocation:
|
|
104
123
|
|
|
105
124
|
```ts
|
|
@@ -188,6 +188,9 @@ Read-only diagnosis can use resource commands:
|
|
|
188
188
|
```bash
|
|
189
189
|
openxiangda form list --profile dev
|
|
190
190
|
openxiangda form pull customer --profile dev
|
|
191
|
+
openxiangda form export customer --mode xlsx --profile dev
|
|
192
|
+
openxiangda form export customer --mode xlsx-images --profile dev --all
|
|
193
|
+
openxiangda form export customer --mode package --profile dev --output ./exports/
|
|
191
194
|
openxiangda page list --profile dev
|
|
192
195
|
openxiangda workflow list --form-code customer --profile dev
|
|
193
196
|
openxiangda automation list --form-code customer --profile dev
|
|
@@ -106,6 +106,30 @@ openxiangda form publish customer --bundle-url <url> --profile <name>
|
|
|
106
106
|
|
|
107
107
|
Do not use `openxiangda form create` as the normal way to generate a user-facing page. It creates the old platform form shell and will show the legacy default schema until a workspace bundle is published.
|
|
108
108
|
|
|
109
|
+
## Data Export
|
|
110
|
+
|
|
111
|
+
Use `openxiangda form export` for read-only data delivery. It reuses the active profile token and the workspace app binding, so do not hand-write export URLs or ask for AK/SK.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
openxiangda form export customer --mode xlsx --profile dev
|
|
115
|
+
openxiangda form export customer --mode xlsx-images --profile dev --all
|
|
116
|
+
openxiangda form export customer --mode package --profile dev --output ./exports/
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Modes:
|
|
120
|
+
|
|
121
|
+
- `xlsx`: normal Excel export. Images and attachments stay as links/text.
|
|
122
|
+
- `xlsx-images`: Excel export with `ImageField` images embedded when the server can read a supported `png` / `jpg` / `gif`; failures degrade to links.
|
|
123
|
+
- `package`: zip export containing `data.xlsx`, `attachments/`, and `manifest.json`; attachment/image cells use package-relative paths and the `Attachments` sheet lists one file per row.
|
|
124
|
+
|
|
125
|
+
Useful filters:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
openxiangda form export customer --mode package --profile dev --all --fields formInstId,instance_title,field_xxx
|
|
129
|
+
openxiangda form export customer --mode xlsx --profile dev --filters-json ./filters.json --order-json ./order.json
|
|
130
|
+
openxiangda form export customer --mode xlsx --profile dev --page 1 --page-size 100 --condition-type and --search-keyword 张三 --instance-status completed
|
|
131
|
+
```
|
|
132
|
+
|
|
109
133
|
## Workspace Form Rules
|
|
110
134
|
|
|
111
135
|
Read these references only when writing or reviewing schema:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openxiangda",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.149",
|
|
4
4
|
"description": "OpenXiangda CLI, workspace build tools, runtime SDK, and form components.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"bin": {
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
"scripts": {
|
|
63
63
|
"build:sdk": "tsup --config packages/sdk/tsup.config.ts",
|
|
64
64
|
"check": "node --check bin/openxiangda.js && node --check lib/*.js && node --check packages/sdk/src/build-source/src/cli.mjs && node --check packages/sdk/src/build-source/scripts/*.mjs && node --check packages/sdk/src/build-source/scripts/utils/*.mjs",
|
|
65
|
+
"test": "npm run check && node scripts/form-export-cli-smoke.mjs",
|
|
65
66
|
"prepack": "npm run build:sdk",
|
|
66
67
|
"test:profile-isolation": "bash scripts/profile-isolation-smoke.sh",
|
|
67
68
|
"test:resource-plan": "node scripts/resource-plan-smoke.mjs",
|
|
@@ -269,7 +269,15 @@ interface FormRuntimeApi {
|
|
|
269
269
|
success: boolean;
|
|
270
270
|
}>;
|
|
271
271
|
createDownloadTicket: (bucketName: string, objectName: string, fileName?: string) => Promise<any>;
|
|
272
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Creates a protected file access ticket.
|
|
274
|
+
*
|
|
275
|
+
* previewPageUrl is the page URL that can be opened directly.
|
|
276
|
+
* previewUrl is the raw file content stream URL for iframe/img/PDF viewers.
|
|
277
|
+
*/
|
|
278
|
+
createFileAccessTicket: (bucketName: string, objectName: string, fileName?: string, purpose?: 'download' | 'preview' | 'onlyoffice', options?: {
|
|
279
|
+
appType?: string;
|
|
280
|
+
}) => Promise<any>;
|
|
273
281
|
getUserById: (id: string) => Promise<any>;
|
|
274
282
|
getUserList: (params?: Record<string, any>) => Promise<any[]>;
|
|
275
283
|
getDepartmentRoots: () => Promise<any[]>;
|
|
@@ -269,7 +269,15 @@ interface FormRuntimeApi {
|
|
|
269
269
|
success: boolean;
|
|
270
270
|
}>;
|
|
271
271
|
createDownloadTicket: (bucketName: string, objectName: string, fileName?: string) => Promise<any>;
|
|
272
|
-
|
|
272
|
+
/**
|
|
273
|
+
* Creates a protected file access ticket.
|
|
274
|
+
*
|
|
275
|
+
* previewPageUrl is the page URL that can be opened directly.
|
|
276
|
+
* previewUrl is the raw file content stream URL for iframe/img/PDF viewers.
|
|
277
|
+
*/
|
|
278
|
+
createFileAccessTicket: (bucketName: string, objectName: string, fileName?: string, purpose?: 'download' | 'preview' | 'onlyoffice', options?: {
|
|
279
|
+
appType?: string;
|
|
280
|
+
}) => Promise<any>;
|
|
273
281
|
getUserById: (id: string) => Promise<any>;
|
|
274
282
|
getUserList: (params?: Record<string, any>) => Promise<any[]>;
|
|
275
283
|
getDepartmentRoots: () => Promise<any[]>;
|
|
@@ -3317,6 +3317,11 @@ function canPreview(item) {
|
|
|
3317
3317
|
const ext = getFileExtension(item.name);
|
|
3318
3318
|
return isImageFile(item.name, item.contentType) || ["mp4", "webm", "ogg", "mov", "m4v"].includes(ext) || PREVIEW_PAGE_EXTENSIONS.includes(ext);
|
|
3319
3319
|
}
|
|
3320
|
+
function appendTicketIfMissing(url, ticket) {
|
|
3321
|
+
if (!url || !ticket || /[?&]ticket=/.test(url)) return url;
|
|
3322
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
3323
|
+
return `${url}${separator}ticket=${encodeURIComponent(ticket)}`;
|
|
3324
|
+
}
|
|
3320
3325
|
function AttachmentFieldPC({
|
|
3321
3326
|
fieldId,
|
|
3322
3327
|
behavior,
|
|
@@ -3432,16 +3437,15 @@ function AttachmentFieldPC({
|
|
|
3432
3437
|
item.bucketName || bucketName,
|
|
3433
3438
|
item.objectName,
|
|
3434
3439
|
item.name,
|
|
3435
|
-
"preview"
|
|
3440
|
+
"preview",
|
|
3441
|
+
{ appType: item.appType || config.appType }
|
|
3436
3442
|
);
|
|
3437
3443
|
const ticketValue = typeof ticket === "string" ? { previewUrl: ticket } : ticket;
|
|
3438
|
-
if (ticketValue?.previewPageUrl
|
|
3439
|
-
|
|
3440
|
-
return `${ticketValue.previewPageUrl}${separator}ticket=${encodeURIComponent(ticketValue.ticket)}`;
|
|
3444
|
+
if (ticketValue?.previewPageUrl) {
|
|
3445
|
+
return appendTicketIfMissing(ticketValue.previewPageUrl, ticketValue.ticket);
|
|
3441
3446
|
}
|
|
3442
3447
|
if (ticketValue?.ticket && PREVIEW_PAGE_EXTENSIONS.includes(getFileExtension(item.name))) {
|
|
3443
|
-
|
|
3444
|
-
return `${previewPagePath}${separator}ticket=${encodeURIComponent(ticketValue.ticket)}`;
|
|
3448
|
+
return appendTicketIfMissing(previewPagePath, ticketValue.ticket);
|
|
3445
3449
|
}
|
|
3446
3450
|
return ticketValue?.previewUrl || ticketValue?.url || "";
|
|
3447
3451
|
}
|
|
@@ -3683,6 +3687,11 @@ function canPreview2(item) {
|
|
|
3683
3687
|
const ext = getFileExtension(item.name);
|
|
3684
3688
|
return isImageFile(item.name, item.contentType) || ["mp4", "webm", "ogg", "mov", "m4v"].includes(ext) || PREVIEW_PAGE_EXTENSIONS2.includes(ext);
|
|
3685
3689
|
}
|
|
3690
|
+
function appendTicketIfMissing2(url, ticket) {
|
|
3691
|
+
if (!url || !ticket || /[?&]ticket=/.test(url)) return url;
|
|
3692
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
3693
|
+
return `${url}${separator}ticket=${encodeURIComponent(ticket)}`;
|
|
3694
|
+
}
|
|
3686
3695
|
function AttachmentFieldMobile({
|
|
3687
3696
|
fieldId,
|
|
3688
3697
|
behavior,
|
|
@@ -3831,16 +3840,15 @@ function AttachmentFieldMobile({
|
|
|
3831
3840
|
item.bucketName || bucketName,
|
|
3832
3841
|
item.objectName,
|
|
3833
3842
|
item.name,
|
|
3834
|
-
"preview"
|
|
3843
|
+
"preview",
|
|
3844
|
+
{ appType: item.appType || config.appType }
|
|
3835
3845
|
);
|
|
3836
3846
|
const ticketValue = typeof ticket === "string" ? { previewUrl: ticket } : ticket;
|
|
3837
|
-
if (ticketValue?.previewPageUrl
|
|
3838
|
-
|
|
3839
|
-
return `${ticketValue.previewPageUrl}${separator}ticket=${encodeURIComponent(ticketValue.ticket)}`;
|
|
3847
|
+
if (ticketValue?.previewPageUrl) {
|
|
3848
|
+
return appendTicketIfMissing2(ticketValue.previewPageUrl, ticketValue.ticket);
|
|
3840
3849
|
}
|
|
3841
3850
|
if (ticketValue?.ticket && PREVIEW_PAGE_EXTENSIONS2.includes(getFileExtension(item.name))) {
|
|
3842
|
-
|
|
3843
|
-
return `${previewPagePath}${separator}ticket=${encodeURIComponent(ticketValue.ticket)}`;
|
|
3851
|
+
return appendTicketIfMissing2(previewPagePath, ticketValue.ticket);
|
|
3844
3852
|
}
|
|
3845
3853
|
return ticketValue?.previewUrl || ticketValue?.url || "";
|
|
3846
3854
|
}
|
|
@@ -3971,6 +3979,11 @@ function canPreview3(item) {
|
|
|
3971
3979
|
const ext = getFileExtension(item.name);
|
|
3972
3980
|
return isImageFile(item.name, item.contentType) || ["mp4", "webm", "ogg", "mov", "m4v"].includes(ext) || PREVIEW_PAGE_EXTENSIONS3.includes(ext);
|
|
3973
3981
|
}
|
|
3982
|
+
function appendTicketIfMissing3(url, ticket) {
|
|
3983
|
+
if (!url || !ticket || /[?&]ticket=/.test(url)) return url;
|
|
3984
|
+
const separator = url.includes("?") ? "&" : "?";
|
|
3985
|
+
return `${url}${separator}ticket=${encodeURIComponent(ticket)}`;
|
|
3986
|
+
}
|
|
3974
3987
|
function AttachmentFieldReadonly({
|
|
3975
3988
|
fieldId,
|
|
3976
3989
|
readonlyClassName,
|
|
@@ -3981,7 +3994,7 @@ function AttachmentFieldReadonly({
|
|
|
3981
3994
|
showFileTypeBadge = false,
|
|
3982
3995
|
previewPagePath = "/file-preview"
|
|
3983
3996
|
}) {
|
|
3984
|
-
const { formData, api } = useFormContext();
|
|
3997
|
+
const { formData, api, config } = useFormContext();
|
|
3985
3998
|
const { isMobile } = useDeviceDetect();
|
|
3986
3999
|
const value = formData[fieldId] ?? [];
|
|
3987
4000
|
if (value.length === 0) {
|
|
@@ -4006,15 +4019,14 @@ function AttachmentFieldReadonly({
|
|
|
4006
4019
|
item.bucketName || bucketName,
|
|
4007
4020
|
item.objectName,
|
|
4008
4021
|
item.name,
|
|
4009
|
-
"preview"
|
|
4022
|
+
"preview",
|
|
4023
|
+
{ appType: item.appType || config.appType }
|
|
4010
4024
|
);
|
|
4011
4025
|
const ticketValue = typeof ticket === "string" ? { previewUrl: ticket } : ticket;
|
|
4012
|
-
if (ticketValue?.previewPageUrl
|
|
4013
|
-
|
|
4014
|
-
url = `${ticketValue.previewPageUrl}${separator}ticket=${encodeURIComponent(ticketValue.ticket)}`;
|
|
4026
|
+
if (ticketValue?.previewPageUrl) {
|
|
4027
|
+
url = appendTicketIfMissing3(ticketValue.previewPageUrl, ticketValue.ticket);
|
|
4015
4028
|
} else if (ticketValue?.ticket && PREVIEW_PAGE_EXTENSIONS3.includes(getFileExtension(item.name))) {
|
|
4016
|
-
|
|
4017
|
-
url = `${previewPagePath}${separator}ticket=${encodeURIComponent(ticketValue.ticket)}`;
|
|
4029
|
+
url = appendTicketIfMissing3(previewPagePath, ticketValue.ticket);
|
|
4018
4030
|
} else {
|
|
4019
4031
|
url = getTicketUrl(ticketValue, url);
|
|
4020
4032
|
}
|
|
@@ -10817,11 +10829,11 @@ function createFormRuntimeApi(config) {
|
|
|
10817
10829
|
});
|
|
10818
10830
|
return normalizeFileTicketResult(baseUrl, response.data || response.result);
|
|
10819
10831
|
},
|
|
10820
|
-
createFileAccessTicket: async (bucketName, objectName, fileName, purpose = "preview") => {
|
|
10832
|
+
createFileAccessTicket: async (bucketName, objectName, fileName, purpose = "preview", options = {}) => {
|
|
10821
10833
|
const response = await request({
|
|
10822
10834
|
url: "/file/access-ticket",
|
|
10823
10835
|
method: "post",
|
|
10824
|
-
data: { bucketName, objectName, fileName, purpose }
|
|
10836
|
+
data: { bucketName, objectName, fileName, purpose, appType: options.appType }
|
|
10825
10837
|
});
|
|
10826
10838
|
return normalizeFileTicketResult(baseUrl, response.data || response.result);
|
|
10827
10839
|
},
|