@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
package/dist/input/Upload.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Upload.js","names":["_isVNode","Upload","_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_vue_shallowRef","_createVNode","_mergeProps","FileIcon","_Fragment"],"sources":["../../src/input/Upload.tsx"],"sourcesContent":["/**\n * 上传(div):FileDrop 选文件,下面列出文件、上传进度、失败重试与移除。只负责界面与状态,怎样上传由 upload 决定:\n *\n * <Upload\n * _value={files}\n * multiple\n * accept=\"image/*\"\n * maxSize={5_000_000}\n * upload={(file, { onProgress, signal }) => api.upload(file, { onProgress, signal })}\n * />\n * <Upload _value={files} action=\"/files\" /> 用应用配置的请求客户端(http)上传\n *\n * - value 是 UploadFile 的数组(文件、状态、进度、错误、upload 的结果),父级写 _value={list};\n * - 给了 upload 或 action 时选到文件就开始上传,否则停在 selected(等提交时一起上传);移除正在上传的文件会中止(signal);\n * - action:经 createUi / Provider 的 http 把文件 POST 过去(multipart,字段名 fieldName),与其他请求共用 baseURL、令牌与拆包;\n * fetch 拿不到上传的字节进度,进度条是不确定的,要进度时写 upload(XMLHttpRequest 的 upload.onprogress);\n * - 超过 maxSize、不在 accept 里的文件也列出来,带错误、不上传;单选(不写 multiple)时新文件替换旧的;\n * - 已有文件(之前上传过、从后端读回来的)用 existingUploadFile 放进 value:状态是 done,没有 File,\n * 给了 url 时文件名链到它(预览、下载),可以移除;\n *\n * let files = _state([existingUploadFile({ name: '验证报告.pdf', url: '/files/123' })]);\n * - 在 Field 里时关联标签与错误;写了 name 时有一个隐藏的 file input 带着全部文件,原生提交(FormData)也拿得到。\n *\n * - 图片墙(listType=\"picture\"):方形缩略图(选的文件用本地地址显示,不等上传),点开看大图(整面墙是一个 ImageGroup,\n * 大图里左右切换);上传中盖一层进度,失败标红;选文件的区域是墙上最后一格。sortable 时拖动把手(或聚焦后用键盘)调整顺序,\n * pasteable 时焦点在组件里按 Ctrl + V 把剪贴板里的图片加进来(截图、从别处复制的图)。\n *\n * 部件:Root(div)、Drop(FileDrop)、List(ul)、Item(li)、Icon(文件图标)、Body(文件名与下一行)、Name、Meta(大小或错误)、\n * Progress、Retry(重试按钮)、Remove(移除按钮)。\n */\nimport {\n CircleAlert,\n CircleCheck,\n File as FileIcon,\n GripVertical,\n ImagePlus,\n RotateCw,\n X,\n} from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { onBeforeUnmount, watch } from 'vue';\nimport { ButtonBase } from '../base/ButtonBase';\nimport { Image, ImageGroup } from '../display/Image';\nimport { useSortable } from '../utils/dom/sortable';\nimport { Icon, type IconProps } from '../base/Icon';\nimport { Link } from '../base/Link';\nimport { IconButton, type IconButtonProps } from '../base/IconButton';\nimport { VisuallyHidden } from '../base/VisuallyHidden';\nimport { useLang } from '../config/context';\nimport { useFormat } from '../config/lang/format';\nimport { useFieldControl } from '../form/Field';\nimport { useHttp } from '../utils/request/http';\nimport { Progress, type ProgressProps } from '../feedback/Progress';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { itemStyles } from '../utils/style/item-styles';\nimport { FileDrop, matchesAccept, type FileDropProps } from './FileDrop';\nimport { GridList, type GridProps } from '../layout/Grid';\n\n/** 列表里的一个文件 */\nexport interface UploadFile {\n /** 组件给的 id,同一个列表里唯一 */\n readonly id: string;\n /** 文件名 */\n readonly name: string;\n /** 大小(字节);已有文件不知道时为 undefined */\n readonly size?: number;\n /** 选的文件;已有文件没有 */\n readonly file?: File;\n /** 已有文件的地址:文件名链到它 */\n readonly url?: string;\n /** selected:选了、还没上传;uploading:上传中;done:完成;error:失败或没通过检查 */\n readonly status: 'selected' | 'uploading' | 'done' | 'error';\n /** 上传进度 0–1;upload 没报告进度时为 undefined(不确定进度) */\n readonly progress?: number;\n /** 错误信息 */\n readonly error?: string;\n /** 没通过大小或类型检查:不会上传,也不能重试 */\n readonly rejected?: boolean;\n /** upload 的返回值(如文件地址) */\n readonly response?: unknown;\n}\n\n/** 上传一个文件:报告进度、响应中止;成功时 resolve(值记在 response 上),失败时 reject */\nexport type UploadHandler = (\n file: File,\n context: { onProgress: (fraction: number) => void; signal: AbortSignal },\n) => Promise<unknown>;\n\n/** 列表的样子 */\nexport interface UploadListTypes {\n /** 一行一个文件:图标、文件名、大小或进度(默认) */\n text: unknown;\n /** 图片墙:方形缩略图,点开看大图 */\n picture: unknown;\n}\n\n/** 未声明的属性落到外层 div 上 */\nexport interface UploadProps extends PartProps<'div'> {\n /** 文件列表:父级写 _value={list} */\n _value?: UploadFile[];\n /** 怎样上传一个文件;不写时用 action,都没有时只选文件、不上传 */\n upload?: UploadHandler;\n /** 上传地址:没写 upload 时用应用配置的请求客户端(useHttp)POST 过去,响应体记在 response 上 */\n action?: string;\n /** action 上传时文件在表单里的字段名,默认 file */\n fieldName?: string;\n /** 接受的文件类型(同原生 accept) */\n accept?: string;\n /** 可以选多个;不写时新文件替换旧的 */\n multiple?: boolean;\n /** 单个文件的大小上限(字节) */\n maxSize?: number;\n /** 最多几个文件,超出的不加进来 */\n maxFiles?: number;\n /** 原生表单提交时的字段名:写了才有隐藏的 file input */\n name?: string;\n /** 不可用 */\n disabled?: boolean;\n /** 只读:只列出文件,不能再选、不能移除或重试 */\n readonly?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 必填 */\n required?: boolean;\n /** 选择区域里提示文字下面的说明(大小、类型限制) */\n description?: FileDropProps['description'];\n /** 列表的样子:text 一行一个文件(默认),picture 是图片墙 */\n listType?: keyof UploadListTypes;\n /** 图片墙里缩略图的边长(px),默认 104 */\n thumbnailSize?: number;\n /** 拖动把手(或聚焦后用键盘)调整文件的顺序(图片墙) */\n sortable?: boolean;\n /** 焦点在组件里时 Ctrl + V 把剪贴板里的图片、文件加进来 */\n pasteable?: boolean;\n /** 选择区域(FileDrop)的部件属性 */\n propsDrop?: Partial<FileDropProps>;\n /** 列表的部件属性 */\n propsList?: Partial<GridProps<'ul'>>;\n /** 每一项的部件属性,可以按文件计算 */\n propsItem?: PartProps<'li'> | ((file: UploadFile) => PartProps<'li'>);\n /** 每一项的文件图标(按状态是文件、对勾或警告)的部件属性 */\n propsIcon?: Partial<IconProps>;\n /** 每一项里文件名与下一行(大小、错误或进度)的外层的部件属性 */\n propsBody?: PartProps<'div'>;\n /** 每一项里文件名的部件属性 */\n propsName?: PartProps<'span'>;\n /** 每一项里文件大小或错误信息的部件属性 */\n propsMeta?: PartProps<'span'>;\n /** 每一项上传中的进度条的部件属性;进度与名称(文件名)由组件管理 */\n propsProgress?: Omit<Partial<ProgressProps>, 'value' | 'aria-label'>;\n /**\n * 每一项失败后的重试按钮的部件属性。名称(重新上传 文件名)每项不同,由组件按语言包生成,\n * 改文案请改语言包的 uploadRetry;点击由组件管理\n */\n propsRetry?: Omit<Partial<IconButtonProps>, 'aria-label' | 'onClick'>;\n /** 每一项的移除按钮的部件属性;名称(语言包的 uploadRemove)、点击与不可用由组件管理 */\n propsRemove?: Omit<Partial<IconButtonProps>, 'aria-label' | 'onClick' | 'disabled'>;\n}\n\n/** Upload 的样式:root、list、item、icon、body、name、meta、error、progress */\nexport const uploadStyles = css.create({\n root: { display: 'grid', gap: '_sm' },\n item: {\n cursor: 'default',\n gap: '_sm',\n borderWidth: '_xs',\n borderStyle: 'solid',\n borderColor: '_borderMuted',\n borderRadius: '_md',\n },\n icon: { flexShrink: 0, color: '_textMuted' },\n body: { display: 'grid', gap: '_xs', flexGrow: 1, minWidth: 0 },\n name: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },\n meta: { fontSize: '_sm', color: '_textMuted' },\n error: { color: '_dangerText' },\n done: { color: '_successText' },\n progress: { width: '_full' },\n // 图片墙:缩略图与“添加图片”一样大,排成一行、放不下换行\n wall: { display: 'flex', flexWrap: 'wrap', gap: '_sm' },\n wallList: { display: 'contents' },\n card: {\n position: 'relative',\n boxSizing: 'border-box',\n flexShrink: 0,\n borderWidth: '_xs',\n borderStyle: 'solid',\n borderColor: '_borderMuted',\n borderRadius: '_md',\n overflow: 'hidden',\n backgroundColor: '_fill',\n },\n cardError: { borderColor: '_danger' },\n // 选文件的那一格:同样大小,图标与文字居中\n addCard: { padding: '_xs', gap: '_xs', textAlign: 'center', fontSize: '_sm' },\n // 上传中、失败:盖在缩略图上\n overlay: {\n position: 'absolute',\n insetInline: 0,\n bottom: 0,\n display: 'flex',\n flexDirection: 'column',\n gap: '_xs',\n padding: '_xs',\n backgroundColor: 'rgb(0 0 0 / 0.55)',\n color: 'white',\n fontSize: '_xs',\n },\n placeholder: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: '_xs',\n width: '_full',\n height: '_full',\n padding: '_xs',\n boxSizing: 'border-box',\n color: '_textMuted',\n fontSize: '_xs',\n textAlign: 'center',\n overflowWrap: 'anywhere',\n },\n // 右上角的把手、重试、移除:浅色底上的小按钮\n actions: {\n position: 'absolute',\n top: '_xs',\n insetInlineEnd: '_xs',\n display: 'flex',\n gap: '2px',\n },\n action: { backgroundColor: { default: 'rgb(255 255 255 / 0.85)', ':hover': 'white' } },\n handle: {\n display: 'grid',\n placeItems: 'center',\n width: '1.75em',\n height: '1.75em',\n borderRadius: '_sm',\n color: '_text',\n backgroundColor: 'rgb(255 255 255 / 0.85)',\n cursor: { default: 'grab', ':active': 'grabbing' },\n touchAction: 'none',\n },\n});\n\n/** Upload 的动态样式:缩略图的边长 */\nexport const uploadDynamicStyles = css.create({\n square: (size: number) => ({ width: `${size}px`, height: `${size}px` }),\n});\n\nlet nextId = 0;\n\n/** 已有文件(之前上传过、从后端读回来的):放进 Upload 的 value,状态是 done */\nexport function existingUploadFile(file: {\n /** 标识;不写时生成一个 */\n id?: string;\n /** 文件名 */\n name: string;\n /** 大小(字节) */\n size?: number;\n /** 地址:文件名链到它 */\n url?: string;\n}): UploadFile {\n return { ...file, id: file.id ?? `upload-${++nextId}`, status: 'done', response: file.url };\n}\n\n/** 上传:选文件、列出文件与进度、失败重试、移除 */\nexport const Upload = _component(\n ({\n _value: value = _bindable<UploadFile[]>([]),\n upload,\n action,\n fieldName = 'file',\n accept,\n multiple = false,\n maxSize,\n maxFiles,\n name,\n disabled,\n readonly,\n invalid,\n required,\n description,\n listType = 'text',\n thumbnailSize = 104,\n sortable = false,\n pasteable = false,\n propsDrop,\n propsList,\n propsItem,\n propsIcon,\n propsBody,\n propsName,\n propsMeta,\n propsProgress,\n propsRetry,\n propsRemove,\n ...rest\n }: UploadProps) => {\n const lang = useLang();\n const format = useFormat();\n // delegate:里面的 FileDrop 照常从 Field 拿 id、说明与标签\n const control = useFieldControl(() => ({ invalid, required, disabled, readonly }), {\n delegate: true,\n });\n const http = useHttp();\n /** 实际用的上传函数:upload 优先,其次按 action 用请求客户端发 */\n const uploader = (): UploadHandler | undefined =>\n upload ??\n (action\n ? (file, { signal }) => {\n const body = new FormData();\n body.append(fieldName, file);\n return http.request({ method: 'POST', url: action, body, signal });\n }\n : undefined);\n let hidden = _state.raw<HTMLInputElement | null>(null);\n // 每个正在上传的文件一个:移除、重试、卸载时中止\n const controllers = new Map<string, AbortController>();\n onBeforeUnmount(() => controllers.forEach((controller) => controller.abort()));\n\n const patch = (id: string, changes: Partial<UploadFile>) =>\n (value = value.map((item) => (item.id === id ? { ...item, ...changes } : item)));\n\n async function start(item: UploadFile) {\n const send = uploader();\n if (!send || !item.file) return;\n controllers.get(item.id)?.abort();\n const controller = new AbortController();\n controllers.set(item.id, controller);\n patch(item.id, { status: 'uploading', progress: undefined, error: undefined });\n try {\n const response = await send(item.file!, {\n onProgress: (fraction) => {\n if (!controller.signal.aborted) patch(item.id, { progress: fraction });\n },\n signal: controller.signal,\n });\n if (!controller.signal.aborted) patch(item.id, { status: 'done', progress: 1, response });\n } catch (error) {\n if (controller.signal.aborted) return;\n const message = error instanceof Error && error.message ? error.message : lang.uploadFailed;\n patch(item.id, { status: 'error', error: message });\n } finally {\n if (controllers.get(item.id) === controller) controllers.delete(item.id);\n }\n }\n\n function remove(id: string) {\n controllers.get(id)?.abort();\n controllers.delete(id);\n value = value.filter((item) => item.id !== id);\n }\n\n function add(files: File[], rejectedType = false) {\n const entries = files.map((file): UploadFile => {\n const base = { id: `upload-${++nextId}`, name: file.name, size: file.size, file };\n if (rejectedType) {\n return { ...base, status: 'error', error: lang.fileTypeRejected, rejected: true };\n }\n if (maxSize != null && file.size > maxSize) {\n const error = lang.fileTooLarge(format.fileSize(maxSize));\n return { ...base, status: 'error', error, rejected: true };\n }\n return { ...base, status: 'selected' };\n });\n if (!multiple) value.forEach((item) => controllers.get(item.id)?.abort());\n const kept = multiple ? value : [];\n const room = maxFiles == null ? entries.length : Math.max(0, maxFiles - kept.length);\n const added = (multiple ? entries : entries.slice(-1)).slice(0, room);\n value = [...kept, ...added];\n added.filter((item) => item.status === 'selected').forEach((item) => void start(item));\n }\n\n // 图片墙里选的文件的本地地址:用完(移除、卸载)回收\n const previews = new Map<string, string>();\n const previewOf = (item: UploadFile) => {\n if (item.url) return item.url;\n if (!item.file || !item.file.type.startsWith('image/')) return undefined;\n let url = previews.get(item.id);\n if (!url) previews.set(item.id, (url = URL.createObjectURL(item.file)));\n return url;\n };\n watch(\n () => value,\n (list) => {\n const kept = new Set(list.map((item) => item.id));\n for (const [id, url] of previews)\n if (!kept.has(id)) {\n URL.revokeObjectURL(url);\n previews.delete(id);\n }\n },\n );\n onBeforeUnmount(() => previews.forEach((url) => URL.revokeObjectURL(url)));\n\n let wall = _state.raw<HTMLElement | null>(null);\n const sortableWall = useSortable<UploadFile>({\n items: () => value,\n getKey: (item) => item.id,\n getLabel: (item) => item.name,\n onChange: (next) => (value = next),\n container: () => wall,\n orientation: () => 'horizontal',\n disabled: () => !sortable || control().readonly || control().disabled,\n });\n\n /** 粘贴:剪贴板里的文件(截图是 image/png)按 accept 分开,不符合的照常列出、带错误 */\n function onPaste(event: ClipboardEvent) {\n const state = control();\n if (!pasteable || state.disabled || state.readonly) return;\n const files = [...(event.clipboardData?.files ?? [])];\n if (files.length === 0) return;\n event.preventDefault();\n const accepted = files.filter((file) => matchesAccept(file, accept));\n const rejected = files.filter((file) => !matchesAccept(file, accept));\n if (accepted.length) add(accepted);\n if (rejected.length) add(rejected, true);\n }\n\n // 隐藏的 file input 带着全部可提交的文件:原生提交、FormData 拿得到\n _effect(() => {\n if (!hidden || typeof DataTransfer === 'undefined') return;\n const transfer = new DataTransfer();\n for (const item of value) if (item.file && !item.rejected) transfer.items.add(item.file);\n hidden.files = transfer.files;\n });\n\n function renderItem(item: UploadFile) {\n const state = control();\n const own = typeof propsItem === 'function' ? propsItem(item) : propsItem;\n const fileName = item.name;\n const failed = item.status === 'error';\n return (\n <li\n key={item.id}\n {...mergeAttrs<'li'>({ sx: [itemStyles.row, uploadStyles.item] }, own)}\n data-state={item.status}\n >\n <Icon\n {...mergePartProps<IconProps>(\n {\n icon: failed ? CircleAlert : item.status === 'done' ? CircleCheck : FileIcon,\n sx: [\n uploadStyles.icon,\n failed && uploadStyles.error,\n item.status === 'done' && uploadStyles.done,\n ],\n },\n propsIcon,\n )}\n />\n <div {...mergeAttrs<'div'>({ sx: uploadStyles.body }, propsBody)}>\n <span {...mergeAttrs<'span'>({ sx: uploadStyles.name }, propsName)}>\n {item.url ? (\n <Link href={item.url} target=\"_blank\">\n {fileName}\n </Link>\n ) : (\n fileName\n )}\n </span>\n {item.status === 'uploading' ? (\n <Progress\n {...mergePartProps<ProgressProps>(\n { size: 'xs', sx: uploadStyles.progress },\n propsProgress,\n {\n value: item.progress == null ? null : Math.round(item.progress * 100),\n 'aria-label': fileName,\n },\n )}\n />\n ) : (\n <span\n {...mergeAttrs<'span'>(\n { sx: [uploadStyles.meta, failed && uploadStyles.error] },\n propsMeta,\n )}\n >\n {failed ? item.error : item.size != null && format.fileSize(item.size)}\n {item.status === 'done' && <VisuallyHidden> {lang.uploadDone}</VisuallyHidden>}\n </span>\n )}\n </div>\n {failed && uploader() && !item.rejected && !state.readonly && (\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: RotateCw,\n variant: 'ghost',\n size: 'sm',\n // 名称带文件名、每项不同:类型里不让使用方覆盖(同一个值会让各项的按钮同名)\n 'aria-label': lang.uploadRetry(fileName),\n },\n propsRetry,\n { onClick: () => void start(item) },\n )}\n />\n )}\n {!state.readonly && (\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: X,\n variant: 'ghost',\n size: 'sm',\n 'aria-label': lang.uploadRemove(fileName),\n },\n propsRemove,\n { disabled: state.disabled, onClick: () => remove(item.id) },\n )}\n />\n )}\n </li>\n );\n }\n\n /** 图片墙的一格:缩略图(点开看大图)、上传进度或错误、把手、重试与移除 */\n function renderCard(item: UploadFile) {\n const state = control();\n const failed = item.status === 'error';\n const src = previewOf(item);\n return (\n <div\n key={item.id}\n {...mergeAttrs<'div'>(\n {\n sx: [\n uploadStyles.card,\n uploadDynamicStyles.square(thumbnailSize),\n failed && uploadStyles.cardError,\n ],\n },\n sortableWall.itemProps(item.id),\n { role: 'listitem' },\n )}\n data-state={item.status}\n >\n {src && !item.rejected ? (\n <Image\n src={src}\n alt={item.name}\n width={thumbnailSize - 2}\n height={thumbnailSize - 2}\n radius=\"none\"\n preview={{ caption: item.name, download: item.name }}\n />\n ) : (\n <div {...css.attrs(uploadStyles.placeholder)}>\n <Icon icon={failed ? CircleAlert : FileIcon} sx={failed && uploadStyles.error} />\n {item.name}\n </div>\n )}\n {item.status === 'uploading' && (\n <div {...css.attrs(uploadStyles.overlay)}>\n <Progress\n {...mergePartProps<ProgressProps>({ size: 'xs' }, propsProgress, {\n value: item.progress == null ? null : Math.round(item.progress * 100),\n 'aria-label': item.name,\n })}\n />\n </div>\n )}\n {failed && src && !item.rejected && (\n <div {...css.attrs(uploadStyles.overlay)} title={item.error}>\n {item.error}\n </div>\n )}\n {item.status === 'done' && <VisuallyHidden>{lang.uploadDone}</VisuallyHidden>}\n {!state.readonly && (\n <div {...css.attrs(uploadStyles.actions)}>\n {sortable && (\n <ButtonBase\n sx={uploadStyles.handle}\n ripple={false}\n disabled={state.disabled}\n aria-label={lang.sortableHandle(item.name)}\n {...sortableWall.handleProps(item.id)}\n >\n <Icon icon={GripVertical} />\n </ButtonBase>\n )}\n {failed && uploader() && !item.rejected && (\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: RotateCw,\n variant: 'ghost',\n size: 'xs',\n sx: uploadStyles.action,\n 'aria-label': lang.uploadRetry(item.name),\n },\n propsRetry,\n { onClick: () => void start(item) },\n )}\n />\n )}\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: X,\n variant: 'ghost',\n size: 'xs',\n sx: uploadStyles.action,\n 'aria-label': lang.uploadRemove(item.name),\n },\n propsRemove,\n { disabled: state.disabled, onClick: () => remove(item.id) },\n )}\n />\n </div>\n )}\n </div>\n );\n }\n\n /** 选文件的区域:列表模式在上方,图片墙模式是最后一格 */\n function renderDrop(picture: boolean) {\n const state = control();\n const full = maxFiles != null && multiple && value.length >= maxFiles;\n if (state.readonly || (picture && full)) return null;\n const hint = pasteable ? lang.uploadPasteHint : undefined;\n const props = mergePartProps<FileDropProps>(\n {\n accept,\n multiple,\n disabled: state.disabled,\n invalid: state.invalid,\n // 选好的文件不在这个输入框里(它选完就清空):已有文件时不再原生必填,免得表单交不了\n required: state.required && value.length === 0,\n description: picture\n ? undefined\n : hint\n ? [description, description != null && ' · ', hint]\n : description,\n propsRoot: picture\n ? { sx: [uploadStyles.addCard, uploadDynamicStyles.square(thumbnailSize)] }\n : undefined,\n },\n propsDrop,\n {\n onFiles: (files: File[]) => add(files),\n onReject: (files: File[]) => add(files, true),\n },\n );\n // 列表模式不传子元素:FileDrop 用默认的图标、提示与说明(写了 undefined 也算传了,会把默认内容换掉)\n if (!picture) return <FileDrop {...props} />;\n return (\n <FileDrop {...props}>\n <Icon icon={ImagePlus} />\n {lang.uploadAddImage}\n </FileDrop>\n );\n }\n\n return () => {\n const state = control();\n if (listType === 'picture')\n return (\n <div {...mergeAttrs<'div'>({ sx: uploadStyles.root }, rest, { onPaste })}>\n <ImageGroup sx={uploadStyles.wall}>\n {/* 列表里只有缩略图(“添加图片”与预览不是列表项);display: contents,缩略图仍和“添加图片”排在一行 */}\n <div\n ref={_toRef(wall)}\n role=\"list\"\n // 状态变化(失败、完成)由读屏播报\n aria-live=\"polite\"\n {...css.attrs(uploadStyles.wallList)}\n >\n {value.map(renderCard)}\n </div>\n {renderDrop(true)}\n </ImageGroup>\n {pasteable && !state.readonly && (\n <span {...css.attrs(uploadStyles.meta)}>\n {[description, description != null && ' · ', lang.uploadPasteHint]}\n </span>\n )}\n {sortable && (\n <>\n <VisuallyHidden id={sortableWall.instructionsId}>\n {lang.sortableInstructions}\n </VisuallyHidden>\n <VisuallyHidden aria-live=\"assertive\">{sortableWall.announcement}</VisuallyHidden>\n </>\n )}\n {name != null && (\n <input\n ref={_toRef(hidden)}\n type=\"file\"\n name={name}\n multiple\n hidden\n tabindex={-1}\n aria-hidden=\"true\"\n />\n )}\n </div>\n );\n return (\n <div {...mergeAttrs<'div'>({ sx: uploadStyles.root }, rest, { onPaste })}>\n {renderDrop(false)}\n {name != null && (\n <input\n ref={_toRef(hidden)}\n type=\"file\"\n name={name}\n multiple\n hidden\n tabindex={-1}\n aria-hidden=\"true\"\n />\n )}\n {value.length > 0 && (\n // 状态变化(失败、完成)由读屏播报\n <GridList\n {...mergePartProps<GridProps<'ul'>>({ gap: 'xs' }, propsList)}\n aria-live=\"polite\"\n >\n {value.map(renderItem)}\n </GridList>\n )}\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Upload.js","names":["_isVNode","Upload","_vtp_bindable","_vtp_readonlyProp","_vtp_restProps","_vue_shallowRef","_createVNode","_mergeProps","FileIcon","_Fragment"],"sources":["../../src/input/Upload.tsx"],"sourcesContent":["/**\n * 上传(div):FileDrop 选文件,下面列出文件、上传进度、失败重试与移除。只负责界面与状态,怎样上传由 upload 决定:\n *\n * <Upload\n * _value={files}\n * multiple\n * accept=\"image/*\"\n * maxSize={5_000_000}\n * upload={(file, { onProgress, signal }) => api.upload(file, { onProgress, signal })}\n * />\n * <Upload _value={files} action=\"/files\" /> 用应用配置的请求客户端(http)上传\n *\n * - value 是 UploadFile 的数组(文件、状态、进度、错误、upload 的结果),父级写 _value={list};\n * - 给了 upload 或 action 时选到文件就开始上传,否则停在 selected(等提交时一起上传);移除正在上传的文件会中止(signal);\n * - action:经 createUi / Provider 的 http 把文件 POST 过去(multipart,字段名 fieldName),与其他请求共用 baseURL、令牌与拆包;\n * fetch 拿不到上传的字节进度,进度条是不确定的,要进度时写 upload(XMLHttpRequest 的 upload.onprogress);\n * - 超过 maxSize、不在 accept 里的文件也列出来,带错误、不上传;单选(不写 multiple)时新文件替换旧的;\n * - 已有文件(之前上传过、从后端读回来的)用 existingUploadFile 放进 value:状态是 done,没有 File,\n * 给了 url 时文件名链到它(预览、下载),可以移除;\n *\n * let files = _state([existingUploadFile({ name: '验证报告.pdf', url: '/files/123' })]);\n * - 在 Field 里时关联标签与错误;写了 name 时有一个隐藏的 file input 带着全部文件,原生提交(FormData)也拿得到。\n *\n * - 图片墙(listType=\"picture\"):方形缩略图(选的文件用本地地址显示,不等上传),点开看大图(整面墙是一个 ImageGroup,\n * 大图里左右切换);上传中盖一层进度,失败标红;选文件的区域是墙上最后一格。sortable 时拖动把手(或聚焦后用键盘)调整顺序,\n * pasteable 时焦点在组件里按 Ctrl + V 把剪贴板里的图片加进来(截图、从别处复制的图)。\n *\n * 部件:Root(div)、Drop(FileDrop)、List(ul)、Item(li)、Icon(文件图标)、Body(文件名与下一行)、Name、Meta(大小或错误)、\n * Progress、Retry(重试按钮)、Remove(移除按钮)。\n */\nimport {\n CircleAlert,\n CircleCheck,\n File as FileIcon,\n GripVertical,\n ImagePlus,\n RotateCw,\n X,\n} from '@lucide/icons';\nimport { _component } from '@zerodep-dev/vue-tsx-pro-tsx';\nimport * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';\nimport { onBeforeUnmount, watch } from 'vue';\nimport { ButtonBase } from '../base/ButtonBase';\nimport { Image, ImageGroup } from '../display/Image';\nimport { useSortable } from '../utils/dom/sortable';\nimport { Icon, type IconProps } from '../base/Icon';\nimport { Link } from '../base/Link';\nimport { IconButton, type IconButtonProps } from '../base/IconButton';\nimport { VisuallyHidden } from '../base/VisuallyHidden';\nimport { useLang } from '../config/context';\nimport { useFormat } from '../config/lang/format';\nimport { useFieldControl } from '../form/Field';\nimport { useHttp } from '../utils/request/http';\nimport { errorCodeText } from '../utils/request/error-message';\nimport { Progress, type ProgressProps } from '../feedback/Progress';\nimport { mergeAttrs, mergePartProps, type PartProps } from '../utils/props';\nimport { itemStyles } from '../utils/style/item-styles';\nimport { FileDrop, matchesAccept, type FileDropProps } from './FileDrop';\nimport { GridList, type GridProps } from '../layout/Grid';\n\n/** 列表里的一个文件 */\nexport interface UploadFile {\n /** 组件给的 id,同一个列表里唯一 */\n readonly id: string;\n /** 文件名 */\n readonly name: string;\n /** 大小(字节);已有文件不知道时为 undefined */\n readonly size?: number;\n /** 选的文件;已有文件没有 */\n readonly file?: File;\n /** 已有文件的地址:文件名链到它 */\n readonly url?: string;\n /** selected:选了、还没上传;uploading:上传中;done:完成;error:失败或没通过检查 */\n readonly status: 'selected' | 'uploading' | 'done' | 'error';\n /** 上传进度 0–1;upload 没报告进度时为 undefined(不确定进度) */\n readonly progress?: number;\n /** 错误信息 */\n readonly error?: string;\n /** 没通过大小或类型检查:不会上传,也不能重试 */\n readonly rejected?: boolean;\n /** upload 的返回值(如文件地址) */\n readonly response?: unknown;\n}\n\n/** 上传一个文件:报告进度、响应中止;成功时 resolve(值记在 response 上),失败时 reject */\nexport type UploadHandler = (\n file: File,\n context: { onProgress: (fraction: number) => void; signal: AbortSignal },\n) => Promise<unknown>;\n\n/** 列表的样子 */\nexport interface UploadListTypes {\n /** 一行一个文件:图标、文件名、大小或进度(默认) */\n text: unknown;\n /** 图片墙:方形缩略图,点开看大图 */\n picture: unknown;\n}\n\n/** 未声明的属性落到外层 div 上 */\nexport interface UploadProps extends PartProps<'div'> {\n /** 文件列表:父级写 _value={list} */\n _value?: UploadFile[];\n /** 怎样上传一个文件;不写时用 action,都没有时只选文件、不上传 */\n upload?: UploadHandler;\n /** 上传地址:没写 upload 时用应用配置的请求客户端(useHttp)POST 过去,响应体记在 response 上 */\n action?: string;\n /** action 上传时文件在表单里的字段名,默认 file */\n fieldName?: string;\n /** 接受的文件类型(同原生 accept) */\n accept?: string;\n /** 可以选多个;不写时新文件替换旧的 */\n multiple?: boolean;\n /** 单个文件的大小上限(字节) */\n maxSize?: number;\n /** 最多几个文件,超出的不加进来 */\n maxFiles?: number;\n /** 原生表单提交时的字段名:写了才有隐藏的 file input */\n name?: string;\n /** 不可用 */\n disabled?: boolean;\n /** 只读:只列出文件,不能再选、不能移除或重试 */\n readonly?: boolean;\n /** 校验未通过 */\n invalid?: boolean;\n /** 必填 */\n required?: boolean;\n /** 选择区域里提示文字下面的说明(大小、类型限制) */\n description?: FileDropProps['description'];\n /** 列表的样子:text 一行一个文件(默认),picture 是图片墙 */\n listType?: keyof UploadListTypes;\n /** 图片墙里缩略图的边长(px),默认 104 */\n thumbnailSize?: number;\n /** 拖动把手(或聚焦后用键盘)调整文件的顺序(图片墙) */\n sortable?: boolean;\n /** 焦点在组件里时 Ctrl + V 把剪贴板里的图片、文件加进来 */\n pasteable?: boolean;\n /** 选择区域(FileDrop)的部件属性 */\n propsDrop?: Partial<FileDropProps>;\n /** 列表的部件属性 */\n propsList?: Partial<GridProps<'ul'>>;\n /** 每一项的部件属性,可以按文件计算 */\n propsItem?: PartProps<'li'> | ((file: UploadFile) => PartProps<'li'>);\n /** 每一项的文件图标(按状态是文件、对勾或警告)的部件属性 */\n propsIcon?: Partial<IconProps>;\n /** 每一项里文件名与下一行(大小、错误或进度)的外层的部件属性 */\n propsBody?: PartProps<'div'>;\n /** 每一项里文件名的部件属性 */\n propsName?: PartProps<'span'>;\n /** 每一项里文件大小或错误信息的部件属性 */\n propsMeta?: PartProps<'span'>;\n /** 每一项上传中的进度条的部件属性;进度与名称(文件名)由组件管理 */\n propsProgress?: Omit<Partial<ProgressProps>, 'value' | 'aria-label'>;\n /**\n * 每一项失败后的重试按钮的部件属性。名称(重新上传 文件名)每项不同,由组件按语言包生成,\n * 改文案请改语言包的 uploadRetry;点击由组件管理\n */\n propsRetry?: Omit<Partial<IconButtonProps>, 'aria-label' | 'onClick'>;\n /** 每一项的移除按钮的部件属性;名称(语言包的 uploadRemove)、点击与不可用由组件管理 */\n propsRemove?: Omit<Partial<IconButtonProps>, 'aria-label' | 'onClick' | 'disabled'>;\n}\n\n/** Upload 的样式:root、list、item、icon、body、name、meta、error、progress */\nexport const uploadStyles = css.create({\n root: { display: 'grid', gap: '_sm' },\n item: {\n cursor: 'default',\n gap: '_sm',\n borderWidth: '_xs',\n borderStyle: 'solid',\n borderColor: '_borderMuted',\n borderRadius: '_md',\n },\n icon: { flexShrink: 0, color: '_textMuted' },\n body: { display: 'grid', gap: '_xs', flexGrow: 1, minWidth: 0 },\n name: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },\n meta: { fontSize: '_sm', color: '_textMuted' },\n error: { color: '_dangerText' },\n done: { color: '_successText' },\n progress: { width: '_full' },\n // 图片墙:缩略图与“添加图片”一样大,排成一行、放不下换行\n wall: { display: 'flex', flexWrap: 'wrap', gap: '_sm' },\n wallList: { display: 'contents' },\n card: {\n position: 'relative',\n boxSizing: 'border-box',\n flexShrink: 0,\n borderWidth: '_xs',\n borderStyle: 'solid',\n borderColor: '_borderMuted',\n borderRadius: '_md',\n overflow: 'hidden',\n backgroundColor: '_fill',\n },\n cardError: { borderColor: '_danger' },\n // 选文件的那一格:同样大小,图标与文字居中\n addCard: { padding: '_xs', gap: '_xs', textAlign: 'center', fontSize: '_sm' },\n // 上传中、失败:盖在缩略图上\n overlay: {\n position: 'absolute',\n insetInline: 0,\n bottom: 0,\n display: 'flex',\n flexDirection: 'column',\n gap: '_xs',\n padding: '_xs',\n backgroundColor: 'rgb(0 0 0 / 0.55)',\n color: 'white',\n fontSize: '_xs',\n },\n placeholder: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'center',\n justifyContent: 'center',\n gap: '_xs',\n width: '_full',\n height: '_full',\n padding: '_xs',\n boxSizing: 'border-box',\n color: '_textMuted',\n fontSize: '_xs',\n textAlign: 'center',\n overflowWrap: 'anywhere',\n },\n // 右上角的把手、重试、移除:浅色底上的小按钮\n actions: {\n position: 'absolute',\n top: '_xs',\n insetInlineEnd: '_xs',\n display: 'flex',\n gap: '2px',\n },\n action: { backgroundColor: { default: 'rgb(255 255 255 / 0.85)', ':hover': 'white' } },\n handle: {\n display: 'grid',\n placeItems: 'center',\n width: '1.75em',\n height: '1.75em',\n borderRadius: '_sm',\n color: '_text',\n backgroundColor: 'rgb(255 255 255 / 0.85)',\n cursor: { default: 'grab', ':active': 'grabbing' },\n touchAction: 'none',\n },\n});\n\n/** Upload 的动态样式:缩略图的边长 */\nexport const uploadDynamicStyles = css.create({\n square: (size: number) => ({ width: `${size}px`, height: `${size}px` }),\n});\n\nlet nextId = 0;\n\n/** 已有文件(之前上传过、从后端读回来的):放进 Upload 的 value,状态是 done */\nexport function existingUploadFile(file: {\n /** 标识;不写时生成一个 */\n id?: string;\n /** 文件名 */\n name: string;\n /** 大小(字节) */\n size?: number;\n /** 地址:文件名链到它 */\n url?: string;\n}): UploadFile {\n return { ...file, id: file.id ?? `upload-${++nextId}`, status: 'done', response: file.url };\n}\n\n/** 上传:选文件、列出文件与进度、失败重试、移除 */\nexport const Upload = _component(\n ({\n _value: value = _bindable<UploadFile[]>([]),\n upload,\n action,\n fieldName = 'file',\n accept,\n multiple = false,\n maxSize,\n maxFiles,\n name,\n disabled,\n readonly,\n invalid,\n required,\n description,\n listType = 'text',\n thumbnailSize = 104,\n sortable = false,\n pasteable = false,\n propsDrop,\n propsList,\n propsItem,\n propsIcon,\n propsBody,\n propsName,\n propsMeta,\n propsProgress,\n propsRetry,\n propsRemove,\n ...rest\n }: UploadProps) => {\n const lang = useLang();\n const format = useFormat();\n // delegate:里面的 FileDrop 照常从 Field 拿 id、说明与标签\n const control = useFieldControl(() => ({ invalid, required, disabled, readonly }), {\n delegate: true,\n });\n const http = useHttp();\n /** 实际用的上传函数:upload 优先,其次按 action 用请求客户端发 */\n const uploader = (): UploadHandler | undefined =>\n upload ??\n (action\n ? (file, { signal }) => {\n const body = new FormData();\n body.append(fieldName, file);\n return http.request({ method: 'POST', url: action, body, signal });\n }\n : undefined);\n let hidden = _state.raw<HTMLInputElement | null>(null);\n // 每个正在上传的文件一个:移除、重试、卸载时中止\n const controllers = new Map<string, AbortController>();\n onBeforeUnmount(() => controllers.forEach((controller) => controller.abort()));\n\n const patch = (id: string, changes: Partial<UploadFile>) =>\n (value = value.map((item) => (item.id === id ? { ...item, ...changes } : item)));\n\n async function start(item: UploadFile) {\n const send = uploader();\n if (!send || !item.file) return;\n controllers.get(item.id)?.abort();\n const controller = new AbortController();\n controllers.set(item.id, controller);\n patch(item.id, { status: 'uploading', progress: undefined, error: undefined });\n try {\n const response = await send(item.file!, {\n onProgress: (fraction) => {\n if (!controller.signal.aborted) patch(item.id, { progress: fraction });\n },\n signal: controller.signal,\n });\n if (!controller.signal.aborted) patch(item.id, { status: 'done', progress: 1, response });\n } catch (error) {\n if (controller.signal.aborted) return;\n // 码有文案时按码(网络断了、后端拒绝),否则是上传函数自己的说明,再没有是笼统的“上传失败”\n const message =\n errorCodeText(error, lang) ??\n (error instanceof Error && error.message ? error.message : lang.uploadFailed);\n patch(item.id, { status: 'error', error: message });\n } finally {\n if (controllers.get(item.id) === controller) controllers.delete(item.id);\n }\n }\n\n function remove(id: string) {\n controllers.get(id)?.abort();\n controllers.delete(id);\n value = value.filter((item) => item.id !== id);\n }\n\n function add(files: File[], rejectedType = false) {\n const entries = files.map((file): UploadFile => {\n const base = { id: `upload-${++nextId}`, name: file.name, size: file.size, file };\n if (rejectedType) {\n return { ...base, status: 'error', error: lang.fileTypeRejected, rejected: true };\n }\n if (maxSize != null && file.size > maxSize) {\n const error = lang.fileTooLarge(format.fileSize(maxSize));\n return { ...base, status: 'error', error, rejected: true };\n }\n return { ...base, status: 'selected' };\n });\n if (!multiple) value.forEach((item) => controllers.get(item.id)?.abort());\n const kept = multiple ? value : [];\n const room = maxFiles == null ? entries.length : Math.max(0, maxFiles - kept.length);\n const added = (multiple ? entries : entries.slice(-1)).slice(0, room);\n value = [...kept, ...added];\n added.filter((item) => item.status === 'selected').forEach((item) => void start(item));\n }\n\n // 图片墙里选的文件的本地地址:用完(移除、卸载)回收\n const previews = new Map<string, string>();\n const previewOf = (item: UploadFile) => {\n if (item.url) return item.url;\n if (!item.file || !item.file.type.startsWith('image/')) return undefined;\n let url = previews.get(item.id);\n if (!url) previews.set(item.id, (url = URL.createObjectURL(item.file)));\n return url;\n };\n watch(\n () => value,\n (list) => {\n const kept = new Set(list.map((item) => item.id));\n for (const [id, url] of previews)\n if (!kept.has(id)) {\n URL.revokeObjectURL(url);\n previews.delete(id);\n }\n },\n );\n onBeforeUnmount(() => previews.forEach((url) => URL.revokeObjectURL(url)));\n\n let wall = _state.raw<HTMLElement | null>(null);\n const sortableWall = useSortable<UploadFile>({\n items: () => value,\n getKey: (item) => item.id,\n getLabel: (item) => item.name,\n onChange: (next) => (value = next),\n container: () => wall,\n orientation: () => 'horizontal',\n disabled: () => !sortable || control().readonly || control().disabled,\n });\n\n /** 粘贴:剪贴板里的文件(截图是 image/png)按 accept 分开,不符合的照常列出、带错误 */\n function onPaste(event: ClipboardEvent) {\n const state = control();\n if (!pasteable || state.disabled || state.readonly) return;\n const files = [...(event.clipboardData?.files ?? [])];\n if (files.length === 0) return;\n event.preventDefault();\n const accepted = files.filter((file) => matchesAccept(file, accept));\n const rejected = files.filter((file) => !matchesAccept(file, accept));\n if (accepted.length) add(accepted);\n if (rejected.length) add(rejected, true);\n }\n\n // 隐藏的 file input 带着全部可提交的文件:原生提交、FormData 拿得到\n _effect(() => {\n if (!hidden || typeof DataTransfer === 'undefined') return;\n const transfer = new DataTransfer();\n for (const item of value) if (item.file && !item.rejected) transfer.items.add(item.file);\n hidden.files = transfer.files;\n });\n\n function renderItem(item: UploadFile) {\n const state = control();\n const own = typeof propsItem === 'function' ? propsItem(item) : propsItem;\n const fileName = item.name;\n const failed = item.status === 'error';\n return (\n <li\n key={item.id}\n {...mergeAttrs<'li'>({ sx: [itemStyles.row, uploadStyles.item] }, own)}\n data-state={item.status}\n >\n <Icon\n {...mergePartProps<IconProps>(\n {\n icon: failed ? CircleAlert : item.status === 'done' ? CircleCheck : FileIcon,\n sx: [\n uploadStyles.icon,\n failed && uploadStyles.error,\n item.status === 'done' && uploadStyles.done,\n ],\n },\n propsIcon,\n )}\n />\n <div {...mergeAttrs<'div'>({ sx: uploadStyles.body }, propsBody)}>\n <span {...mergeAttrs<'span'>({ sx: uploadStyles.name }, propsName)}>\n {item.url ? (\n <Link href={item.url} target=\"_blank\">\n {fileName}\n </Link>\n ) : (\n fileName\n )}\n </span>\n {item.status === 'uploading' ? (\n <Progress\n {...mergePartProps<ProgressProps>(\n { size: 'xs', sx: uploadStyles.progress },\n propsProgress,\n {\n value: item.progress == null ? null : Math.round(item.progress * 100),\n 'aria-label': fileName,\n },\n )}\n />\n ) : (\n <span\n {...mergeAttrs<'span'>(\n { sx: [uploadStyles.meta, failed && uploadStyles.error] },\n propsMeta,\n )}\n >\n {failed ? item.error : item.size != null && format.fileSize(item.size)}\n {item.status === 'done' && <VisuallyHidden> {lang.uploadDone}</VisuallyHidden>}\n </span>\n )}\n </div>\n {failed && uploader() && !item.rejected && !state.readonly && (\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: RotateCw,\n variant: 'ghost',\n size: 'sm',\n // 名称带文件名、每项不同:类型里不让使用方覆盖(同一个值会让各项的按钮同名)\n 'aria-label': lang.uploadRetry(fileName),\n },\n propsRetry,\n { onClick: () => void start(item) },\n )}\n />\n )}\n {!state.readonly && (\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: X,\n variant: 'ghost',\n size: 'sm',\n 'aria-label': lang.uploadRemove(fileName),\n },\n propsRemove,\n { disabled: state.disabled, onClick: () => remove(item.id) },\n )}\n />\n )}\n </li>\n );\n }\n\n /** 图片墙的一格:缩略图(点开看大图)、上传进度或错误、把手、重试与移除 */\n function renderCard(item: UploadFile) {\n const state = control();\n const failed = item.status === 'error';\n const src = previewOf(item);\n return (\n <div\n key={item.id}\n {...mergeAttrs<'div'>(\n {\n sx: [\n uploadStyles.card,\n uploadDynamicStyles.square(thumbnailSize),\n failed && uploadStyles.cardError,\n ],\n },\n sortableWall.itemProps(item.id),\n { role: 'listitem' },\n )}\n data-state={item.status}\n >\n {src && !item.rejected ? (\n <Image\n src={src}\n alt={item.name}\n width={thumbnailSize - 2}\n height={thumbnailSize - 2}\n radius=\"none\"\n preview={{ caption: item.name, download: item.name }}\n />\n ) : (\n <div {...css.attrs(uploadStyles.placeholder)}>\n <Icon icon={failed ? CircleAlert : FileIcon} sx={failed && uploadStyles.error} />\n {item.name}\n </div>\n )}\n {item.status === 'uploading' && (\n <div {...css.attrs(uploadStyles.overlay)}>\n <Progress\n {...mergePartProps<ProgressProps>({ size: 'xs' }, propsProgress, {\n value: item.progress == null ? null : Math.round(item.progress * 100),\n 'aria-label': item.name,\n })}\n />\n </div>\n )}\n {failed && src && !item.rejected && (\n <div {...css.attrs(uploadStyles.overlay)} title={item.error}>\n {item.error}\n </div>\n )}\n {item.status === 'done' && <VisuallyHidden>{lang.uploadDone}</VisuallyHidden>}\n {!state.readonly && (\n <div {...css.attrs(uploadStyles.actions)}>\n {sortable && (\n <ButtonBase\n sx={uploadStyles.handle}\n ripple={false}\n disabled={state.disabled}\n aria-label={lang.sortableHandle(item.name)}\n {...sortableWall.handleProps(item.id)}\n >\n <Icon icon={GripVertical} />\n </ButtonBase>\n )}\n {failed && uploader() && !item.rejected && (\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: RotateCw,\n variant: 'ghost',\n size: 'xs',\n sx: uploadStyles.action,\n 'aria-label': lang.uploadRetry(item.name),\n },\n propsRetry,\n { onClick: () => void start(item) },\n )}\n />\n )}\n <IconButton\n {...mergePartProps<IconButtonProps>(\n {\n icon: X,\n variant: 'ghost',\n size: 'xs',\n sx: uploadStyles.action,\n 'aria-label': lang.uploadRemove(item.name),\n },\n propsRemove,\n { disabled: state.disabled, onClick: () => remove(item.id) },\n )}\n />\n </div>\n )}\n </div>\n );\n }\n\n /** 选文件的区域:列表模式在上方,图片墙模式是最后一格 */\n function renderDrop(picture: boolean) {\n const state = control();\n const full = maxFiles != null && multiple && value.length >= maxFiles;\n if (state.readonly || (picture && full)) return null;\n const hint = pasteable ? lang.uploadPasteHint : undefined;\n const props = mergePartProps<FileDropProps>(\n {\n accept,\n multiple,\n disabled: state.disabled,\n invalid: state.invalid,\n // 选好的文件不在这个输入框里(它选完就清空):已有文件时不再原生必填,免得表单交不了\n required: state.required && value.length === 0,\n description: picture\n ? undefined\n : hint\n ? [description, description != null && ' · ', hint]\n : description,\n propsRoot: picture\n ? { sx: [uploadStyles.addCard, uploadDynamicStyles.square(thumbnailSize)] }\n : undefined,\n },\n propsDrop,\n {\n onFiles: (files: File[]) => add(files),\n onReject: (files: File[]) => add(files, true),\n },\n );\n // 列表模式不传子元素:FileDrop 用默认的图标、提示与说明(写了 undefined 也算传了,会把默认内容换掉)\n if (!picture) return <FileDrop {...props} />;\n return (\n <FileDrop {...props}>\n <Icon icon={ImagePlus} />\n {lang.uploadAddImage}\n </FileDrop>\n );\n }\n\n return () => {\n const state = control();\n if (listType === 'picture')\n return (\n <div {...mergeAttrs<'div'>({ sx: uploadStyles.root }, rest, { onPaste })}>\n <ImageGroup sx={uploadStyles.wall}>\n {/* 列表里只有缩略图(“添加图片”与预览不是列表项);display: contents,缩略图仍和“添加图片”排在一行 */}\n <div\n ref={_toRef(wall)}\n role=\"list\"\n // 状态变化(失败、完成)由读屏播报\n aria-live=\"polite\"\n {...css.attrs(uploadStyles.wallList)}\n >\n {value.map(renderCard)}\n </div>\n {renderDrop(true)}\n </ImageGroup>\n {pasteable && !state.readonly && (\n <span {...css.attrs(uploadStyles.meta)}>\n {[description, description != null && ' · ', lang.uploadPasteHint]}\n </span>\n )}\n {sortable && (\n <>\n <VisuallyHidden id={sortableWall.instructionsId}>\n {lang.sortableInstructions}\n </VisuallyHidden>\n <VisuallyHidden aria-live=\"assertive\">{sortableWall.announcement}</VisuallyHidden>\n </>\n )}\n {name != null && (\n <input\n ref={_toRef(hidden)}\n type=\"file\"\n name={name}\n multiple\n hidden\n tabindex={-1}\n aria-hidden=\"true\"\n />\n )}\n </div>\n );\n return (\n <div {...mergeAttrs<'div'>({ sx: uploadStyles.root }, rest, { onPaste })}>\n {renderDrop(false)}\n {name != null && (\n <input\n ref={_toRef(hidden)}\n type=\"file\"\n name={name}\n multiple\n hidden\n tabindex={-1}\n aria-hidden=\"true\"\n />\n )}\n {value.length > 0 && (\n // 状态变化(失败、完成)由读屏播报\n <GridList\n {...mergePartProps<GridProps<'ul'>>({ gap: 'xs' }, propsList)}\n aria-live=\"polite\"\n >\n {value.map(renderItem)}\n </GridList>\n )}\n </div>\n );\n };\n },\n);\n"],"mappings":"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"}
|
|
@@ -138,7 +138,7 @@ export declare const toText: (value: unknown) => string;
|
|
|
138
138
|
export declare function isRuleComplete(rule: ConditionRule): boolean;
|
|
139
139
|
/**
|
|
140
140
|
* 记录满不满足条件。字段的值默认按名字取属性,read 可以改(嵌套字段、计算字段)。
|
|
141
|
-
* 规则与 query 库的 conditionBy(后端写成 SQL,query 仓库的 0017
|
|
141
|
+
* 规则与 query 库的 conditionBy(后端写成 SQL,query 仓库的 0017)、流程引擎的模型包(flow 仓库)一致,同一份条件在各处筛出同样的行:
|
|
142
142
|
* 没填完整的比较(isRuleComplete)与空的组不加条件,组里只看加了条件的那几项;不等于、不在其中、不包含把空值算作满足
|
|
143
143
|
*/
|
|
144
144
|
export declare function matchesCondition<T>(condition: Condition, record: T, read?: (record: T, field: string) => unknown): boolean;
|
package/dist/input/condition.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { compareDecimalText, decimalTextOf, isDecimal } from "../utils/data/numeric.js";
|
|
1
2
|
//#region src/input/condition.ts
|
|
2
3
|
/** 各类型默认能用的比较方式,第一个是新条件默认的 */
|
|
3
4
|
var conditionOperators = {
|
|
@@ -65,21 +66,24 @@ function isRuleComplete(rule) {
|
|
|
65
66
|
default: return !isBlank(rule.value);
|
|
66
67
|
}
|
|
67
68
|
}
|
|
68
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* 两个值按大小比:数字、大整数、Decimal、十进制写法的文字按精确的数值(不经过浮点数,长小数、大整数不丢精度),
|
|
71
|
+
* 其余写成文字后按编码顺序(不按语言环境;ISO 日期按文字比就是先后)。与流程引擎模型包(flow 的 values.ts)同一套规则
|
|
72
|
+
*/
|
|
69
73
|
function compare(a, b) {
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
const x = decimalTextOf(a);
|
|
75
|
+
const y = decimalTextOf(b);
|
|
76
|
+
if (x !== void 0 && y !== void 0) return compareDecimalText(x, y);
|
|
77
|
+
const left = toText(a);
|
|
78
|
+
const right = toText(b);
|
|
79
|
+
return left === right ? 0 : left < right ? -1 : 1;
|
|
75
80
|
}
|
|
76
|
-
/** 等于:数字与数字文字视为相等(表单里的 '5' 与数据里的 5
|
|
77
|
-
var same = (a, b) => a === b || a != null && b != null && typeof a !== "object" && compare(a, b) === 0;
|
|
78
|
-
var text = (value) => toText(value).
|
|
79
|
-
/**
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
switch (rule.op) {
|
|
81
|
+
/** 等于:数字与数字文字视为相等(表单里的 '5' 与数据里的 5);对象只有 Decimal 参与比较 */
|
|
82
|
+
var same = (a, b) => a === b || a != null && b != null && (typeof a !== "object" || isDecimal(a)) && (typeof b !== "object" || isDecimal(b)) && compare(a, b) === 0;
|
|
83
|
+
var text = (value) => toText(value).toLowerCase();
|
|
84
|
+
/** 对一个(不是数组的)值,这条比较成不成立 */
|
|
85
|
+
function matchesOne(op, actual, value) {
|
|
86
|
+
switch (op) {
|
|
83
87
|
case "eq": return same(actual, value);
|
|
84
88
|
case "ne": return !same(actual, value);
|
|
85
89
|
case "gt": return actual != null && compare(actual, value) > 0;
|
|
@@ -100,9 +104,23 @@ function matchesRule(rule, actual) {
|
|
|
100
104
|
case "empty": return isBlank(actual);
|
|
101
105
|
}
|
|
102
106
|
}
|
|
107
|
+
/** "没有一项满足"的比较 */
|
|
108
|
+
var NEGATIVE = /* @__PURE__ */ new Set([
|
|
109
|
+
"ne",
|
|
110
|
+
"notIn",
|
|
111
|
+
"notContains"
|
|
112
|
+
]);
|
|
113
|
+
/**
|
|
114
|
+
* 一条比较成不成立。字段的值是数组时(多选、人员的编号列表)对每一项比:等于、是其中之一、包含、大小这些是"有一项满足",
|
|
115
|
+
* 不等于、不在其中、不包含是"没有一项满足";有值、没有值看数组是不是空的
|
|
116
|
+
*/
|
|
117
|
+
function matchesRule(rule, actual) {
|
|
118
|
+
if (rule.op === "exists" || rule.op === "empty" || !Array.isArray(actual)) return matchesOne(rule.op, actual, rule.value);
|
|
119
|
+
return NEGATIVE.has(rule.op) ? actual.every((item) => matchesOne(rule.op, item, rule.value)) : actual.some((item) => matchesOne(rule.op, item, rule.value));
|
|
120
|
+
}
|
|
103
121
|
/**
|
|
104
122
|
* 记录满不满足条件。字段的值默认按名字取属性,read 可以改(嵌套字段、计算字段)。
|
|
105
|
-
* 规则与 query 库的 conditionBy(后端写成 SQL,query 仓库的 0017
|
|
123
|
+
* 规则与 query 库的 conditionBy(后端写成 SQL,query 仓库的 0017)、流程引擎的模型包(flow 仓库)一致,同一份条件在各处筛出同样的行:
|
|
106
124
|
* 没填完整的比较(isRuleComplete)与空的组不加条件,组里只看加了条件的那几项;不等于、不在其中、不包含把空值算作满足
|
|
107
125
|
*/
|
|
108
126
|
function matchesCondition(condition, record, read = (item, field) => item?.[field]) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"condition.js","names":[],"sources":["../../src/input/condition.ts"],"sourcesContent":["/**\n * 条件:比较一个字段,或用 all / any / not 组合的表达式树。纯 JSON,可以存库、传给后端,前后端各自求值、结果一致。\n * ConditionBuilder 编辑它;FilterBar 的高级筛选、流程连线的条件都用这一种写法。\n *\n * const condition: Condition = {\n * all: [\n * { field: 'amount', op: 'gte', value: 10000 },\n * { any: [{ field: 'category', op: 'in', value: ['钢材', '铝材'] }, { not: { field: 'urgent', op: 'eq', value: false } }] },\n * ],\n * };\n * rows.filter((row) => matchesCondition(condition, row));\n * describeCondition(condition, fields, lang); // “金额 大于等于 10000,且(品类 是其中之一 钢材、铝材,或 不满足:加急 是 否)”\n *\n * 字段(ConditionField)给出名字、显示名与类型;类型决定能用哪些比较方式(conditionOperators)与值用什么控件填。\n * 值的形式:between 是 [下限, 上限](含两端,任一端为空时不限),in / notIn 是数组,exists / empty 没有值;日期是 ISO 文字,按文字比就是先后。\n */\nimport type { VNodeChild } from 'vue';\nimport type { Lang } from '../config/lang/lang';\n\n/** 比较的方式 */\nexport interface ConditionOps {\n /** 等于 */\n eq: unknown;\n /** 不等于 */\n ne: unknown;\n /** 大于(日期:晚于) */\n gt: unknown;\n /** 大于等于 */\n gte: unknown;\n /** 小于(日期:早于) */\n lt: unknown;\n /** 小于等于 */\n lte: unknown;\n /** 在两个值之间(含两端):value 是 [下限, 上限] */\n between: unknown;\n /** 是其中之一:value 是数组 */\n in: unknown;\n /** 不是其中任何一个:value 是数组 */\n notIn: unknown;\n /** 文字包含 */\n contains: unknown;\n /** 文字不包含 */\n notContains: unknown;\n /** 文字以它开头 */\n startsWith: unknown;\n /** 有值(不是 null、undefined、空字符串、空数组) */\n exists: unknown;\n /** 没有值 */\n empty: unknown;\n}\n\n/** 比较的方式 */\nexport type ConditionOp = keyof ConditionOps;\n\n/** 比较一个字段 */\nexport interface ConditionRule {\n /** 字段名 */\n field: string;\n /** 比较的方式 */\n op: ConditionOp;\n /** 比较的值;exists、empty 没有 */\n value?: unknown;\n}\n\n/** 条件:比较一个字段,或用 all(全部满足)/ any(任一满足)/ not(不满足)组合 */\nexport type Condition =\n | ConditionRule\n | { all: readonly Condition[] }\n | { any: readonly Condition[] }\n | { not: Condition };\n\n/** 字段的类型:决定能用哪些比较方式、值用什么控件填 */\nexport interface ConditionFieldTypes {\n /** 文字:包含、等于、开头是 */\n text: unknown;\n /** 数字:大小、介于 */\n number: unknown;\n /** 日期(ISO 文字):早于、晚于、介于 */\n date: unknown;\n /** 从选项里选:是、不是、是其中之一 */\n select: unknown;\n /** 是或否 */\n boolean: unknown;\n}\n\n/** 字段的类型 */\nexport type ConditionFieldType = keyof ConditionFieldTypes;\n\n/** 选项型字段的一个选项 */\nexport interface ConditionOption {\n /** 值 */\n value: string | number;\n /** 显示的文字 */\n label: string;\n}\n\n/** 自己画值的输入时拿到的参数 */\nexport interface ConditionInputContext {\n /** 字段 */\n field: ConditionField;\n /** 比较的方式 */\n op: ConditionOp;\n /** 现在的值 */\n value: unknown;\n /** 改值 */\n onChange: (value: unknown) => void;\n /** 无障碍名称(“金额的值”) */\n label: string;\n}\n\n/** 能参与比较的字段 */\nexport interface ConditionField {\n /** 字段名:写进条件的 field */\n name: string;\n /** 显示名 */\n label: string;\n /** 类型 */\n type: ConditionFieldType;\n /** 分组名:字段多时字段列表按它分组(如“申请单”“流程变量”) */\n group?: string;\n /** 选项(select 类型) */\n options?: readonly ConditionOption[];\n /** 能用的比较方式,默认按类型(conditionOperators) */\n operators?: readonly ConditionOp[];\n /** 自己画值的输入(人员选择器、带单位的数字……);类型仍决定比较方式与摘要里值的写法 */\n input?: (context: ConditionInputContext) => VNodeChild;\n /** 摘要里值的写法,默认选项的文字、日期原样、是否写成“是 / 否” */\n format?: (value: unknown) => string;\n}\n\n/** 各类型默认能用的比较方式,第一个是新条件默认的 */\nexport const conditionOperators: Readonly<Record<ConditionFieldType, readonly ConditionOp[]>> = {\n text: ['contains', 'notContains', 'eq', 'ne', 'startsWith', 'empty', 'exists'],\n number: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'between', 'empty', 'exists'],\n date: ['eq', 'lt', 'gt', 'lte', 'gte', 'between', 'empty', 'exists'],\n select: ['eq', 'ne', 'in', 'notIn', 'empty', 'exists'],\n boolean: ['eq'],\n};\n\n/** 字段能用的比较方式 */\nexport const operatorsOf = (field: ConditionField): readonly ConditionOp[] =>\n field.operators ?? conditionOperators[field.type];\n\n/** 这种比较要不要值 */\nexport const needsValue = (op: ConditionOp) => op !== 'exists' && op !== 'empty';\n\n/** 值的形状:between 两个值,in / notIn 一组值,exists / empty 没有,其余一个 */\nexport type ConditionValueShape = 'none' | 'single' | 'range' | 'list';\n\n/** 比较方式要的值的形状 */\nexport function valueShape(op: ConditionOp): ConditionValueShape {\n if (!needsValue(op)) return 'none';\n if (op === 'between') return 'range';\n if (op === 'in' || op === 'notIn') return 'list';\n return 'single';\n}\n\n/** 是组合(all / any / not),不是比较 */\nexport const isConditionGroup = (\n condition: Condition,\n): condition is Exclude<Condition, ConditionRule> =>\n 'all' in condition || 'any' in condition || 'not' in condition;\n\n/** 值写成文字:null、undefined 是空字符串,对象(不该出现在比较里)写成 JSON */\nexport const toText = (value: unknown): string =>\n value == null\n ? ''\n : typeof value === 'object'\n ? JSON.stringify(value)\n : String(value as string | number | boolean | bigint);\n\nconst isBlank = (value: unknown) =>\n value == null || value === '' || (Array.isArray(value) && value.length === 0);\n\n/** 比较已经填完整:要值的有值,between 至少一端有值 */\nexport function isRuleComplete(rule: ConditionRule): boolean {\n switch (valueShape(rule.op)) {\n case 'none':\n return true;\n case 'range':\n return Array.isArray(rule.value) && rule.value.some((end) => !isBlank(end));\n default:\n return !isBlank(rule.value);\n }\n}\n\n/** 两个值按大小比:数字与能读成数字的文字按数值,其余按文字(ISO 日期按文字比就是先后) */\nfunction compare(a: unknown, b: unknown): number {\n const numeric = (value: unknown) =>\n typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))\n ? Number(value)\n : typeof value === 'bigint'\n ? Number(value)\n : value;\n const x = numeric(a);\n const y = numeric(b);\n if (typeof x === 'number' && typeof y === 'number') return x - y;\n return toText(x).localeCompare(toText(y));\n}\n\n/** 等于:数字与数字文字视为相等(表单里的 '5' 与数据里的 5) */\nconst same = (a: unknown, b: unknown) =>\n a === b || (a != null && b != null && typeof a !== 'object' && compare(a, b) === 0);\n\nconst text = (value: unknown) => toText(value).toLocaleLowerCase();\n\n/** 一条比较成不成立;read 取记录里字段的值,默认按字段名取属性 */\nfunction matchesRule(rule: ConditionRule, actual: unknown): boolean {\n const { value } = rule;\n switch (rule.op) {\n case 'eq':\n return same(actual, value);\n case 'ne':\n return !same(actual, value);\n case 'gt':\n return actual != null && compare(actual, value) > 0;\n case 'gte':\n return actual != null && compare(actual, value) >= 0;\n case 'lt':\n return actual != null && compare(actual, value) < 0;\n case 'lte':\n return actual != null && compare(actual, value) <= 0;\n case 'between': {\n const [low, high] = Array.isArray(value) ? value : [];\n if (actual == null) return false;\n return (\n (isBlank(low) || compare(actual, low) >= 0) && (isBlank(high) || compare(actual, high) <= 0)\n );\n }\n case 'in':\n return Array.isArray(value) && value.some((item) => same(actual, item));\n case 'notIn':\n return !Array.isArray(value) || !value.some((item) => same(actual, item));\n case 'contains':\n return text(actual).includes(text(value));\n case 'notContains':\n return !text(actual).includes(text(value));\n case 'startsWith':\n return text(actual).startsWith(text(value));\n case 'exists':\n return !isBlank(actual);\n case 'empty':\n return isBlank(actual);\n }\n}\n\n/**\n * 记录满不满足条件。字段的值默认按名字取属性,read 可以改(嵌套字段、计算字段)。\n * 规则与 query 库的 conditionBy(后端写成 SQL,query 仓库的 0017)一致,同一份条件在两边筛出同样的行:\n * 没填完整的比较(isRuleComplete)与空的组不加条件,组里只看加了条件的那几项;不等于、不在其中、不包含把空值算作满足\n */\nexport function matchesCondition<T>(\n condition: Condition,\n record: T,\n read: (record: T, field: string) => unknown = (item, field) =>\n (item as Record<string, unknown> | null)?.[field],\n): boolean {\n return evaluate(condition, record, read) ?? true;\n}\n\n/** 求值;不加条件(没填完整的比较、空的组)时是 undefined,同 SQL 端丢掉空条件 */\nfunction evaluate<T>(\n condition: Condition,\n record: T,\n read: (record: T, field: string) => unknown,\n): boolean | undefined {\n if ('not' in condition) {\n const inner = evaluate(condition.not, record, read);\n return inner === undefined ? undefined : !inner;\n }\n if ('all' in condition || 'any' in condition) {\n const results = ('all' in condition ? condition.all : condition.any)\n .map((item) => evaluate(item, record, read))\n .filter((result) => result !== undefined);\n if (results.length === 0) return undefined;\n return 'all' in condition ? results.every(Boolean) : results.some(Boolean);\n }\n if (!isRuleComplete(condition)) return undefined;\n return matchesRule(condition, read(record, condition.field));\n}\n\n/** 比较方式的文字:日期、选项、是否这几种类型有自己的说法(早于、是其中之一) */\nexport function operatorLabel(lang: Lang, type: ConditionFieldType, op: ConditionOp): string {\n return lang.conditionTypeOperators[type]?.[op] ?? lang.conditionOperators[op];\n}\n\n/** 值在摘要里的写法 */\nexport function formatConditionValue(\n lang: Lang,\n field: ConditionField | undefined,\n value: unknown,\n) {\n const one = (item: unknown): string => {\n if (field?.format) return field.format(item);\n if (field?.type === 'boolean') return item ? lang.conditionTrue : lang.conditionFalse;\n const option = field?.options?.find((candidate) => candidate.value === item);\n if (option) return option.label;\n if (typeof item === 'number') return item.toLocaleString(lang.code);\n return toText(item);\n };\n if (Array.isArray(value)) return value.map(one).join(lang.conditionValueSeparator);\n return one(value);\n}\n\n/**\n * 条件写成一句话:收起的高级筛选、流程连线上的条件摘要用。不认识的字段原样写字段名;\n * 嵌套的组加括号,not 写成“不满足:…”\n */\nexport function describeCondition(\n condition: Condition,\n fields: readonly ConditionField[],\n lang: Lang,\n): string {\n const describe = (item: Condition, nested: boolean): string => {\n if ('not' in item) return `${lang.conditionNotPrefix}${describe(item.not, true)}`;\n if ('all' in item || 'any' in item) {\n const list = 'all' in item ? item.all : item.any;\n const joiner = 'all' in item ? lang.conditionAndJoiner : lang.conditionOrJoiner;\n const text = list.map((child) => describe(child, true)).join(joiner);\n return nested && list.length > 1 ? `(${text})` : text;\n }\n const field = fields.find((candidate) => candidate.name === item.field);\n const parts = [field?.label ?? item.field, operatorLabel(lang, field?.type ?? 'text', item.op)];\n const shape = valueShape(item.op);\n if (shape === 'range') {\n const [low, high] = Array.isArray(item.value) ? item.value : [];\n parts.push(\n lang.conditionRange(\n formatConditionValue(lang, field, low),\n formatConditionValue(lang, field, high),\n ),\n );\n } else if (shape !== 'none') {\n parts.push(formatConditionValue(lang, field, item.value));\n }\n return parts.join(' ');\n };\n return describe(condition, false);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"condition.js","names":[],"sources":["../../src/input/condition.ts"],"sourcesContent":["/**\n * 条件:比较一个字段,或用 all / any / not 组合的表达式树。纯 JSON,可以存库、传给后端,前后端各自求值、结果一致。\n * ConditionBuilder 编辑它;FilterBar 的高级筛选、流程连线的条件都用这一种写法。\n *\n * const condition: Condition = {\n * all: [\n * { field: 'amount', op: 'gte', value: 10000 },\n * { any: [{ field: 'category', op: 'in', value: ['钢材', '铝材'] }, { not: { field: 'urgent', op: 'eq', value: false } }] },\n * ],\n * };\n * rows.filter((row) => matchesCondition(condition, row));\n * describeCondition(condition, fields, lang); // “金额 大于等于 10000,且(品类 是其中之一 钢材、铝材,或 不满足:加急 是 否)”\n *\n * 字段(ConditionField)给出名字、显示名与类型;类型决定能用哪些比较方式(conditionOperators)与值用什么控件填。\n * 值的形式:between 是 [下限, 上限](含两端,任一端为空时不限),in / notIn 是数组,exists / empty 没有值;日期是 ISO 文字,按文字比就是先后。\n */\nimport type { VNodeChild } from 'vue';\nimport type { Lang } from '../config/lang/lang';\nimport { compareDecimalText, decimalTextOf, isDecimal } from '../utils/data/numeric';\n\n/** 比较的方式 */\nexport interface ConditionOps {\n /** 等于 */\n eq: unknown;\n /** 不等于 */\n ne: unknown;\n /** 大于(日期:晚于) */\n gt: unknown;\n /** 大于等于 */\n gte: unknown;\n /** 小于(日期:早于) */\n lt: unknown;\n /** 小于等于 */\n lte: unknown;\n /** 在两个值之间(含两端):value 是 [下限, 上限] */\n between: unknown;\n /** 是其中之一:value 是数组 */\n in: unknown;\n /** 不是其中任何一个:value 是数组 */\n notIn: unknown;\n /** 文字包含 */\n contains: unknown;\n /** 文字不包含 */\n notContains: unknown;\n /** 文字以它开头 */\n startsWith: unknown;\n /** 有值(不是 null、undefined、空字符串、空数组) */\n exists: unknown;\n /** 没有值 */\n empty: unknown;\n}\n\n/** 比较的方式 */\nexport type ConditionOp = keyof ConditionOps;\n\n/** 比较一个字段 */\nexport interface ConditionRule {\n /** 字段名 */\n field: string;\n /** 比较的方式 */\n op: ConditionOp;\n /** 比较的值;exists、empty 没有 */\n value?: unknown;\n}\n\n/** 条件:比较一个字段,或用 all(全部满足)/ any(任一满足)/ not(不满足)组合 */\nexport type Condition =\n | ConditionRule\n | { all: readonly Condition[] }\n | { any: readonly Condition[] }\n | { not: Condition };\n\n/** 字段的类型:决定能用哪些比较方式、值用什么控件填 */\nexport interface ConditionFieldTypes {\n /** 文字:包含、等于、开头是 */\n text: unknown;\n /** 数字:大小、介于 */\n number: unknown;\n /** 日期(ISO 文字):早于、晚于、介于 */\n date: unknown;\n /** 从选项里选:是、不是、是其中之一 */\n select: unknown;\n /** 是或否 */\n boolean: unknown;\n}\n\n/** 字段的类型 */\nexport type ConditionFieldType = keyof ConditionFieldTypes;\n\n/** 选项型字段的一个选项 */\nexport interface ConditionOption {\n /** 值 */\n value: string | number;\n /** 显示的文字 */\n label: string;\n}\n\n/** 自己画值的输入时拿到的参数 */\nexport interface ConditionInputContext {\n /** 字段 */\n field: ConditionField;\n /** 比较的方式 */\n op: ConditionOp;\n /** 现在的值 */\n value: unknown;\n /** 改值 */\n onChange: (value: unknown) => void;\n /** 无障碍名称(“金额的值”) */\n label: string;\n}\n\n/** 能参与比较的字段 */\nexport interface ConditionField {\n /** 字段名:写进条件的 field */\n name: string;\n /** 显示名 */\n label: string;\n /** 类型 */\n type: ConditionFieldType;\n /** 分组名:字段多时字段列表按它分组(如“申请单”“流程变量”) */\n group?: string;\n /** 选项(select 类型) */\n options?: readonly ConditionOption[];\n /** 能用的比较方式,默认按类型(conditionOperators) */\n operators?: readonly ConditionOp[];\n /** 自己画值的输入(人员选择器、带单位的数字……);类型仍决定比较方式与摘要里值的写法 */\n input?: (context: ConditionInputContext) => VNodeChild;\n /** 摘要里值的写法,默认选项的文字、日期原样、是否写成“是 / 否” */\n format?: (value: unknown) => string;\n}\n\n/** 各类型默认能用的比较方式,第一个是新条件默认的 */\nexport const conditionOperators: Readonly<Record<ConditionFieldType, readonly ConditionOp[]>> = {\n text: ['contains', 'notContains', 'eq', 'ne', 'startsWith', 'empty', 'exists'],\n number: ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'between', 'empty', 'exists'],\n date: ['eq', 'lt', 'gt', 'lte', 'gte', 'between', 'empty', 'exists'],\n select: ['eq', 'ne', 'in', 'notIn', 'empty', 'exists'],\n boolean: ['eq'],\n};\n\n/** 字段能用的比较方式 */\nexport const operatorsOf = (field: ConditionField): readonly ConditionOp[] =>\n field.operators ?? conditionOperators[field.type];\n\n/** 这种比较要不要值 */\nexport const needsValue = (op: ConditionOp) => op !== 'exists' && op !== 'empty';\n\n/** 值的形状:between 两个值,in / notIn 一组值,exists / empty 没有,其余一个 */\nexport type ConditionValueShape = 'none' | 'single' | 'range' | 'list';\n\n/** 比较方式要的值的形状 */\nexport function valueShape(op: ConditionOp): ConditionValueShape {\n if (!needsValue(op)) return 'none';\n if (op === 'between') return 'range';\n if (op === 'in' || op === 'notIn') return 'list';\n return 'single';\n}\n\n/** 是组合(all / any / not),不是比较 */\nexport const isConditionGroup = (\n condition: Condition,\n): condition is Exclude<Condition, ConditionRule> =>\n 'all' in condition || 'any' in condition || 'not' in condition;\n\n/** 值写成文字:null、undefined 是空字符串,对象(不该出现在比较里)写成 JSON */\nexport const toText = (value: unknown): string =>\n value == null\n ? ''\n : typeof value === 'object'\n ? JSON.stringify(value)\n : String(value as string | number | boolean | bigint);\n\nconst isBlank = (value: unknown) =>\n value == null || value === '' || (Array.isArray(value) && value.length === 0);\n\n/** 比较已经填完整:要值的有值,between 至少一端有值 */\nexport function isRuleComplete(rule: ConditionRule): boolean {\n switch (valueShape(rule.op)) {\n case 'none':\n return true;\n case 'range':\n return Array.isArray(rule.value) && rule.value.some((end) => !isBlank(end));\n default:\n return !isBlank(rule.value);\n }\n}\n\n/**\n * 两个值按大小比:数字、大整数、Decimal、十进制写法的文字按精确的数值(不经过浮点数,长小数、大整数不丢精度),\n * 其余写成文字后按编码顺序(不按语言环境;ISO 日期按文字比就是先后)。与流程引擎模型包(flow 的 values.ts)同一套规则\n */\nfunction compare(a: unknown, b: unknown): number {\n const x = decimalTextOf(a);\n const y = decimalTextOf(b);\n if (x !== undefined && y !== undefined) return compareDecimalText(x, y);\n const left = toText(a);\n const right = toText(b);\n return left === right ? 0 : left < right ? -1 : 1;\n}\n\n/** 等于:数字与数字文字视为相等(表单里的 '5' 与数据里的 5);对象只有 Decimal 参与比较 */\nconst same = (a: unknown, b: unknown) =>\n a === b ||\n (a != null &&\n b != null &&\n (typeof a !== 'object' || isDecimal(a)) &&\n (typeof b !== 'object' || isDecimal(b)) &&\n compare(a, b) === 0);\n\nconst text = (value: unknown) => toText(value).toLowerCase();\n\n/** 对一个(不是数组的)值,这条比较成不成立 */\nfunction matchesOne(op: ConditionOp, actual: unknown, value: unknown): boolean {\n switch (op) {\n case 'eq':\n return same(actual, value);\n case 'ne':\n return !same(actual, value);\n case 'gt':\n return actual != null && compare(actual, value) > 0;\n case 'gte':\n return actual != null && compare(actual, value) >= 0;\n case 'lt':\n return actual != null && compare(actual, value) < 0;\n case 'lte':\n return actual != null && compare(actual, value) <= 0;\n case 'between': {\n const [low, high] = Array.isArray(value) ? value : [];\n if (actual == null) return false;\n return (\n (isBlank(low) || compare(actual, low) >= 0) && (isBlank(high) || compare(actual, high) <= 0)\n );\n }\n case 'in':\n return Array.isArray(value) && value.some((item) => same(actual, item));\n case 'notIn':\n return !Array.isArray(value) || !value.some((item) => same(actual, item));\n case 'contains':\n return text(actual).includes(text(value));\n case 'notContains':\n return !text(actual).includes(text(value));\n case 'startsWith':\n return text(actual).startsWith(text(value));\n case 'exists':\n return !isBlank(actual);\n case 'empty':\n return isBlank(actual);\n }\n}\n\n/** \"没有一项满足\"的比较 */\nconst NEGATIVE: ReadonlySet<ConditionOp> = new Set(['ne', 'notIn', 'notContains']);\n\n/**\n * 一条比较成不成立。字段的值是数组时(多选、人员的编号列表)对每一项比:等于、是其中之一、包含、大小这些是\"有一项满足\",\n * 不等于、不在其中、不包含是\"没有一项满足\";有值、没有值看数组是不是空的\n */\nfunction matchesRule(rule: ConditionRule, actual: unknown): boolean {\n if (rule.op === 'exists' || rule.op === 'empty' || !Array.isArray(actual))\n return matchesOne(rule.op, actual, rule.value);\n return NEGATIVE.has(rule.op)\n ? actual.every((item) => matchesOne(rule.op, item, rule.value))\n : actual.some((item) => matchesOne(rule.op, item, rule.value));\n}\n\n/**\n * 记录满不满足条件。字段的值默认按名字取属性,read 可以改(嵌套字段、计算字段)。\n * 规则与 query 库的 conditionBy(后端写成 SQL,query 仓库的 0017)、流程引擎的模型包(flow 仓库)一致,同一份条件在各处筛出同样的行:\n * 没填完整的比较(isRuleComplete)与空的组不加条件,组里只看加了条件的那几项;不等于、不在其中、不包含把空值算作满足\n */\nexport function matchesCondition<T>(\n condition: Condition,\n record: T,\n read: (record: T, field: string) => unknown = (item, field) =>\n (item as Record<string, unknown> | null)?.[field],\n): boolean {\n return evaluate(condition, record, read) ?? true;\n}\n\n/** 求值;不加条件(没填完整的比较、空的组)时是 undefined,同 SQL 端丢掉空条件 */\nfunction evaluate<T>(\n condition: Condition,\n record: T,\n read: (record: T, field: string) => unknown,\n): boolean | undefined {\n if ('not' in condition) {\n const inner = evaluate(condition.not, record, read);\n return inner === undefined ? undefined : !inner;\n }\n if ('all' in condition || 'any' in condition) {\n const results = ('all' in condition ? condition.all : condition.any)\n .map((item) => evaluate(item, record, read))\n .filter((result) => result !== undefined);\n if (results.length === 0) return undefined;\n return 'all' in condition ? results.every(Boolean) : results.some(Boolean);\n }\n if (!isRuleComplete(condition)) return undefined;\n return matchesRule(condition, read(record, condition.field));\n}\n\n/** 比较方式的文字:日期、选项、是否这几种类型有自己的说法(早于、是其中之一) */\nexport function operatorLabel(lang: Lang, type: ConditionFieldType, op: ConditionOp): string {\n return lang.conditionTypeOperators[type]?.[op] ?? lang.conditionOperators[op];\n}\n\n/** 值在摘要里的写法 */\nexport function formatConditionValue(\n lang: Lang,\n field: ConditionField | undefined,\n value: unknown,\n) {\n const one = (item: unknown): string => {\n if (field?.format) return field.format(item);\n if (field?.type === 'boolean') return item ? lang.conditionTrue : lang.conditionFalse;\n const option = field?.options?.find((candidate) => candidate.value === item);\n if (option) return option.label;\n if (typeof item === 'number') return item.toLocaleString(lang.code);\n return toText(item);\n };\n if (Array.isArray(value)) return value.map(one).join(lang.conditionValueSeparator);\n return one(value);\n}\n\n/**\n * 条件写成一句话:收起的高级筛选、流程连线上的条件摘要用。不认识的字段原样写字段名;\n * 嵌套的组加括号,not 写成“不满足:…”\n */\nexport function describeCondition(\n condition: Condition,\n fields: readonly ConditionField[],\n lang: Lang,\n): string {\n const describe = (item: Condition, nested: boolean): string => {\n if ('not' in item) return `${lang.conditionNotPrefix}${describe(item.not, true)}`;\n if ('all' in item || 'any' in item) {\n const list = 'all' in item ? item.all : item.any;\n const joiner = 'all' in item ? lang.conditionAndJoiner : lang.conditionOrJoiner;\n const text = list.map((child) => describe(child, true)).join(joiner);\n return nested && list.length > 1 ? `(${text})` : text;\n }\n const field = fields.find((candidate) => candidate.name === item.field);\n const parts = [field?.label ?? item.field, operatorLabel(lang, field?.type ?? 'text', item.op)];\n const shape = valueShape(item.op);\n if (shape === 'range') {\n const [low, high] = Array.isArray(item.value) ? item.value : [];\n parts.push(\n lang.conditionRange(\n formatConditionValue(lang, field, low),\n formatConditionValue(lang, field, high),\n ),\n );\n } else if (shape !== 'none') {\n parts.push(formatConditionValue(lang, field, item.value));\n }\n return parts.join(' ');\n };\n return describe(condition, false);\n}\n"],"mappings":"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"}
|
package/dist/input/people.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 人员与组织选择器(PeoplePicker、PeoplePickerPanel
|
|
2
|
+
* 人员与组织选择器(PeoplePicker、PeoplePickerPanel)的数据:标准的是人员、部门、角色三种(标准的组织模型),数据由接入方的 source 提供。
|
|
3
|
+
* 岗位、项目组这类由接入方给 PeopleTypes 加类型(接口合并),名字写进语言包的 peopleTypes。
|
|
3
4
|
*
|
|
4
5
|
* const source: PeopleSource = {
|
|
5
6
|
* children: (parent, signal) => http.get('/org/children', { query: { parent: parent?.id }, signal }),
|
|
6
7
|
* search: (query, signal) => http.get('/org/search', { query: { q: query }, signal }),
|
|
7
|
-
*
|
|
8
|
+
* roles: (signal) => http.get('/org/roles', { signal }),
|
|
8
9
|
* };
|
|
9
10
|
* <PeoplePicker source={source} _value={owners} />
|
|
10
11
|
*
|
|
@@ -17,20 +18,20 @@ export interface PeopleTypes {
|
|
|
17
18
|
user: unknown;
|
|
18
19
|
/** 部门:选中就是部门里的人(怎么展开由后端定) */
|
|
19
20
|
department: unknown;
|
|
20
|
-
/**
|
|
21
|
-
|
|
21
|
+
/** 角色:流程的角色收件箱 */
|
|
22
|
+
role: unknown;
|
|
22
23
|
}
|
|
23
24
|
/** 能选的类型 */
|
|
24
25
|
export type PeopleType = keyof PeopleTypes;
|
|
25
|
-
/**
|
|
26
|
+
/** 一个人员、部门或角色 */
|
|
26
27
|
export interface PeopleEntry {
|
|
27
|
-
/**
|
|
28
|
-
id: string;
|
|
28
|
+
/** 标识:同一种类型里唯一。一般是接入方组织表的自增主键(数字),用工号、编码这类文字也行 */
|
|
29
|
+
id: string | number;
|
|
29
30
|
/** 类型 */
|
|
30
31
|
type: PeopleType;
|
|
31
32
|
/** 名字 */
|
|
32
33
|
name: string;
|
|
33
|
-
/**
|
|
34
|
+
/** 说明:人员的部门与角色、部门的上级路径;搜索结果里分辨同名的 */
|
|
34
35
|
description?: string;
|
|
35
36
|
/** 头像地址(人员);没有时显示名字的头一个字 */
|
|
36
37
|
avatar?: string;
|
|
@@ -41,25 +42,25 @@ export interface PeopleEntry {
|
|
|
41
42
|
export interface PeopleSource {
|
|
42
43
|
/** 一个部门下的子部门与人员(部门在前);parent 为 null 时是最上层 */
|
|
43
44
|
children(parent: PeopleEntry | null, signal: AbortSignal): Promise<readonly PeopleEntry[]>;
|
|
44
|
-
/**
|
|
45
|
+
/** 按关键字搜,人员、部门、角色都可以返回;不写时没有搜索框 */
|
|
45
46
|
search?(query: string, signal: AbortSignal): Promise<readonly PeopleEntry[]>;
|
|
46
|
-
/**
|
|
47
|
-
|
|
47
|
+
/** 全部角色;不写时没有“角色”页 */
|
|
48
|
+
roles?(signal: AbortSignal): Promise<readonly PeopleEntry[]>;
|
|
48
49
|
}
|
|
49
50
|
/** 组织架构的一个节点:部门带着下级 */
|
|
50
51
|
export interface PeopleNode extends PeopleEntry {
|
|
51
52
|
/** 子部门与人员 */
|
|
52
53
|
children?: readonly PeopleNode[];
|
|
53
54
|
}
|
|
54
|
-
/**
|
|
55
|
+
/** 同一个人员、部门、角色的键:类型加标识 */
|
|
55
56
|
export declare const peopleKey: (entry: Pick<PeopleEntry, 'type' | 'id'>) => string;
|
|
56
57
|
/** 去掉下级、只留这一项本身(写进选中的值、最近选过的) */
|
|
57
58
|
declare const plain: ({ id, type, name, description, avatar, hasChildren }: PeopleEntry) => PeopleEntry;
|
|
58
59
|
/**
|
|
59
60
|
* 整棵在前端的组织架构做成数据来源:示例、测试与规模不大的系统直接用。
|
|
60
|
-
*
|
|
61
|
+
* 搜索按名字与说明(不分大小写),角色单独给;部门的 hasChildren 按有没有 children 填上
|
|
61
62
|
*/
|
|
62
|
-
export declare function localPeopleSource(tree: readonly PeopleNode[],
|
|
63
|
+
export declare function localPeopleSource(tree: readonly PeopleNode[], roles?: readonly PeopleEntry[]): PeopleSource;
|
|
63
64
|
/** 写进值里的一项:去掉下级与其他字段 */
|
|
64
65
|
export declare const toPeopleValue: typeof plain;
|
|
65
66
|
export {};
|
package/dist/input/people.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/input/people.ts
|
|
2
|
-
/**
|
|
2
|
+
/** 同一个人员、部门、角色的键:类型加标识 */
|
|
3
3
|
var peopleKey = (entry) => `${entry.type}:${entry.id}`;
|
|
4
4
|
/** 去掉下级、只留这一项本身(写进选中的值、最近选过的) */
|
|
5
5
|
var plain = ({ id, type, name, description, avatar, hasChildren }) => ({
|
|
@@ -12,9 +12,9 @@ var plain = ({ id, type, name, description, avatar, hasChildren }) => ({
|
|
|
12
12
|
});
|
|
13
13
|
/**
|
|
14
14
|
* 整棵在前端的组织架构做成数据来源:示例、测试与规模不大的系统直接用。
|
|
15
|
-
*
|
|
15
|
+
* 搜索按名字与说明(不分大小写),角色单独给;部门的 hasChildren 按有没有 children 填上
|
|
16
16
|
*/
|
|
17
|
-
function localPeopleSource(tree,
|
|
17
|
+
function localPeopleSource(tree, roles = []) {
|
|
18
18
|
const byKey = /* @__PURE__ */ new Map();
|
|
19
19
|
const all = [];
|
|
20
20
|
const visit = (level) => {
|
|
@@ -34,9 +34,9 @@ function localPeopleSource(tree, positions = []) {
|
|
|
34
34
|
children: (parent) => Promise.resolve((parent ? byKey.get(peopleKey(parent))?.children ?? [] : tree).map(view)),
|
|
35
35
|
search: (query) => {
|
|
36
36
|
const text = query.trim().toLocaleLowerCase();
|
|
37
|
-
return Promise.resolve([...all.map(view), ...
|
|
37
|
+
return Promise.resolve([...all.map(view), ...roles.map(plain)].filter((entry) => matches(entry, text)));
|
|
38
38
|
},
|
|
39
|
-
...
|
|
39
|
+
...roles.length > 0 ? { roles: () => Promise.resolve(roles.map(plain)) } : {}
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
/** 写进值里的一项:去掉下级与其他字段 */
|
package/dist/input/people.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"people.js","names":[],"sources":["../../src/input/people.ts"],"sourcesContent":["/**\n * 人员与组织选择器(PeoplePicker、PeoplePickerPanel
|
|
1
|
+
{"version":3,"file":"people.js","names":[],"sources":["../../src/input/people.ts"],"sourcesContent":["/**\n * 人员与组织选择器(PeoplePicker、PeoplePickerPanel)的数据:标准的是人员、部门、角色三种(标准的组织模型),数据由接入方的 source 提供。\n * 岗位、项目组这类由接入方给 PeopleTypes 加类型(接口合并),名字写进语言包的 peopleTypes。\n *\n * const source: PeopleSource = {\n * children: (parent, signal) => http.get('/org/children', { query: { parent: parent?.id }, signal }),\n * search: (query, signal) => http.get('/org/search', { query: { q: query }, signal }),\n * roles: (signal) => http.get('/org/roles', { signal }),\n * };\n * <PeoplePicker source={source} _value={owners} />\n *\n * 选中的值是 PeopleEntry 的数组(带名字,回显不用再查);同一个人、部门按 type + id 判断(peopleKey)。\n * 组织架构不大、已经整棵在前端时用 localPeopleSource。\n */\n\n/** 能选的类型 */\nexport interface PeopleTypes {\n /** 人员 */\n user: unknown;\n /** 部门:选中就是部门里的人(怎么展开由后端定) */\n department: unknown;\n /** 角色:流程的角色收件箱 */\n role: unknown;\n}\n\n// 接入方加类型(岗位):\n// declare module '@zerodep-dev/vue-tsx-pro-ui' { interface PeopleTypes { position: unknown } }\n\n/** 能选的类型 */\nexport type PeopleType = keyof PeopleTypes;\n\n/** 一个人员、部门或角色 */\nexport interface PeopleEntry {\n /** 标识:同一种类型里唯一。一般是接入方组织表的自增主键(数字),用工号、编码这类文字也行 */\n id: string | number;\n /** 类型 */\n type: PeopleType;\n /** 名字 */\n name: string;\n /** 说明:人员的部门与角色、部门的上级路径;搜索结果里分辨同名的 */\n description?: string;\n /** 头像地址(人员);没有时显示名字的头一个字 */\n avatar?: string;\n /** 部门下还有子部门或人员;false 时没有“下级”,不写时按有 */\n hasChildren?: boolean;\n}\n\n/** 数据来源:接入方按自己的接口实现;signal 在换了部门、改了搜索词、关掉选择器时中止 */\nexport interface PeopleSource {\n /** 一个部门下的子部门与人员(部门在前);parent 为 null 时是最上层 */\n children(parent: PeopleEntry | null, signal: AbortSignal): Promise<readonly PeopleEntry[]>;\n /** 按关键字搜,人员、部门、角色都可以返回;不写时没有搜索框 */\n search?(query: string, signal: AbortSignal): Promise<readonly PeopleEntry[]>;\n /** 全部角色;不写时没有“角色”页 */\n roles?(signal: AbortSignal): Promise<readonly PeopleEntry[]>;\n}\n\n/** 组织架构的一个节点:部门带着下级 */\nexport interface PeopleNode extends PeopleEntry {\n /** 子部门与人员 */\n children?: readonly PeopleNode[];\n}\n\n/** 同一个人员、部门、角色的键:类型加标识 */\nexport const peopleKey = (entry: Pick<PeopleEntry, 'type' | 'id'>) => `${entry.type}:${entry.id}`;\n\n/** 去掉下级、只留这一项本身(写进选中的值、最近选过的) */\nconst plain = ({ id, type, name, description, avatar, hasChildren }: PeopleEntry): PeopleEntry => ({\n id,\n type,\n name,\n ...(description != null ? { description } : {}),\n ...(avatar != null ? { avatar } : {}),\n ...(hasChildren != null ? { hasChildren } : {}),\n});\n\n/**\n * 整棵在前端的组织架构做成数据来源:示例、测试与规模不大的系统直接用。\n * 搜索按名字与说明(不分大小写),角色单独给;部门的 hasChildren 按有没有 children 填上\n */\nexport function localPeopleSource(\n tree: readonly PeopleNode[],\n roles: readonly PeopleEntry[] = [],\n): PeopleSource {\n const byKey = new Map<string, PeopleNode>();\n const all: PeopleNode[] = [];\n const visit = (level: readonly PeopleNode[]) => {\n for (const node of level) {\n byKey.set(peopleKey(node), node);\n all.push(node);\n visit(node.children ?? []);\n }\n };\n visit(tree);\n const view = (node: PeopleNode): PeopleEntry =>\n plain(node.type === 'department' ? { ...node, hasChildren: !!node.children?.length } : node);\n const matches = (entry: PeopleEntry, text: string) =>\n entry.name.toLocaleLowerCase().includes(text) ||\n !!entry.description?.toLocaleLowerCase().includes(text);\n return {\n children: (parent) =>\n Promise.resolve((parent ? (byKey.get(peopleKey(parent))?.children ?? []) : tree).map(view)),\n search: (query) => {\n const text = query.trim().toLocaleLowerCase();\n return Promise.resolve(\n [...all.map(view), ...roles.map(plain)].filter((entry) => matches(entry, text)),\n );\n },\n ...(roles.length > 0 ? { roles: () => Promise.resolve(roles.map(plain)) } : {}),\n };\n}\n\n/** 写进值里的一项:去掉下级与其他字段 */\nexport const toPeopleValue = plain;\n"],"mappings":";;AAgEA,IAAa,aAAa,UAA4C,GAAG,MAAM,KAAI,GAAI,MAAM;;AAG7F,IAAM,SAAS,EAAE,IAAI,MAAM,MAAM,aAAa,QAAQ,mBAA6C;CACjG;CACA;CACA;CACA,GAAI,eAAe,OAAO,EAAE,YAAY,IAAI,CAAC;CAC7C,GAAI,UAAU,OAAO,EAAE,OAAO,IAAI,CAAC;CACnC,GAAI,eAAe,OAAO,EAAE,YAAY,IAAI,CAAC;AAC/C;;;;;AAMA,SAAgB,kBACd,MACA,QAAgC,CAAA,GAClB;CACd,MAAM,wBAAQ,IAAI,IAAwB;CAC1C,MAAM,MAAoB,CAAA;CAC1B,MAAM,SAAS,UAAiC;EAC9C,KAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,IAAI,UAAU,IAAI,GAAG,IAAI;GAC/B,IAAI,KAAK,IAAI;GACb,MAAM,KAAK,YAAY,CAAA,CAAE;EAC3B;CACF;CACA,MAAM,IAAI;CACV,MAAM,QAAQ,SACZ,MAAM,KAAK,SAAS,eAAe;EAAE,GAAG;EAAM,aAAa,CAAC,CAAC,KAAK,UAAU;CAAO,IAAI,IAAI;CAC7F,MAAM,WAAW,OAAoB,SACnC,MAAM,KAAK,kBAAkB,CAAC,CAAC,SAAS,IAAI,KAC5C,CAAC,CAAC,MAAM,aAAa,kBAAkB,CAAC,CAAC,SAAS,IAAI;CACxD,OAAO;EACL,WAAW,WACT,QAAQ,SAAS,SAAU,MAAM,IAAI,UAAU,MAAM,CAAC,CAAC,EAAE,YAAY,CAAA,IAAM,KAAA,CAAM,IAAI,IAAI,CAAC;EAC5F,SAAS,UAAU;GACjB,MAAM,OAAO,MAAM,KAAK,CAAC,CAAC,kBAAkB;GAC5C,OAAO,QAAQ,QACb,CAAC,GAAG,IAAI,IAAI,IAAI,GAAG,GAAG,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,QAAQ,UAAU,QAAQ,OAAO,IAAI,CAAC,CAChF;EACF;EACA,GAAI,MAAM,SAAS,IAAI,EAAE,aAAa,QAAQ,QAAQ,MAAM,IAAI,KAAK,CAAC,EAAE,IAAI,CAAC;CAC/E;AACF;;AAGA,IAAa,gBAAgB"}
|
|
@@ -15,6 +15,8 @@ export declare function useRemoteItems<T>(read: {
|
|
|
15
15
|
items: () => readonly T[];
|
|
16
16
|
/** 正在请求 */
|
|
17
17
|
loading: () => boolean;
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 最后一次请求失败时列表里的提示:错误码有文案时用它(网络断了、没有权限),否则是笼统的“加载失败,请重试”;没失败时为 undefined
|
|
20
|
+
*/
|
|
21
|
+
failure: () => string | undefined;
|
|
20
22
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { useLang } from "../config/context.js";
|
|
2
|
+
import { errorCodeText } from "../utils/request/error-message.js";
|
|
1
3
|
import { useRequest } from "../utils/request/use-request.js";
|
|
2
4
|
import { watch } from "vue";
|
|
3
5
|
//#region src/input/remote-items.ts
|
|
@@ -10,6 +12,7 @@ import { watch } from "vue";
|
|
|
10
12
|
var DEBOUNCE = 300;
|
|
11
13
|
/** 远程选项的状态;没给 load 时原样用 items */
|
|
12
14
|
function useRemoteItems(read) {
|
|
15
|
+
const lang = useLang();
|
|
13
16
|
const request = useRequest((query, signal) => read.load()(query, signal), {
|
|
14
17
|
params: () => read.query(),
|
|
15
18
|
debounce: DEBOUNCE,
|
|
@@ -26,8 +29,10 @@ function useRemoteItems(read) {
|
|
|
26
29
|
items: () => read.load() ? request.data ?? read.items() : read.items(),
|
|
27
30
|
/** 正在请求 */
|
|
28
31
|
loading: () => request.loading,
|
|
29
|
-
/**
|
|
30
|
-
|
|
32
|
+
/**
|
|
33
|
+
* 最后一次请求失败时列表里的提示:错误码有文案时用它(网络断了、没有权限),否则是笼统的“加载失败,请重试”;没失败时为 undefined
|
|
34
|
+
*/
|
|
35
|
+
failure: () => request.error === void 0 ? void 0 : errorCodeText(request.error, lang) ?? lang.listLoadFailed
|
|
31
36
|
};
|
|
32
37
|
}
|
|
33
38
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-items.js","names":[],"sources":["../../src/input/remote-items.ts"],"sourcesContent":["/**\n * 远程选项(Select、Combobox、TreeSelect 的 load):弹层打开时按当前搜索词请求,之后搜索词变了停下 300 ms 再请求;\n * 只采用最后一次的结果(useRequest),结果代替 items。弹层关着不请求。\n * 第一次结果回来之前用 items:编辑页把已选的那一项放进 items,触发框一开始就有文字。\n */\nimport { watch } from 'vue';\nimport { useRequest } from '../utils/request/use-request';\n\n/** 远程选项的加载函数:搜索词 → 选项;signal 在被后来的请求顶掉、组件卸载时中止 */\nexport type OptionsLoader<T> = (query: string, signal: AbortSignal) => Promise<readonly T[]>;\n\n/** 搜索词停下多久再请求(毫秒) */\nconst DEBOUNCE = 300;\n\n/** 远程选项的状态;没给 load 时原样用 items */\nexport function useRemoteItems<T>(read: {\n /** 加载函数;没有时不请求 */\n load: () => OptionsLoader<T> | undefined;\n /** 当前的搜索词 */\n query: () => string;\n /** 弹层是否打开 */\n open: () => boolean;\n /** 本地的选项(有 load 时是第一次结果之前的选项) */\n items: () => readonly T[];\n}) {\n const request = useRequest((query: string, signal) => read.load()!(query, signal), {\n params: () => read.query(),\n debounce: DEBOUNCE,\n immediate: false,\n enabled: () => read.load() != null && read.open(),\n });\n // 打开时立即按当前搜索词请求(不等防抖),一开始就是打开的也算;关闭时取消还在进行的\n watch(\n read.open,\n (open) => {\n if (!read.load()) return;\n if (open) void request.refresh();\n else request.cancel();\n },\n { immediate: true },\n );\n return {\n /** 当前的选项 */\n items: (): readonly T[] => (read.load() ? (request.data ?? read.items()) : read.items()),\n /** 正在请求 */\n loading: () => request.loading,\n
|
|
1
|
+
{"version":3,"file":"remote-items.js","names":[],"sources":["../../src/input/remote-items.ts"],"sourcesContent":["/**\n * 远程选项(Select、Combobox、TreeSelect 的 load):弹层打开时按当前搜索词请求,之后搜索词变了停下 300 ms 再请求;\n * 只采用最后一次的结果(useRequest),结果代替 items。弹层关着不请求。\n * 第一次结果回来之前用 items:编辑页把已选的那一项放进 items,触发框一开始就有文字。\n */\nimport { watch } from 'vue';\nimport { useLang } from '../config/context';\nimport { errorCodeText } from '../utils/request/error-message';\nimport { useRequest } from '../utils/request/use-request';\n\n/** 远程选项的加载函数:搜索词 → 选项;signal 在被后来的请求顶掉、组件卸载时中止 */\nexport type OptionsLoader<T> = (query: string, signal: AbortSignal) => Promise<readonly T[]>;\n\n/** 搜索词停下多久再请求(毫秒) */\nconst DEBOUNCE = 300;\n\n/** 远程选项的状态;没给 load 时原样用 items */\nexport function useRemoteItems<T>(read: {\n /** 加载函数;没有时不请求 */\n load: () => OptionsLoader<T> | undefined;\n /** 当前的搜索词 */\n query: () => string;\n /** 弹层是否打开 */\n open: () => boolean;\n /** 本地的选项(有 load 时是第一次结果之前的选项) */\n items: () => readonly T[];\n}) {\n const lang = useLang();\n const request = useRequest((query: string, signal) => read.load()!(query, signal), {\n params: () => read.query(),\n debounce: DEBOUNCE,\n immediate: false,\n enabled: () => read.load() != null && read.open(),\n });\n // 打开时立即按当前搜索词请求(不等防抖),一开始就是打开的也算;关闭时取消还在进行的\n watch(\n read.open,\n (open) => {\n if (!read.load()) return;\n if (open) void request.refresh();\n else request.cancel();\n },\n { immediate: true },\n );\n return {\n /** 当前的选项 */\n items: (): readonly T[] => (read.load() ? (request.data ?? read.items()) : read.items()),\n /** 正在请求 */\n loading: () => request.loading,\n /**\n * 最后一次请求失败时列表里的提示:错误码有文案时用它(网络断了、没有权限),否则是笼统的“加载失败,请重试”;没失败时为 undefined\n */\n failure: (): string | undefined =>\n request.error === undefined\n ? undefined\n : (errorCodeText(request.error, lang) ?? lang.listLoadFailed),\n };\n}\n"],"mappings":";;;;;;;;;;;AAcA,IAAM,WAAW;;AAGjB,SAAgB,eAAkB,MAS/B;CACD,MAAM,OAAO,QAAQ;CACrB,MAAM,UAAU,YAAY,OAAe,WAAW,KAAK,KAAK,CAAC,CAAE,OAAO,MAAM,GAAG;EACjF,cAAc,KAAK,MAAM;EACzB,UAAU;EACV,WAAW;EACX,eAAe,KAAK,KAAK,KAAK,QAAQ,KAAK,KAAK;CAClD,CAAC;CAED,MACE,KAAK,OACJ,SAAS;EACR,IAAI,CAAC,KAAK,KAAK,GAAG;EAClB,IAAI,MAAM,QAAa,QAAQ;OAC1B,QAAQ,OAAO;CACtB,GACA,EAAE,WAAW,KAAK,CACpB;CACA,OAAO;;EAEL,aAA4B,KAAK,KAAK,IAAK,QAAQ,QAAQ,KAAK,MAAM,IAAK,KAAK,MAAM;;EAEtF,eAAe,QAAQ;;;;EAIvB,eACE,QAAQ,UAAU,KAAA,IACd,KAAA,IACC,cAAc,QAAQ,OAAO,IAAI,KAAK,KAAK;CACpD;AACF"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as css from '@zerodep-dev/vue-tsx-pro-tsx/styles';
|
|
2
2
|
import { type VNodeChild } from 'vue';
|
|
3
3
|
import { type IconButtonProps } from '../base/IconButton';
|
|
4
|
+
import { type AlertProps } from '../feedback/Alert';
|
|
4
5
|
import { type PaginationBarProps } from '../navigation/PaginationBar';
|
|
5
6
|
import type { DataTableColumn } from '../table/columns';
|
|
6
7
|
import { type DataTableProps } from '../table/DataTable';
|
|
@@ -48,6 +49,8 @@ export interface QueryTableProps<T, Filters extends object> extends PartProps<'d
|
|
|
48
49
|
propsSelectionBar?: Partial<SelectionBarProps>;
|
|
49
50
|
/** 分页栏的部件属性 */
|
|
50
51
|
propsPagination?: Partial<PaginationBarProps>;
|
|
52
|
+
/** 加载失败时表格上方的提示条(Alert)的部件属性 */
|
|
53
|
+
propsError?: Partial<AlertProps>;
|
|
51
54
|
/** 工具栏内置按钮(刷新、行高、全屏)的部件属性 */
|
|
52
55
|
propsToolbarButton?: Partial<IconButtonProps>;
|
|
53
56
|
}
|