@zerodep-dev/vue-tsx-pro-ui 0.1.0 → 0.2.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/code-editor/CodeEditor.d.ts +18 -0
- package/dist/code-editor/CodeEditor.js +57 -0
- package/dist/code-editor/CodeEditor.js.map +1 -0
- package/dist/code-editor/editor.d.ts +52 -0
- package/dist/code-editor/editor.js +225 -0
- package/dist/code-editor/editor.js.map +1 -0
- package/dist/code-editor/index.d.ts +9 -0
- package/dist/code-editor/index.js +3 -0
- package/dist/config/lang/en-US.js +21 -3
- package/dist/config/lang/en-US.js.map +1 -1
- package/dist/config/lang/format.d.ts +2 -1
- package/dist/config/lang/format.js +9 -2
- package/dist/config/lang/format.js.map +1 -1
- package/dist/config/lang/lang.d.ts +19 -6
- package/dist/config/lang/lang.js +11 -2
- package/dist/config/lang/lang.js.map +1 -1
- package/dist/config/lang/zh-CN.js +21 -3
- package/dist/config/lang/zh-CN.js.map +1 -1
- package/dist/diagram/index.d.ts +2 -1
- package/dist/diagram/index.js +2 -1
- package/dist/diagram/layout.d.ts +57 -0
- package/dist/diagram/layout.js +50 -0
- package/dist/diagram/layout.js.map +1 -0
- package/dist/form/Field.d.ts +7 -0
- package/dist/form/Field.js +14 -3
- package/dist/form/Field.js.map +1 -1
- package/dist/form/useForm.d.ts +4 -1
- package/dist/form/useForm.js +35 -13
- package/dist/form/useForm.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +8 -5
- package/dist/input/Combobox.js +1 -1
- package/dist/input/Combobox.js.map +1 -1
- package/dist/input/Mentions.js +1 -1
- package/dist/input/Mentions.js.map +1 -1
- package/dist/input/PeoplePickerPanel.js +11 -11
- package/dist/input/PeoplePickerPanel.js.map +1 -1
- package/dist/input/Select.js +1 -1
- package/dist/input/Select.js.map +1 -1
- package/dist/input/TreeSelect.js +1 -1
- package/dist/input/TreeSelect.js.map +1 -1
- package/dist/input/Upload.js +2 -1
- package/dist/input/Upload.js.map +1 -1
- package/dist/input/condition.d.ts +1 -1
- package/dist/input/condition.js +32 -14
- package/dist/input/condition.js.map +1 -1
- package/dist/input/people.d.ts +15 -14
- package/dist/input/people.js +5 -5
- package/dist/input/people.js.map +1 -1
- package/dist/input/remote-items.d.ts +4 -2
- package/dist/input/remote-items.js +7 -2
- package/dist/input/remote-items.js.map +1 -1
- package/dist/patterns/QueryTable.d.ts +3 -0
- package/dist/patterns/QueryTable.js +39 -14
- package/dist/patterns/QueryTable.js.map +1 -1
- package/dist/patterns/messages.d.ts +2 -0
- package/dist/patterns/messages.js +2 -0
- package/dist/patterns/messages.js.map +1 -1
- package/dist/utils/data/clone.d.ts +8 -0
- package/dist/utils/data/clone.js +21 -0
- package/dist/utils/data/clone.js.map +1 -0
- package/dist/utils/data/date.d.ts +10 -0
- package/dist/utils/data/date.js +13 -1
- package/dist/utils/data/date.js.map +1 -1
- package/dist/utils/data/numeric.d.ts +7 -0
- package/dist/utils/data/numeric.js +46 -1
- package/dist/utils/data/numeric.js.map +1 -1
- package/dist/utils/dom/download.d.ts +8 -1
- package/dist/utils/dom/download.js +19 -2
- package/dist/utils/dom/download.js.map +1 -1
- package/dist/utils/request/error-message.d.ts +17 -0
- package/dist/utils/request/error-message.js +50 -0
- package/dist/utils/request/error-message.js.map +1 -0
- package/dist/utils/request/http.d.ts +68 -4
- package/dist/utils/request/http.js +116 -23
- package/dist/utils/request/http.js.map +1 -1
- package/dist/utils/request/use-autosave.d.ts +35 -0
- package/dist/utils/request/use-autosave.js +69 -0
- package/dist/utils/request/use-autosave.js.map +1 -0
- package/package.json +57 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.js","names":[],"sources":["../../../src/utils/request/http.ts"],"sourcesContent":["/**\n * 请求层:组件(远程选项、上传、列表查询)只依赖 HttpClient 接口,应用经 createUi / Provider 的 http 给出实现。\n * 默认实现 createHttpClient 是原生 fetch 的薄封装(参考 ky、ofetch):baseURL、query、JSON 请求体、超时与取消、\n * 状态码错误(HttpError)、请求与响应钩子。用 axios、ky 时只要写一个 request:\n *\n * createUi({ http: { request: (r) => axios.request({ ...r, params: r.query, data: r.body }).then((res) => res.data) } })\n *\n * 值按线上的 typed JSON 传(见 wire.ts):请求体里的 bigint、Decimal、CalendarDate 写成文字并附上 `$types`,\n * 地址栏参数写成同样的文字,带 `$types` 的响应先还原再交给 onResponse;`wire: false` 时按普通 JSON。\n *\n * 后端把数据包在 { code, data, message } 里时,在 onResponse 里拆开,业务错误抛 HttpError:\n *\n * const http = createHttpClient({\n * baseURL: '/api',\n * onRequest: (request) => request.headers.set('Authorization', `Bearer ${token()}`),\n * onResponse: (body: Envelope, context) => {\n * if (body.code !== 0) throw new HttpError(body.message, { ...context, data: body });\n * return body.data;\n * },\n * });\n */\nimport { useConfig } from '../../config/context';\nimport { fromWire, toWire, wireText, type WireReviveOptions, type WireScalar } from './wire';\n\n/** 请求方法 */\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';\n\n/**\n * query 里的一个值:null、undefined 不写进地址,数组写成重复的键(a=1&a=2,数组里的 null 是空字符串);\n * bigint、Decimal、日期等按线上的文字写(wireText:Decimal 不用科学计数法,日期是 ISO)\n */\nexport type QueryValue = WireScalar | readonly WireScalar[];\n\n/** 一次请求 */\nexport interface HttpRequest {\n /** 请求方法,默认 GET */\n method?: HttpMethod;\n /** 地址:相对地址拼在 baseURL 后面,完整地址(https://…)原样使用 */\n url: string;\n /** 查询参数 */\n query?: Readonly<Record<string, QueryValue>>;\n /** 请求体:普通对象与数组按 JSON 发送;FormData、Blob、URLSearchParams、字符串原样发送 */\n body?: unknown;\n /** 请求头,盖过客户端的默认请求头 */\n headers?: Readonly<Record<string, string>>;\n /** 取消:abort 后请求中止,Promise 以 AbortError 拒绝 */\n signal?: AbortSignal;\n /** 超时(毫秒),盖过客户端的设置;0 表示不限 */\n timeout?: number;\n /** 怎样读响应体,默认按 content-type:JSON 解析,text/* 读成文字,其余读成 Blob */\n responseType?: 'json' | 'text' | 'blob';\n}\n\n/** 组件依赖的请求接口:发出请求,返回解析(拆包)后的数据;失败时拒绝 */\nexport interface HttpClient {\n /** 发出一次请求,返回解析(拆包)后的数据;失败时以 HttpError 拒绝,取消时以 AbortError 拒绝 */\n request<T = unknown>(request: HttpRequest): Promise<T>;\n}\n\n/** 请求失败时的上下文:发生在哪个请求、拿到的响应 */\nexport interface HttpErrorContext {\n /** 出错的请求 */\n request: HttpRequest;\n /** 响应;网络错误、超时时没有 */\n response?: Response;\n /** 响应体(错误详情),读得到时带上 */\n data?: unknown;\n /** 底层的错误(网络错误、超时) */\n cause?: unknown;\n}\n\n/** 请求失败:状态码不是 2xx、网络错误、超时,或 onResponse 判定的业务错误 */\nexport class HttpError extends Error {\n /** 错误名,同类名:日志与 instanceof 之外的判断用 */\n override readonly name = 'HttpError';\n /** 出错的请求 */\n readonly request: HttpRequest;\n /** 响应;网络错误、超时时没有 */\n readonly response?: Response;\n /** HTTP 状态码;网络错误、超时为 0 */\n readonly status: number;\n /** 响应体(错误详情) */\n readonly data?: unknown;\n\n constructor(message: string, { request, response, data, cause }: HttpErrorContext) {\n super(message, { cause });\n this.request = request;\n this.response = response;\n this.status = response?.status ?? 0;\n this.data = data;\n }\n}\n\n/** 是不是主动取消(abort)造成的拒绝:界面上不当作错误 */\nexport const isAbortError = (error: unknown) =>\n error instanceof DOMException && error.name === 'AbortError';\n\n/** createHttpClient 的选项 */\nexport interface HttpClientOptions {\n /** 相对地址的前缀,如 '/api' */\n baseURL?: string;\n /** 每个请求都带的请求头 */\n headers?: Readonly<Record<string, string>>;\n /** 超时(毫秒),默认 30000;0 表示不限 */\n timeout?: number;\n /** 发出之前改请求(加令牌、追踪编号);可以返回一个新的 Request */\n onRequest?: (request: Request) => Request | void | Promise<Request | void>;\n /** 2xx 响应解析后的数据交给它:拆掉后端的包装、判定业务错误(抛 HttpError),返回值就是请求的结果 */\n onResponse?: (data: unknown, context: { request: HttpRequest; response: Response }) => unknown;\n /**\n * 线上的 typed JSON 怎样还原(见 wire.ts):给 `{ decimal: Decimal }` 时 decimal 还原成 Decimal,`dates: 'objects'`\n * 时日期还原成 CalendarDate 等;默认 `{}`(bigint 还原成 bigint,decimal 与日期留作文字)。false 时请求体按普通 JSON 发、响应不还原\n */\n wire?: WireReviveOptions | false;\n /** 换掉 fetch(测试、SSR、注入公共处理) */\n fetch?: typeof fetch;\n}\n\n/** createHttpClient 的结果:request 之外,再给常用方法的简写 */\nexport interface FetchClient extends HttpClient {\n /** GET:参数写在 options.query 里 */\n get<T = unknown>(url: string, options?: Omit<HttpRequest, 'url' | 'method'>): Promise<T>;\n /** DELETE */\n delete<T = unknown>(url: string, options?: Omit<HttpRequest, 'url' | 'method'>): Promise<T>;\n /** POST:普通对象的 body 按 JSON 发送 */\n post<T = unknown>(\n url: string,\n body?: unknown,\n options?: Omit<HttpRequest, 'url' | 'method' | 'body'>,\n ): Promise<T>;\n /** PUT:普通对象的 body 按 JSON 发送 */\n put<T = unknown>(\n url: string,\n body?: unknown,\n options?: Omit<HttpRequest, 'url' | 'method' | 'body'>,\n ): Promise<T>;\n /** PATCH:普通对象的 body 按 JSON 发送 */\n patch<T = unknown>(\n url: string,\n body?: unknown,\n options?: Omit<HttpRequest, 'url' | 'method' | 'body'>,\n ): Promise<T>;\n}\n\n/** 原生 fetch 的薄封装(见文件开头) */\nexport function createHttpClient(options: HttpClientOptions = {}): FetchClient {\n const {\n baseURL = '',\n timeout: defaultTimeout = 30_000,\n onRequest,\n onResponse,\n wire = {},\n } = options;\n\n async function request<T>(spec: HttpRequest): Promise<T> {\n const { method = 'GET', body, signal, timeout = defaultTimeout } = spec;\n const headers = new Headers({ ...options.headers, ...spec.headers });\n const json = isJsonBody(body);\n if (json && !headers.has('content-type')) headers.set('content-type', 'application/json');\n const signals = [signal, timeout > 0 ? AbortSignal.timeout(timeout) : undefined].filter(\n (item): item is AbortSignal => item != null,\n );\n let prepared = new Request(buildUrl(baseURL, spec.url, spec.query), {\n method,\n headers,\n body: json\n ? JSON.stringify(wire === false ? body : toWire(body))\n : (body as BodyInit | undefined),\n signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],\n });\n prepared = (await onRequest?.(prepared)) ?? prepared;\n\n let response: Response;\n try {\n response = await (options.fetch ?? fetch)(prepared);\n } catch (cause) {\n // 调用方取消的:原样交出(AbortError),界面据此不当作错误\n if (signal?.aborted) throw cause;\n const timedOut = cause instanceof DOMException && cause.name === 'TimeoutError';\n const message = timedOut\n ? `请求超时(${timeout / 1000} 秒):${method} ${spec.url}`\n : `网络错误:${method} ${spec.url}`;\n throw new HttpError(message, { request: spec, cause });\n }\n const raw = await readBody(response, spec.responseType);\n const data = wire === false ? raw : fromWire(raw, wire);\n if (!response.ok) {\n const status = `${response.status}${response.statusText ? ` ${response.statusText}` : ''}`;\n throw new HttpError(`请求失败(${status}):${method} ${spec.url}`, {\n request: spec,\n response,\n data,\n });\n }\n return (onResponse ? onResponse(data, { request: spec, response }) : data) as T;\n }\n\n const withBody =\n (method: HttpMethod) =>\n <T>(url: string, body?: unknown, rest?: Omit<HttpRequest, 'url' | 'method' | 'body'>) =>\n request<T>({ ...rest, method, url, body });\n return {\n request,\n get: (url, rest) => request({ ...rest, method: 'GET', url }),\n delete: (url, rest) => request({ ...rest, method: 'DELETE', url }),\n post: withBody('POST'),\n put: withBody('PUT'),\n patch: withBody('PATCH'),\n };\n}\n\nlet fallback: FetchClient | undefined;\n\n/**\n * 当前配置的请求客户端(在 setup 里调用):createUi / Provider 的 http;都没给时是一个默认的 createHttpClient()\n */\nexport function useHttp(): HttpClient {\n return useConfig().http ?? (fallback ??= createHttpClient());\n}\n\n/** 地址 + query:相对地址拼在 baseURL 后面(斜杠不重复),null、undefined 的参数跳过 */\nfunction buildUrl(baseURL: string, url: string, query: HttpRequest['query']): string {\n const absolute = /^[a-z][a-z\\d+.-]*:/i.test(url) || url.startsWith('//');\n let full =\n absolute || !baseURL ? url : `${baseURL.replace(/\\/+$/, '')}/${url.replace(/^\\/+/, '')}`;\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value == null) continue;\n const items: readonly WireScalar[] = Array.isArray(value) ? value : [value as WireScalar];\n for (const item of items) params.append(key, wireText(item));\n }\n const search = params.toString();\n if (search) full += `${full.includes('?') ? '&' : '?'}${search}`;\n return full;\n}\n\n/** 普通对象与数组按 JSON 发送 */\nfunction isJsonBody(body: unknown): boolean {\n if (body == null || typeof body !== 'object') return false;\n const prototype = Object.getPrototypeOf(body) as unknown;\n return Array.isArray(body) || prototype === Object.prototype || prototype === null;\n}\n\nasync function readBody(response: Response, type: HttpRequest['responseType']): Promise<unknown> {\n if (\n response.status === 204 ||\n response.status === 205 ||\n response.headers.get('content-length') === '0'\n ) {\n return undefined;\n }\n const contentType = response.headers.get('content-type') ?? '';\n const kind =\n type ??\n (/[/+]json\\b/.test(contentType) ? 'json' : contentType.startsWith('text/') ? 'text' : 'blob');\n if (kind === 'blob') return response.blob();\n const text = await response.text();\n if (kind === 'text') return text;\n if (text === '') return undefined;\n try {\n return JSON.parse(text);\n } catch {\n // 声明是 JSON 却解析不了(网关的 HTML 错误页):交出原文,状态码错误里还能看到\n return text;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"http.js","names":[],"sources":["../../../src/utils/request/http.ts"],"sourcesContent":["/**\n * 请求层:组件(远程选项、上传、列表查询)只依赖 HttpClient 接口,应用经 createUi / Provider 的 http 给出实现。\n * 默认实现 createHttpClient 是原生 fetch 的薄封装(参考 ky、ofetch):baseURL、query、JSON 请求体、超时与取消、\n * 拆响应体、带码的错误(HttpError)、请求与响应钩子。用 axios、ky 时只要写一个 request:\n *\n * createUi({ http: { request: (r) => axios.request({ ...r, params: r.query, data: r.body }).then((res) => res.data) } })\n *\n * 值按线上的 typed JSON 传(见 wire.ts):请求体里的 bigint、Decimal、CalendarDate 写成文字并附上 `$types`,\n * 地址栏参数写成同样的文字,带 `$types` 的响应先还原再交给 onResponse;`wire: false` 时按普通 JSON。\n *\n * 响应体按业务错误码的约定拆开(flow 仓库 docs/decisions/0008,query、flow 的后端都这样回):HTTP 一律 200,\n * `{ code: 'ok', data }` 交出 data,`{ code: 'flow.work-item-not-open', message, params, errors }` 抛带这个码的 HttpError;\n * 请求层自己出的错也带码:`http.network`、`http.timeout`、`http.status`(网关回了非 2xx)、`http.parse`(响应读不懂)。\n * 组件按码从语言包取文案(见 error-message.ts)。后端用别的包装(`{ code: 0, data }`)时关掉 envelope,在 onResponse 里拆:\n *\n * const http = createHttpClient({\n * baseURL: '/api',\n * envelope: false,\n * onRequest: (request) => request.headers.set('Authorization', `Bearer ${token()}`),\n * onResponse: (body: Envelope, context) => {\n * if (body.code !== 0) throw new HttpError(body.message, { ...context, code: `app.${body.code}`, data: body });\n * return body.data;\n * },\n * });\n */\nimport { useConfig } from '../../config/context';\nimport { fromWire, toWire, wireText, type WireReviveOptions, type WireScalar } from './wire';\n\n/** 请求方法 */\nexport type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD';\n\n/**\n * query 里的一个值:null、undefined 不写进地址,数组写成重复的键(a=1&a=2,数组里的 null 是空字符串);\n * bigint、Decimal、日期等按线上的文字写(wireText:Decimal 不用科学计数法,日期是 ISO)\n */\nexport type QueryValue = WireScalar | readonly WireScalar[];\n\n/** 一次请求 */\nexport interface HttpRequest {\n /** 请求方法,默认 GET */\n method?: HttpMethod;\n /** 地址:相对地址拼在 baseURL 后面,完整地址(https://…)原样使用 */\n url: string;\n /** 查询参数 */\n query?: Readonly<Record<string, QueryValue>>;\n /** 请求体:普通对象与数组按 JSON 发送;FormData、Blob、URLSearchParams、字符串原样发送 */\n body?: unknown;\n /** 请求头,盖过客户端的默认请求头 */\n headers?: Readonly<Record<string, string>>;\n /** 取消:abort 后请求中止,Promise 以 AbortError 拒绝 */\n signal?: AbortSignal;\n /** 超时(毫秒),盖过客户端的设置;0 表示不限 */\n timeout?: number;\n /** 怎样读响应体,默认按 content-type:JSON 解析,text/* 读成文字,其余读成 Blob */\n responseType?: 'json' | 'text' | 'blob';\n}\n\n/** 组件依赖的请求接口:发出请求,返回解析(拆包)后的数据;失败时拒绝 */\nexport interface HttpClient {\n /** 发出一次请求,返回解析(拆包)后的数据;失败时以 HttpError 拒绝,取消时以 AbortError 拒绝 */\n request<T = unknown>(request: HttpRequest): Promise<T>;\n}\n\n/** 成功的码 */\nexport const OK_CODE = 'ok';\n\n/**\n * 请求层自己出的错的码(0008 的 `http.` 命名空间,只在前端产生)。取消不当作错误,仍以 AbortError 拒绝,\n * errorCodeOf 把它认作 `http.aborted`\n */\nexport const HTTP_ERROR_CODES = [\n 'http.network',\n 'http.timeout',\n 'http.aborted',\n 'http.status',\n 'http.parse',\n] as const;\n\n/** 请求层的错误码 */\nexport type HttpErrorCode = (typeof HTTP_ERROR_CODES)[number];\n\n/**\n * 错误明细的一项(0008):按字段的(表单校验:field、code),或按位置的(流程定义的问题:node、edge);没有的部分省掉。\n * 后端只给说明、不给代号时有 message\n */\nexport interface ApiErrorDetail {\n /** 问题的代号(required、max……) */\n readonly code?: string;\n /** 字段路径(`email`、`lines[0].price`) */\n readonly field?: string;\n /** 后端给的说明,只作兜底 */\n readonly message?: string;\n /** 文案里要填的参数 */\n readonly params?: Readonly<Record<string, unknown>>;\n /** 别的位置信息(节点、连线……) */\n readonly [key: string]: unknown;\n}\n\n/** 后端回的响应体(0008 第 1 节):成功的 code 是 `ok`,失败是业务错误码 */\nexport interface ApiEnvelope<T = unknown> {\n /** `ok` 或业务错误码 */\n readonly code: string;\n /** 后端给的说明,只作兜底:界面按 code 取文案 */\n readonly message?: string;\n /** 文案里要填的参数 */\n readonly params?: Readonly<Record<string, unknown>>;\n /** 按字段、按问题的明细(表单校验) */\n readonly errors?: readonly ApiErrorDetail[];\n /** 成功时的数据 */\n readonly data?: T;\n}\n\n/** 请求失败时的上下文:业务错误码、发生在哪个请求、拿到的响应 */\nexport interface HttpErrorContext {\n /** 业务错误码:后端回的(`flow.`、`query.`、应用自己的)或请求层的(`http.`) */\n code: string;\n /** 文案里要填的参数 */\n params?: Readonly<Record<string, unknown>>;\n /** 按字段、按问题的明细 */\n errors?: readonly ApiErrorDetail[];\n /** 出错的请求 */\n request: HttpRequest;\n /** 响应;网络错误、超时时没有 */\n response?: Response;\n /** 响应体(错误详情),读得到时带上 */\n data?: unknown;\n /** 底层的错误(网络错误、超时) */\n cause?: unknown;\n}\n\n/** 请求失败:后端回了业务错误码、状态码不是 2xx、网络错误、超时、响应读不懂,或 onResponse 判定的业务错误 */\nexport class HttpError extends Error {\n /** 错误名,同类名:日志与 instanceof 之外的判断用 */\n override readonly name = 'HttpError';\n /** 业务错误码(见 HttpErrorContext.code);界面按它取文案 */\n readonly code: string;\n /** 文案里要填的参数 */\n readonly params: Readonly<Record<string, unknown>>;\n /** 按字段、按问题的明细;useForm 的 submit 据此标到字段上 */\n readonly errors: readonly ApiErrorDetail[];\n /** 出错的请求 */\n readonly request: HttpRequest;\n /** 响应;网络错误、超时时没有 */\n readonly response?: Response;\n /** HTTP 状态码;网络错误、超时为 0 */\n readonly status: number;\n /** 响应体(错误详情) */\n readonly data?: unknown;\n\n constructor(\n message: string,\n { code, params, errors, request, response, data, cause }: HttpErrorContext,\n ) {\n super(message, { cause });\n this.code = code;\n this.params = params ?? {};\n this.errors = errors ?? [];\n this.request = request;\n this.response = response;\n this.status = response?.status ?? 0;\n this.data = data;\n }\n}\n\n/** 是不是主动取消(abort)造成的拒绝:界面上不当作错误 */\nexport const isAbortError = (error: unknown) =>\n error instanceof DOMException && error.name === 'AbortError';\n\n/** createHttpClient 的选项 */\nexport interface HttpClientOptions {\n /** 相对地址的前缀,如 '/api' */\n baseURL?: string;\n /** 每个请求都带的请求头 */\n headers?: Readonly<Record<string, string>>;\n /** 超时(毫秒),默认 30000;0 表示不限 */\n timeout?: number;\n /** 发出之前改请求(加令牌、追踪编号);可以返回一个新的 Request */\n onRequest?: (request: Request) => Request | void | Promise<Request | void>;\n /**\n * 按 0008 的约定拆响应体(见文件开头),默认 true:键都在 code、message、params、errors、data 里、code 是文字的对象\n * 才当作包装,其余原样交出。后端用别的包装时给 false,在 onResponse 里拆\n */\n envelope?: boolean;\n /** 成功的响应(拆包之后)的数据交给它:再加工、判定别的业务错误(抛 HttpError),返回值就是请求的结果 */\n onResponse?: (data: unknown, context: { request: HttpRequest; response: Response }) => unknown;\n /**\n * 线上的 typed JSON 怎样还原(见 wire.ts):给 `{ decimal: Decimal }` 时 decimal 还原成 Decimal,`dates: 'objects'`\n * 时日期还原成 CalendarDate 等;默认 `{}`(bigint 还原成 bigint,decimal 与日期留作文字)。false 时请求体按普通 JSON 发、响应不还原\n */\n wire?: WireReviveOptions | false;\n /** 换掉 fetch(测试、SSR、注入公共处理) */\n fetch?: typeof fetch;\n}\n\n/** createHttpClient 的结果:request 之外,再给常用方法的简写 */\nexport interface FetchClient extends HttpClient {\n /** GET:参数写在 options.query 里 */\n get<T = unknown>(url: string, options?: Omit<HttpRequest, 'url' | 'method'>): Promise<T>;\n /** DELETE */\n delete<T = unknown>(url: string, options?: Omit<HttpRequest, 'url' | 'method'>): Promise<T>;\n /** POST:普通对象的 body 按 JSON 发送 */\n post<T = unknown>(\n url: string,\n body?: unknown,\n options?: Omit<HttpRequest, 'url' | 'method' | 'body'>,\n ): Promise<T>;\n /** PUT:普通对象的 body 按 JSON 发送 */\n put<T = unknown>(\n url: string,\n body?: unknown,\n options?: Omit<HttpRequest, 'url' | 'method' | 'body'>,\n ): Promise<T>;\n /** PATCH:普通对象的 body 按 JSON 发送 */\n patch<T = unknown>(\n url: string,\n body?: unknown,\n options?: Omit<HttpRequest, 'url' | 'method' | 'body'>,\n ): Promise<T>;\n}\n\n/** 原生 fetch 的薄封装(见文件开头) */\nexport function createHttpClient(options: HttpClientOptions = {}): FetchClient {\n const {\n baseURL = '',\n timeout: defaultTimeout = 30_000,\n onRequest,\n onResponse,\n wire = {},\n envelope: unwrap = true,\n } = options;\n const revive = (value: unknown) => (wire === false ? value : fromWire(value, wire));\n\n async function request<T>(spec: HttpRequest): Promise<T> {\n const { method = 'GET', body, signal, timeout = defaultTimeout } = spec;\n const headers = new Headers({ ...options.headers, ...spec.headers });\n const json = isJsonBody(body);\n if (json && !headers.has('content-type')) headers.set('content-type', 'application/json');\n const signals = [signal, timeout > 0 ? AbortSignal.timeout(timeout) : undefined].filter(\n (item): item is AbortSignal => item != null,\n );\n let prepared = new Request(buildUrl(baseURL, spec.url, spec.query), {\n method,\n headers,\n body: json\n ? JSON.stringify(wire === false ? body : toWire(body))\n : (body as BodyInit | undefined),\n signal: signals.length > 1 ? AbortSignal.any(signals) : signals[0],\n });\n prepared = (await onRequest?.(prepared)) ?? prepared;\n\n let response: Response;\n try {\n response = await (options.fetch ?? fetch)(prepared);\n } catch (cause) {\n // 调用方取消的:原样交出(AbortError),界面据此不当作错误\n if (signal?.aborted) throw cause;\n const timedOut = cause instanceof DOMException && cause.name === 'TimeoutError';\n const message = timedOut\n ? `请求超时(${timeout / 1000} 秒):${method} ${spec.url}`\n : `网络错误:${method} ${spec.url}`;\n throw new HttpError(message, {\n code: timedOut ? 'http.timeout' : 'http.network',\n params: timedOut ? { seconds: timeout / 1000 } : undefined,\n request: spec,\n cause,\n });\n }\n const context = { request: spec, response };\n const { body: raw, malformed } = await readBody(response, spec.responseType);\n let data: unknown;\n try {\n data = malformed ? raw : revive(raw);\n } catch (cause) {\n throw new HttpError(`响应读不懂:${method} ${spec.url}`, {\n ...context,\n code: 'http.parse',\n data: raw,\n cause,\n });\n }\n const envelope = unwrap && isEnvelope(data) ? data : undefined;\n // 业务错误以响应体的码为准:网关原样转发非 2xx 的业务响应时也认得出\n if (envelope && envelope.code !== OK_CODE) {\n throw new HttpError(envelope.message || envelope.code, {\n ...context,\n code: envelope.code,\n params: envelope.params,\n errors: envelope.errors,\n data,\n });\n }\n if (!response.ok) {\n const status = `${response.status}${response.statusText ? ` ${response.statusText}` : ''}`;\n throw new HttpError(`请求失败(${status}):${method} ${spec.url}`, {\n ...context,\n code: 'http.status',\n params: { status: response.status },\n data,\n });\n }\n if (malformed) {\n throw new HttpError(`响应不是 JSON:${method} ${spec.url}`, {\n ...context,\n code: 'http.parse',\n data,\n });\n }\n // 包装里的 data 自己带 $types 时(后端先 toWire 数据再包),再还原一次;已还原过的不再有 $types\n const result = envelope ? revive(envelope.data) : data;\n return (onResponse ? onResponse(result, context) : result) as T;\n }\n\n return scopeHttp({ request });\n}\n\n/**\n * 在任意请求客户端上加常用方法的简写,相对地址前面拼上 prefix(斜杠不重复;完整地址原样)。\n * 一个模块的接口都在同一个前缀下时用它,不必每次写全地址;useHttp() 拿到的客户端也能这样包:\n *\n * const api = scopeHttp(useHttp(), '/flow');\n * api.get('/queries/todo', { query: { page: 1 }, signal }); // GET /flow/queries/todo?page=1\n * api.post('/commands/claim', { workItemId });\n */\nexport function scopeHttp(client: HttpClient, prefix = ''): FetchClient {\n const at = (url: string) => (!prefix || isAbsolute(url) ? url : joinPath(prefix, url));\n const request = <T>(spec: HttpRequest) => client.request<T>({ ...spec, url: at(spec.url) });\n const withBody =\n (method: HttpMethod) =>\n <T>(url: string, body?: unknown, rest?: Omit<HttpRequest, 'url' | 'method' | 'body'>) =>\n request<T>({ ...rest, method, url, body });\n return {\n request,\n get: (url, rest) => request({ ...rest, method: 'GET', url }),\n delete: (url, rest) => request({ ...rest, method: 'DELETE', url }),\n post: withBody('POST'),\n put: withBody('PUT'),\n patch: withBody('PATCH'),\n };\n}\n\n/** 完整地址(https://…、//host/…):不拼 baseURL、前缀 */\nconst isAbsolute = (url: string) => /^[a-z][a-z\\d+.-]*:/i.test(url) || url.startsWith('//');\n\n/** 两段路径接起来,中间只留一个斜杠 */\nconst joinPath = (base: string, path: string) =>\n `${base.replace(/\\/+$/, '')}/${path.replace(/^\\/+/, '')}`;\n\nlet fallback: FetchClient | undefined;\n\n/**\n * 当前配置的请求客户端(在 setup 里调用):createUi / Provider 的 http;都没给时是一个默认的 createHttpClient()\n */\nexport function useHttp(): HttpClient {\n return useConfig().http ?? (fallback ??= createHttpClient());\n}\n\n/** 地址 + query:相对地址拼在 baseURL 后面(斜杠不重复),null、undefined 的参数跳过 */\nfunction buildUrl(baseURL: string, url: string, query: HttpRequest['query']): string {\n let full = isAbsolute(url) || !baseURL ? url : joinPath(baseURL, url);\n const params = new URLSearchParams();\n for (const [key, value] of Object.entries(query ?? {})) {\n if (value == null) continue;\n const items: readonly WireScalar[] = Array.isArray(value) ? value : [value as WireScalar];\n for (const item of items) params.append(key, wireText(item));\n }\n const search = params.toString();\n if (search) full += `${full.includes('?') ? '&' : '?'}${search}`;\n return full;\n}\n\nconst ENVELOPE_KEYS = new Set(['code', 'message', 'params', 'errors', 'data']);\n\n/** 是不是 0008 的响应体:code 是文字、没有别的键(不把恰好有 code 字段的业务对象当成包装) */\nfunction isEnvelope(value: unknown): value is ApiEnvelope {\n if (value == null || typeof value !== 'object' || Array.isArray(value)) return false;\n const record = value as Record<string, unknown>;\n return (\n typeof record.code === 'string' && Object.keys(record).every((key) => ENVELOPE_KEYS.has(key))\n );\n}\n\n/** 普通对象与数组按 JSON 发送 */\nfunction isJsonBody(body: unknown): boolean {\n if (body == null || typeof body !== 'object') return false;\n const prototype = Object.getPrototypeOf(body) as unknown;\n return Array.isArray(body) || prototype === Object.prototype || prototype === null;\n}\n\n/** 读响应体;声明是 JSON 却解析不了(网关的 HTML 错误页)时交出原文并标上 malformed */\nasync function readBody(\n response: Response,\n type: HttpRequest['responseType'],\n): Promise<{ body: unknown; malformed?: boolean }> {\n if (\n response.status === 204 ||\n response.status === 205 ||\n response.headers.get('content-length') === '0'\n ) {\n return { body: undefined };\n }\n const contentType = response.headers.get('content-type') ?? '';\n const kind =\n type ??\n (/[/+]json\\b/.test(contentType) ? 'json' : contentType.startsWith('text/') ? 'text' : 'blob');\n if (kind === 'blob') return { body: await response.blob() };\n const text = await response.text();\n if (kind === 'text') return { body: text };\n if (text === '') return { body: undefined };\n try {\n return { body: JSON.parse(text) };\n } catch {\n return { body: text, malformed: true };\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** useAutosave 的选项 */
|
|
2
|
+
export interface AutosaveOptions<Saved> {
|
|
3
|
+
/** 现在的内容;和基准相同(===)时不存。对象请给序列化后的文字 */
|
|
4
|
+
content: () => string;
|
|
5
|
+
/** 存一份;抛错时状态是 failed(isConflict 认出的是 conflict) */
|
|
6
|
+
save: (content: string) => Promise<Saved>;
|
|
7
|
+
/** 改完停多久再存(毫秒),默认 2000 */
|
|
8
|
+
delay?: number;
|
|
9
|
+
/** 认出冲突(别人先存过):之后不再自动存,直到再 start */
|
|
10
|
+
isConflict?: (error: unknown) => boolean;
|
|
11
|
+
/** 现在能不能存(比如还没填标识);不给时总能存 */
|
|
12
|
+
enabled?: () => boolean;
|
|
13
|
+
}
|
|
14
|
+
/** 自动保存现在怎样 */
|
|
15
|
+
export interface AutosaveStatus<Saved> {
|
|
16
|
+
/** idle 没改过;saving 正在存;saved 存好了;conflict 别人先存过,我的没存;failed 存的时候出错 */
|
|
17
|
+
state: 'idle' | 'saving' | 'saved' | 'conflict' | 'failed';
|
|
18
|
+
/** 最近一次存好的结果 */
|
|
19
|
+
saved: Saved | null;
|
|
20
|
+
/** 最近一次失败、冲突的错误(冲突时从里面取是谁先存的) */
|
|
21
|
+
error: unknown;
|
|
22
|
+
}
|
|
23
|
+
/** useAutosave 的结果 */
|
|
24
|
+
export interface Autosave<Saved> {
|
|
25
|
+
/** 状态(响应式) */
|
|
26
|
+
readonly status: AutosaveStatus<Saved>;
|
|
27
|
+
/** 开始(或重新开始):清掉状态与计时,现在的内容是基准,之后改了才存;saved 是载入时已有的那份 */
|
|
28
|
+
start(saved?: Saved | null): void;
|
|
29
|
+
/** 有改动时马上存(离开页面前、手动保存) */
|
|
30
|
+
flush(): Promise<void>;
|
|
31
|
+
/** 停下:不再自动存,直到 start */
|
|
32
|
+
stop(): void;
|
|
33
|
+
}
|
|
34
|
+
/** 内容改了停一会儿就存(在 setup 里调用;组件卸载时停下) */
|
|
35
|
+
export declare function useAutosave<Saved>(options: AutosaveOptions<Saved>): Autosave<Saved>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { onBeforeUnmount, shallowReactive, watch } from "vue";
|
|
2
|
+
//#region src/utils/request/use-autosave.ts
|
|
3
|
+
/**
|
|
4
|
+
* 自动保存:内容改了停一会儿就存(编辑器的草稿、长表单)。只在内容和上次存的(或载入时的)不一样时才存;
|
|
5
|
+
* 别人先存过(冲突)时停下来,等使用方决定载入别人的还是放弃。
|
|
6
|
+
*
|
|
7
|
+
* const autosave = useAutosave({
|
|
8
|
+
* content: () => JSON.stringify(definition),
|
|
9
|
+
* save: (content) => api.post('/drafts/save', { content, rev }),
|
|
10
|
+
* isConflict: (error) => error instanceof HttpError && error.code === 'app.concurrent-modification',
|
|
11
|
+
* });
|
|
12
|
+
* autosave.start(); // 编辑器载入好了:现在的内容是基准,之后改了才存
|
|
13
|
+
* autosave.status.state // idle | saving | saved | conflict | failed
|
|
14
|
+
* autosave.start(); // 发布、放弃、载入别人的之后再调一次:清掉状态,现在的内容是新的基准
|
|
15
|
+
*/
|
|
16
|
+
/** 内容改了停一会儿就存(在 setup 里调用;组件卸载时停下) */
|
|
17
|
+
function useAutosave(options) {
|
|
18
|
+
const delay = options.delay ?? 2e3;
|
|
19
|
+
const status = shallowReactive({
|
|
20
|
+
state: "idle",
|
|
21
|
+
saved: null,
|
|
22
|
+
error: null
|
|
23
|
+
});
|
|
24
|
+
let baseline = null;
|
|
25
|
+
let timer;
|
|
26
|
+
async function flush() {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
const content = options.content();
|
|
29
|
+
if (baseline === null || content === baseline || status.state === "conflict" || options.enabled?.() === false) return;
|
|
30
|
+
status.state = "saving";
|
|
31
|
+
try {
|
|
32
|
+
status.saved = await options.save(content);
|
|
33
|
+
baseline = content;
|
|
34
|
+
status.error = null;
|
|
35
|
+
status.state = "saved";
|
|
36
|
+
} catch (error) {
|
|
37
|
+
status.error = error;
|
|
38
|
+
status.state = options.isConflict?.(error) ? "conflict" : "failed";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function start(saved = null) {
|
|
42
|
+
clearTimeout(timer);
|
|
43
|
+
baseline = options.content();
|
|
44
|
+
Object.assign(status, {
|
|
45
|
+
state: "idle",
|
|
46
|
+
saved,
|
|
47
|
+
error: null
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
watch(options.content, () => {
|
|
51
|
+
if (baseline === null) return;
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
timer = setTimeout(() => void flush(), delay);
|
|
54
|
+
});
|
|
55
|
+
onBeforeUnmount(() => clearTimeout(timer));
|
|
56
|
+
return {
|
|
57
|
+
status,
|
|
58
|
+
start,
|
|
59
|
+
flush,
|
|
60
|
+
stop() {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
baseline = null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
export { useAutosave };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=use-autosave.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-autosave.js","names":[],"sources":["../../../src/utils/request/use-autosave.ts"],"sourcesContent":["/**\n * 自动保存:内容改了停一会儿就存(编辑器的草稿、长表单)。只在内容和上次存的(或载入时的)不一样时才存;\n * 别人先存过(冲突)时停下来,等使用方决定载入别人的还是放弃。\n *\n * const autosave = useAutosave({\n * content: () => JSON.stringify(definition),\n * save: (content) => api.post('/drafts/save', { content, rev }),\n * isConflict: (error) => error instanceof HttpError && error.code === 'app.concurrent-modification',\n * });\n * autosave.start(); // 编辑器载入好了:现在的内容是基准,之后改了才存\n * autosave.status.state // idle | saving | saved | conflict | failed\n * autosave.start(); // 发布、放弃、载入别人的之后再调一次:清掉状态,现在的内容是新的基准\n */\nimport { onBeforeUnmount, shallowReactive, watch } from 'vue';\n\n/** useAutosave 的选项 */\nexport interface AutosaveOptions<Saved> {\n /** 现在的内容;和基准相同(===)时不存。对象请给序列化后的文字 */\n content: () => string;\n /** 存一份;抛错时状态是 failed(isConflict 认出的是 conflict) */\n save: (content: string) => Promise<Saved>;\n /** 改完停多久再存(毫秒),默认 2000 */\n delay?: number;\n /** 认出冲突(别人先存过):之后不再自动存,直到再 start */\n isConflict?: (error: unknown) => boolean;\n /** 现在能不能存(比如还没填标识);不给时总能存 */\n enabled?: () => boolean;\n}\n\n/** 自动保存现在怎样 */\nexport interface AutosaveStatus<Saved> {\n /** idle 没改过;saving 正在存;saved 存好了;conflict 别人先存过,我的没存;failed 存的时候出错 */\n state: 'idle' | 'saving' | 'saved' | 'conflict' | 'failed';\n /** 最近一次存好的结果 */\n saved: Saved | null;\n /** 最近一次失败、冲突的错误(冲突时从里面取是谁先存的) */\n error: unknown;\n}\n\n/** useAutosave 的结果 */\nexport interface Autosave<Saved> {\n /** 状态(响应式) */\n readonly status: AutosaveStatus<Saved>;\n /** 开始(或重新开始):清掉状态与计时,现在的内容是基准,之后改了才存;saved 是载入时已有的那份 */\n start(saved?: Saved | null): void;\n /** 有改动时马上存(离开页面前、手动保存) */\n flush(): Promise<void>;\n /** 停下:不再自动存,直到 start */\n stop(): void;\n}\n\n/** 内容改了停一会儿就存(在 setup 里调用;组件卸载时停下) */\nexport function useAutosave<Saved>(options: AutosaveOptions<Saved>): Autosave<Saved> {\n const delay = options.delay ?? 2000;\n const status = shallowReactive<AutosaveStatus<Saved>>({\n state: 'idle',\n saved: null,\n error: null,\n });\n // 和这份内容一样时不用存:载入时的、刚存过的;null 是还没开始\n let baseline: string | null = null;\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n async function flush() {\n clearTimeout(timer);\n const content = options.content();\n if (\n baseline === null ||\n content === baseline ||\n status.state === 'conflict' ||\n options.enabled?.() === false\n )\n return;\n status.state = 'saving';\n try {\n status.saved = await options.save(content);\n baseline = content;\n status.error = null;\n status.state = 'saved';\n } catch (error) {\n status.error = error;\n status.state = options.isConflict?.(error) ? 'conflict' : 'failed';\n }\n }\n\n function start(saved: Saved | null = null) {\n clearTimeout(timer);\n baseline = options.content();\n Object.assign(status, { state: 'idle', saved, error: null });\n }\n\n watch(options.content, () => {\n if (baseline === null) return;\n clearTimeout(timer);\n timer = setTimeout(() => void flush(), delay);\n });\n onBeforeUnmount(() => clearTimeout(timer));\n\n return {\n status,\n start,\n flush,\n stop() {\n clearTimeout(timer);\n baseline = null;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAoDA,SAAgB,YAAmB,SAAkD;CACnF,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,SAAS,gBAAuC;EACpD,OAAO;EACP,OAAO;EACP,OAAO;CACT,CAAC;CAED,IAAI,WAA0B;CAC9B,IAAI;CAEJ,eAAe,QAAQ;EACrB,aAAa,KAAK;EAClB,MAAM,UAAU,QAAQ,QAAQ;EAChC,IACE,aAAa,QACb,YAAY,YACZ,OAAO,UAAU,cACjB,QAAQ,UAAU,MAAM,OAExB;EACF,OAAO,QAAQ;EACf,IAAI;GACF,OAAO,QAAQ,MAAM,QAAQ,KAAK,OAAO;GACzC,WAAW;GACX,OAAO,QAAQ;GACf,OAAO,QAAQ;EACjB,SAAS,OAAO;GACd,OAAO,QAAQ;GACf,OAAO,QAAQ,QAAQ,aAAa,KAAK,IAAI,aAAa;EAC5D;CACF;CAEA,SAAS,MAAM,QAAsB,MAAM;EACzC,aAAa,KAAK;EAClB,WAAW,QAAQ,QAAQ;EAC3B,OAAO,OAAO,QAAQ;GAAE,OAAO;GAAQ;GAAO,OAAO;EAAK,CAAC;CAC7D;CAEA,MAAM,QAAQ,eAAe;EAC3B,IAAI,aAAa,MAAM;EACvB,aAAa,KAAK;EAClB,QAAQ,iBAAiB,KAAK,MAAM,GAAG,KAAK;CAC9C,CAAC;CACD,sBAAsB,aAAa,KAAK,CAAC;CAEzC,OAAO;EACL;EACA;EACA;EACA,OAAO;GACL,aAAa,KAAK;GAClB,WAAW;EACb;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerodep-dev/vue-tsx-pro-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "vue-tsx-pro 组件库",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -78,6 +78,11 @@
|
|
|
78
78
|
"types": "./dist/xlsx/index.d.ts",
|
|
79
79
|
"default": "./dist/xlsx/index.js"
|
|
80
80
|
},
|
|
81
|
+
"./code-editor": {
|
|
82
|
+
"@vue-tsx-pro/source": "./src/code-editor/index.ts",
|
|
83
|
+
"types": "./dist/code-editor/index.d.ts",
|
|
84
|
+
"default": "./dist/code-editor/index.js"
|
|
85
|
+
},
|
|
81
86
|
"./theme-boot": {
|
|
82
87
|
"@vue-tsx-pro/source": "./src/config/theme/boot.ts",
|
|
83
88
|
"types": "./dist/config/theme/boot.d.ts",
|
|
@@ -107,7 +112,7 @@
|
|
|
107
112
|
"@floating-ui/dom": "1.8.0",
|
|
108
113
|
"@internationalized/date": "3.12.4",
|
|
109
114
|
"@radix-ui/colors": "3.0.0",
|
|
110
|
-
"@zerodep-dev/vue-tsx-pro-tsx": "^0.
|
|
115
|
+
"@zerodep-dev/vue-tsx-pro-tsx": "^0.2.0",
|
|
111
116
|
"animejs": "4.5.0",
|
|
112
117
|
"culori": "4.0.2",
|
|
113
118
|
"decimal.js": "10.6.0",
|
|
@@ -124,7 +129,15 @@
|
|
|
124
129
|
"vue-router": "^5.0.0",
|
|
125
130
|
"zod": "^4.6.0",
|
|
126
131
|
"pinyin-pro": "^3.20.0",
|
|
127
|
-
"exceljs": "^4.4.0"
|
|
132
|
+
"exceljs": "^4.4.0",
|
|
133
|
+
"@codemirror/autocomplete": "^6.20.0",
|
|
134
|
+
"@codemirror/commands": "^6.11.0",
|
|
135
|
+
"@codemirror/lang-javascript": "^6.2.0",
|
|
136
|
+
"@codemirror/language": "^6.13.0",
|
|
137
|
+
"@codemirror/search": "^6.7.0",
|
|
138
|
+
"@codemirror/state": "^6.7.0",
|
|
139
|
+
"@codemirror/view": "^6.43.0",
|
|
140
|
+
"@lezer/highlight": "^1.2.0"
|
|
128
141
|
},
|
|
129
142
|
"peerDependenciesMeta": {
|
|
130
143
|
"pinia": {
|
|
@@ -147,13 +160,45 @@
|
|
|
147
160
|
},
|
|
148
161
|
"exceljs": {
|
|
149
162
|
"optional": true
|
|
163
|
+
},
|
|
164
|
+
"@codemirror/autocomplete": {
|
|
165
|
+
"optional": true
|
|
166
|
+
},
|
|
167
|
+
"@codemirror/commands": {
|
|
168
|
+
"optional": true
|
|
169
|
+
},
|
|
170
|
+
"@codemirror/lang-javascript": {
|
|
171
|
+
"optional": true
|
|
172
|
+
},
|
|
173
|
+
"@codemirror/language": {
|
|
174
|
+
"optional": true
|
|
175
|
+
},
|
|
176
|
+
"@codemirror/search": {
|
|
177
|
+
"optional": true
|
|
178
|
+
},
|
|
179
|
+
"@codemirror/state": {
|
|
180
|
+
"optional": true
|
|
181
|
+
},
|
|
182
|
+
"@codemirror/view": {
|
|
183
|
+
"optional": true
|
|
184
|
+
},
|
|
185
|
+
"@lezer/highlight": {
|
|
186
|
+
"optional": true
|
|
150
187
|
}
|
|
151
188
|
},
|
|
152
189
|
"vueTsxPro": {
|
|
153
190
|
"optimizeDeps": {
|
|
154
191
|
"include": [
|
|
192
|
+
"@codemirror/autocomplete",
|
|
193
|
+
"@codemirror/commands",
|
|
194
|
+
"@codemirror/lang-javascript",
|
|
195
|
+
"@codemirror/language",
|
|
196
|
+
"@codemirror/search",
|
|
197
|
+
"@codemirror/state",
|
|
198
|
+
"@codemirror/view",
|
|
155
199
|
"@floating-ui/dom",
|
|
156
200
|
"@internationalized/date",
|
|
201
|
+
"@lezer/highlight",
|
|
157
202
|
"@lucide/icons",
|
|
158
203
|
"@radix-ui/colors",
|
|
159
204
|
"@unovis/ts",
|
|
@@ -195,7 +240,15 @@
|
|
|
195
240
|
"@types/culori": "4.0.1",
|
|
196
241
|
"pinyin-pro": "3.29.5",
|
|
197
242
|
"exceljs": "4.4.0",
|
|
198
|
-
"@types/topojson-simplify": "3.0.3"
|
|
243
|
+
"@types/topojson-simplify": "3.0.3",
|
|
244
|
+
"@codemirror/autocomplete": "6.20.3",
|
|
245
|
+
"@codemirror/commands": "6.11.1",
|
|
246
|
+
"@codemirror/lang-javascript": "6.2.5",
|
|
247
|
+
"@codemirror/language": "6.13.1",
|
|
248
|
+
"@codemirror/search": "6.7.2",
|
|
249
|
+
"@codemirror/state": "6.7.6",
|
|
250
|
+
"@codemirror/view": "6.43.14",
|
|
251
|
+
"@lezer/highlight": "1.2.5"
|
|
199
252
|
},
|
|
200
253
|
"publishConfig": {
|
|
201
254
|
"access": "public"
|