diff --git a/apps/web-tdesign/src/components/cron-tab/cron-tab.vue b/apps/web-tdesign/src/components/cron-tab/cron-tab.vue new file mode 100644 index 000000000..d6dfaef44 --- /dev/null +++ b/apps/web-tdesign/src/components/cron-tab/cron-tab.vue @@ -0,0 +1,966 @@ + + + + + diff --git a/apps/web-tdesign/src/components/cron-tab/index.ts b/apps/web-tdesign/src/components/cron-tab/index.ts new file mode 100644 index 000000000..8f4baae59 --- /dev/null +++ b/apps/web-tdesign/src/components/cron-tab/index.ts @@ -0,0 +1 @@ +export { default as CronTab } from './cron-tab.vue'; diff --git a/apps/web-tdesign/src/components/cron-tab/types.ts b/apps/web-tdesign/src/components/cron-tab/types.ts new file mode 100644 index 000000000..2adf942b7 --- /dev/null +++ b/apps/web-tdesign/src/components/cron-tab/types.ts @@ -0,0 +1,266 @@ +export interface ShortcutsType { + text: string; + value: string; +} + +export interface CronRange { + start: number | string | undefined; + end: number | string | undefined; +} + +export interface CronLoop { + start: number | string | undefined; + end: number | string | undefined; +} + +export interface CronItem { + type: string; + range: CronRange; + loop: CronLoop; + appoint: string[]; + last?: string; +} + +export interface CronValue { + second: CronItem; + minute: CronItem; + hour: CronItem; + day: CronItem; + month: CronItem; + week: CronItem & { last: string }; + year: CronItem; +} + +export interface WeekOption { + value: string; + label: string; +} + +export interface CronData { + second: string[]; + minute: string[]; + hour: string[]; + day: string[]; + month: string[]; + week: WeekOption[]; + year: number[]; +} + +const getYear = (): number[] => { + const v: number[] = []; + const y = new Date().getFullYear(); + for (let i = 0; i < 11; i++) { + v.push(y + i); + } + return v; +}; + +export const CronValueDefault: CronValue = { + second: { + type: '0', + range: { + start: 1, + end: 2, + }, + loop: { + start: 0, + end: 1, + }, + appoint: [], + }, + minute: { + type: '0', + range: { + start: 1, + end: 2, + }, + loop: { + start: 0, + end: 1, + }, + appoint: [], + }, + hour: { + type: '0', + range: { + start: 1, + end: 2, + }, + loop: { + start: 0, + end: 1, + }, + appoint: [], + }, + day: { + type: '0', + range: { + start: 1, + end: 2, + }, + loop: { + start: 1, + end: 1, + }, + appoint: [], + }, + month: { + type: '0', + range: { + start: 1, + end: 2, + }, + loop: { + start: 1, + end: 1, + }, + appoint: [], + }, + week: { + type: '5', + range: { + start: '2', + end: '3', + }, + loop: { + start: 0, + end: '2', + }, + last: '2', + appoint: [], + }, + year: { + type: '-1', + range: { + start: getYear()[0], + end: getYear()[1], + }, + loop: { + start: getYear()[0], + end: 1, + }, + appoint: [], + }, +}; + +export const CronDataDefault: CronData = { + second: [ + '0', + '5', + '15', + '20', + '25', + '30', + '35', + '40', + '45', + '50', + '55', + '59', + ], + minute: [ + '0', + '5', + '15', + '20', + '25', + '30', + '35', + '40', + '45', + '50', + '55', + '59', + ], + hour: [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '20', + '21', + '22', + '23', + ], + day: [ + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '10', + '11', + '12', + '13', + '14', + '15', + '16', + '17', + '18', + '19', + '20', + '21', + '22', + '23', + '24', + '25', + '26', + '27', + '28', + '29', + '30', + '31', + ], + month: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'], + week: [ + { + value: '1', + label: '周日', + }, + { + value: '2', + label: '周一', + }, + { + value: '3', + label: '周二', + }, + { + value: '4', + label: '周三', + }, + { + value: '5', + label: '周四', + }, + { + value: '6', + label: '周五', + }, + { + value: '7', + label: '周六', + }, + ], + year: getYear(), +}; diff --git a/apps/web-tdesign/src/components/cropper/cropper-avatar.vue b/apps/web-tdesign/src/components/cropper/cropper-avatar.vue new file mode 100644 index 000000000..5acb23fdc --- /dev/null +++ b/apps/web-tdesign/src/components/cropper/cropper-avatar.vue @@ -0,0 +1,125 @@ + + + diff --git a/apps/web-tdesign/src/components/cropper/cropper-modal.vue b/apps/web-tdesign/src/components/cropper/cropper-modal.vue new file mode 100644 index 000000000..35e827a23 --- /dev/null +++ b/apps/web-tdesign/src/components/cropper/cropper-modal.vue @@ -0,0 +1,304 @@ + + + diff --git a/apps/web-tdesign/src/components/cropper/cropper.vue b/apps/web-tdesign/src/components/cropper/cropper.vue new file mode 100644 index 000000000..cc8836c9f --- /dev/null +++ b/apps/web-tdesign/src/components/cropper/cropper.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/apps/web-tdesign/src/components/cropper/index.ts b/apps/web-tdesign/src/components/cropper/index.ts new file mode 100644 index 000000000..43fd89ff3 --- /dev/null +++ b/apps/web-tdesign/src/components/cropper/index.ts @@ -0,0 +1,3 @@ +export { default as CropperAvatar } from './cropper-avatar.vue'; +export { default as CropperImage } from './cropper.vue'; +export type { CropperType } from './typing'; diff --git a/apps/web-tdesign/src/components/cropper/typing.ts b/apps/web-tdesign/src/components/cropper/typing.ts new file mode 100644 index 000000000..8293079b3 --- /dev/null +++ b/apps/web-tdesign/src/components/cropper/typing.ts @@ -0,0 +1,68 @@ +import type Cropper from 'cropperjs'; +import type { ButtonProps } from 'tdesign-vue-next'; + +import type { CSSProperties } from 'vue'; + +export interface apiFunParams { + file: Blob; + filename: string; + name: string; +} + +export interface CropendResult { + imgBase64: string; + imgInfo: Cropper.Data; +} + +export interface CropperProps { + src?: string; + alt?: string; + circled?: boolean; + realTimePreview?: boolean; + height?: number | string; + crossorigin?: '' | 'anonymous' | 'use-credentials' | undefined; + imageStyle?: CSSProperties; + options?: Cropper.Options; +} + +export interface CropperAvatarProps { + width?: number | string; + value?: string; + showBtn?: boolean; + btnProps?: ButtonProps; + btnText?: string; + uploadApi?: (params: apiFunParams) => Promise; + size?: number; +} + +export interface CropperModalProps { + circled?: boolean; + uploadApi?: (params: apiFunParams) => Promise; + src?: string; + size?: number; +} + +export const defaultOptions: Cropper.Options = { + aspectRatio: 1, + zoomable: true, + zoomOnTouch: true, + zoomOnWheel: true, + cropBoxMovable: true, + cropBoxResizable: true, + toggleDragModeOnDblclick: true, + autoCrop: true, + background: true, + highlight: true, + center: true, + responsive: true, + restore: true, + checkCrossOrigin: true, + checkOrientation: true, + scalable: true, + modal: true, + guides: true, + movable: true, + rotatable: true, +}; + +export type { Cropper as CropperType }; diff --git a/apps/web-tdesign/src/components/description/description.vue b/apps/web-tdesign/src/components/description/description.vue new file mode 100644 index 000000000..5cfc75d65 --- /dev/null +++ b/apps/web-tdesign/src/components/description/description.vue @@ -0,0 +1,195 @@ + diff --git a/apps/web-tdesign/src/components/description/index.ts b/apps/web-tdesign/src/components/description/index.ts new file mode 100644 index 000000000..a707c4865 --- /dev/null +++ b/apps/web-tdesign/src/components/description/index.ts @@ -0,0 +1,3 @@ +export { default as Description } from './description.vue'; +export * from './typing'; +export { useDescription } from './use-description'; diff --git a/apps/web-tdesign/src/components/description/typing.ts b/apps/web-tdesign/src/components/description/typing.ts new file mode 100644 index 000000000..d73a067c5 --- /dev/null +++ b/apps/web-tdesign/src/components/description/typing.ts @@ -0,0 +1,45 @@ +import type { DescriptionsProps as TdDescriptionsProps } from 'tdesign-vue-next'; +import type { JSX } from 'vue/jsx-runtime'; + +import type { CSSProperties, VNode } from 'vue'; + +import type { Recordable } from '@vben/types'; + +export interface DescriptionItemSchema { + labelMinWidth?: number; + contentMinWidth?: number; + // 自定义标签样式 + labelStyle?: CSSProperties; + // 对应 data 中的字段名 + field: string; + // 内容的描述 + label: JSX.Element | string | VNode; + // 包含列的数量 + span?: number; + // 是否显示 + show?: (...arg: any) => boolean; + // 插槽名称 + slot?: string; + // 自定义需要展示的内容 + render?: ( + val: any, + data?: Recordable, + ) => Element | JSX.Element | number | string | undefined | VNode; +} + +export interface DescriptionProps extends TdDescriptionsProps { + // 是否包含卡片组件 + useCard?: boolean; + // 描述项配置 + schema: DescriptionItemSchema[]; + // 数据 + data: Recordable; + // 标题 + title?: string; + // 是否包含边框 + bordered?: boolean; +} + +export interface DescInstance { + setDescProps(descProps: Partial): void; +} diff --git a/apps/web-tdesign/src/components/description/use-description.ts b/apps/web-tdesign/src/components/description/use-description.ts new file mode 100644 index 000000000..fd24920f0 --- /dev/null +++ b/apps/web-tdesign/src/components/description/use-description.ts @@ -0,0 +1,31 @@ +import type { Component } from 'vue'; + +import type { DescInstance, DescriptionProps } from './typing'; + +import { h, reactive } from 'vue'; + +import Description from './description.vue'; + +export function useDescription(options?: Partial) { + const propsState = reactive>(options || {}); + + const api: DescInstance = { + setDescProps: (descProps: Partial): void => { + Object.assign(propsState, descProps); + }, + }; + + // 创建一个包装组件,将 propsState 合并到 props 中 + const DescriptionWrapper: Component = { + name: 'UseDescription', + inheritAttrs: false, + setup(_props, { attrs, slots }) { + return () => { + // @ts-ignore - 避免类型实例化过深 + return h(Description, { ...propsState, ...attrs }, slots); + }; + }, + }; + + return [DescriptionWrapper, api] as const; +} diff --git a/apps/web-tdesign/src/components/dict-tag/dict-tag.vue b/apps/web-tdesign/src/components/dict-tag/dict-tag.vue new file mode 100644 index 000000000..c40ed80e4 --- /dev/null +++ b/apps/web-tdesign/src/components/dict-tag/dict-tag.vue @@ -0,0 +1,82 @@ + + + diff --git a/apps/web-tdesign/src/components/dict-tag/index.ts b/apps/web-tdesign/src/components/dict-tag/index.ts new file mode 100644 index 000000000..881265a39 --- /dev/null +++ b/apps/web-tdesign/src/components/dict-tag/index.ts @@ -0,0 +1 @@ +export { default as DictTag } from './dict-tag.vue'; diff --git a/apps/web-tdesign/src/components/table-action/icons.ts b/apps/web-tdesign/src/components/table-action/icons.ts new file mode 100644 index 000000000..df5bf1a77 --- /dev/null +++ b/apps/web-tdesign/src/components/table-action/icons.ts @@ -0,0 +1,14 @@ +export const ACTION_ICON = { + DOWNLOAD: 'lucide:download', + UPLOAD: 'lucide:upload', + ADD: 'lucide:plus', + EDIT: 'lucide:edit', + DELETE: 'lucide:trash-2', + REFRESH: 'lucide:refresh-cw', + SEARCH: 'lucide:search', + FILTER: 'lucide:filter', + MORE: 'lucide:ellipsis-vertical', + VIEW: 'lucide:eye', + COPY: 'lucide:copy', + CLOSE: 'lucide:x', +}; diff --git a/apps/web-tdesign/src/components/table-action/index.ts b/apps/web-tdesign/src/components/table-action/index.ts new file mode 100644 index 000000000..672c0a533 --- /dev/null +++ b/apps/web-tdesign/src/components/table-action/index.ts @@ -0,0 +1,4 @@ +export * from './icons'; + +export { default as TableAction } from './table-action.vue'; +export * from './typing'; diff --git a/apps/web-tdesign/src/components/table-action/table-action.vue b/apps/web-tdesign/src/components/table-action/table-action.vue new file mode 100644 index 000000000..8c4fda41a --- /dev/null +++ b/apps/web-tdesign/src/components/table-action/table-action.vue @@ -0,0 +1,282 @@ + + + + + + diff --git a/apps/web-tdesign/src/components/table-action/typing.ts b/apps/web-tdesign/src/components/table-action/typing.ts new file mode 100644 index 000000000..a54e58468 --- /dev/null +++ b/apps/web-tdesign/src/components/table-action/typing.ts @@ -0,0 +1,32 @@ +import type { TdButtonProps, TooltipProps } from 'tdesign-vue-next'; + +export interface PopConfirm { + title: string; + okText?: string; + cancelText?: string; + confirm: () => void; + cancel?: () => void; + icon?: string; + disabled?: boolean; +} + +export interface ActionItem { + onClick?: () => void; + type?: TdButtonProps['theme']; + label?: string; + icon?: string; + color?: 'error' | 'success' | 'warning'; + popConfirm?: PopConfirm; + disabled?: boolean; + divider?: boolean; + // 权限编码控制是否显示 + auth?: string[]; + // 业务控制是否显示 + ifShow?: ((action: ActionItem) => boolean) | boolean; + tooltip?: string | TooltipProps; + loading?: boolean; + size?: TdButtonProps['size']; + shape?: TdButtonProps['shape']; + variant?: TdButtonProps['variant']; + danger?: boolean; +} diff --git a/apps/web-tdesign/src/components/tinymce/editor.vue b/apps/web-tdesign/src/components/tinymce/editor.vue new file mode 100644 index 000000000..722073cf7 --- /dev/null +++ b/apps/web-tdesign/src/components/tinymce/editor.vue @@ -0,0 +1,344 @@ + + + + + + diff --git a/apps/web-tdesign/src/components/tinymce/helper.ts b/apps/web-tdesign/src/components/tinymce/helper.ts new file mode 100644 index 000000000..1f98dda46 --- /dev/null +++ b/apps/web-tdesign/src/components/tinymce/helper.ts @@ -0,0 +1,85 @@ +const validEvents = new Set([ + 'onActivate', + 'onAddUndo', + 'onBeforeAddUndo', + 'onBeforeExecCommand', + 'onBeforeGetContent', + 'onBeforePaste', + 'onBeforeRenderUI', + 'onBeforeSetContent', + 'onBlur', + 'onChange', + 'onClearUndos', + 'onClick', + 'onContextMenu', + 'onCopy', + 'onCut', + 'onDblclick', + 'onDeactivate', + 'onDirty', + 'onDrag', + 'onDragDrop', + 'onDragEnd', + 'onDragGesture', + 'onDragOver', + 'onDrop', + 'onExecCommand', + 'onFocus', + 'onFocusIn', + 'onFocusOut', + 'onGetContent', + 'onHide', + 'onInit', + 'onKeyDown', + 'onKeyPress', + 'onKeyUp', + 'onLoadContent', + 'onMouseDown', + 'onMouseEnter', + 'onMouseLeave', + 'onMouseMove', + 'onMouseOut', + 'onMouseOver', + 'onMouseUp', + 'onNodeChange', + 'onObjectResized', + 'onObjectResizeStart', + 'onObjectSelected', + 'onPaste', + 'onPostProcess', + 'onPostRender', + 'onPreProcess', + 'onProgressState', + 'onRedo', + 'onRemove', + 'onReset', + 'onSaveContent', + 'onSelectionChange', + 'onSetAttrib', + 'onSetContent', + 'onShow', + 'onSubmit', + 'onUndo', + 'onVisualAid', +]); + +const isValidKey = (key: string) => validEvents.has(key); + +export const bindHandlers = ( + initEvent: Event, + listeners: any, + editor: any, +): void => { + Object.keys(listeners) + .filter((element) => isValidKey(element)) + .forEach((key: string) => { + const handler = listeners[key]; + if (typeof handler === 'function') { + if (key === 'onInit') { + handler(initEvent, editor); + } else { + editor.on(key.slice(2), (e: any) => handler(e, editor)); + } + } + }); +}; diff --git a/apps/web-tdesign/src/components/tinymce/img-upload.vue b/apps/web-tdesign/src/components/tinymce/img-upload.vue new file mode 100644 index 000000000..ee300d1ae --- /dev/null +++ b/apps/web-tdesign/src/components/tinymce/img-upload.vue @@ -0,0 +1,111 @@ + + + + diff --git a/apps/web-tdesign/src/components/tinymce/index.ts b/apps/web-tdesign/src/components/tinymce/index.ts new file mode 100644 index 000000000..c277d781d --- /dev/null +++ b/apps/web-tdesign/src/components/tinymce/index.ts @@ -0,0 +1 @@ +export { default as Tinymce } from './editor.vue'; diff --git a/apps/web-tdesign/src/components/tinymce/tinymce.ts b/apps/web-tdesign/src/components/tinymce/tinymce.ts new file mode 100644 index 000000000..45a867b61 --- /dev/null +++ b/apps/web-tdesign/src/components/tinymce/tinymce.ts @@ -0,0 +1,17 @@ +// Any plugins you want to setting has to be imported +// Detail plugins list see https://www.tiny.cloud/docs/plugins/ +// Custom builds see https://www.tiny.cloud/download/custom-builds/ +// colorpicker/contextmenu/textcolor plugin is now built in to the core editor, please remove it from your editor configuration + +export const plugins = + 'preview importcss searchreplace autolink autosave save directionality code visualblocks visualchars fullscreen image link media codesample table charmap pagebreak nonbreaking anchor insertdatetime advlist lists wordcount help emoticons accordion'; + +// 和 vben2.0 不同,从 https://www.tiny.cloud/ 拷贝 Vue 部分,然后去掉 importword exportword exportpdf | math 部分,并额外增加最后一行(来自 vben2.0 差异的部分) +export const toolbar = + 'undo redo | accordion accordionremove | \\\n' + + ' blocks fontfamily fontsize | bold italic underline strikethrough | \\\n' + + ' align numlist bullist | link image | table media | \\\n' + + ' lineheight outdent indent | forecolor backcolor removeformat | \\\n' + + ' charmap emoticons | code fullscreen preview | save print | \\\n' + + ' pagebreak anchor codesample | ltr rtl | \\\n' + + ' hr searchreplace alignleft aligncenter alignright blockquote subscript superscript'; diff --git a/apps/web-tdesign/src/components/upload/file-upload.vue b/apps/web-tdesign/src/components/upload/file-upload.vue new file mode 100644 index 000000000..668197711 --- /dev/null +++ b/apps/web-tdesign/src/components/upload/file-upload.vue @@ -0,0 +1,381 @@ + + + + + diff --git a/apps/web-tdesign/src/components/upload/helper.ts b/apps/web-tdesign/src/components/upload/helper.ts new file mode 100644 index 000000000..27313cea6 --- /dev/null +++ b/apps/web-tdesign/src/components/upload/helper.ts @@ -0,0 +1,20 @@ +/** + * 默认图片类型 + */ +export const defaultImageAccepts = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + +export function checkFileType(file: File, accepts: string[]) { + if (!accepts || accepts.length === 0) { + return true; + } + const newTypes = accepts.join('|'); + const reg = new RegExp(`${String.raw`\.(` + newTypes})$`, 'i'); + return reg.test(file.name); +} + +export function checkImgType( + file: File, + accepts: string[] = defaultImageAccepts, +) { + return checkFileType(file, accepts); +} diff --git a/apps/web-tdesign/src/components/upload/image-upload.vue b/apps/web-tdesign/src/components/upload/image-upload.vue new file mode 100644 index 000000000..14e3776c4 --- /dev/null +++ b/apps/web-tdesign/src/components/upload/image-upload.vue @@ -0,0 +1,362 @@ + + + + + diff --git a/apps/web-tdesign/src/components/upload/index.ts b/apps/web-tdesign/src/components/upload/index.ts new file mode 100644 index 000000000..14e57fede --- /dev/null +++ b/apps/web-tdesign/src/components/upload/index.ts @@ -0,0 +1,3 @@ +export { default as FileUpload } from './file-upload.vue'; +export { default as ImageUpload } from './image-upload.vue'; +export { default as InputUpload } from './input-upload.vue'; diff --git a/apps/web-tdesign/src/components/upload/input-upload.vue b/apps/web-tdesign/src/components/upload/input-upload.vue new file mode 100644 index 000000000..353e3c5d4 --- /dev/null +++ b/apps/web-tdesign/src/components/upload/input-upload.vue @@ -0,0 +1,74 @@ + +