diff --git a/.gitpod.yml b/.gitpod.yml index fb75b433d..5fda2cf70 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -2,5 +2,5 @@ ports: - port: 5555 onOpen: open-preview tasks: - - init: corepack enable && pnpm install + - init: npm i -g corepack && pnpm install command: pnpm run dev:play diff --git a/.vscode/settings.json b/.vscode/settings.json index da724dd17..8b76b2762 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -223,16 +223,5 @@ "commentTranslate.multiLineMerge": true, "vue.server.hybridMode": true, "typescript.tsdk": "node_modules/typescript/lib", - "oxc.enable": false, - "cSpell.words": [ - "archiver", - "axios", - "dotenv", - "isequal", - "jspm", - "napi", - "nolebase", - "rollup", - "vitest" - ] + "oxc.enable": false } diff --git a/apps/web-antd/.env b/apps/web-antd/.env index abac77511..7cb021270 100644 --- a/apps/web-antd/.env +++ b/apps/web-antd/.env @@ -3,6 +3,10 @@ VITE_APP_TITLE=芋道管理系统 # 应用命名空间,用于缓存、store等功能的前缀,确保隔离 VITE_APP_NAMESPACE=yudao-vben-antd + +# 对store进行加密的密钥,在将store持久化到localStorage时会使用该密钥进行加密 +VITE_APP_STORE_SECURE_KEY=please-replace-me-with-your-own-key + # 是否开启模拟数据 VITE_NITRO_MOCK=false @@ -16,4 +20,4 @@ VITE_APP_CAPTCHA_ENABLE=false VITE_APP_DOCALERT_ENABLE=true # 百度统计 -VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093 \ No newline at end of file +VITE_APP_BAIDU_CODE = e98f2eab6ceb8688bc6d8fc5332ff093 diff --git a/apps/web-antd/package.json b/apps/web-antd/package.json index d7e71449a..0e5589863 100644 --- a/apps/web-antd/package.json +++ b/apps/web-antd/package.json @@ -1,6 +1,6 @@ { "name": "@vben/web-antd", - "version": "5.5.4", + "version": "5.5.5", "homepage": "https://vben.pro", "bugs": "https://github.com/vbenjs/vue-vben-admin/issues", "repository": { @@ -26,6 +26,8 @@ "#/*": "./src/*" }, "dependencies": { + "@form-create/ant-design-vue": "catalog:", + "@form-create/antd-designer": "catalog:", "@tinymce/tinymce-vue": "catalog:", "@vben/access": "workspace:*", "@vben/common-ui": "workspace:*", @@ -49,7 +51,9 @@ "highlight.js": "catalog:", "pinia": "catalog:", "vue": "catalog:", - "vue-router": "catalog:" + "vue-dompurify-html": "catalog:", + "vue-router": "catalog:", + "vxe-table": "catalog:" }, "devDependencies": { "@types/crypto-js": "catalog:" diff --git a/apps/web-antd/public/wx-xingyu.png b/apps/web-antd/public/wx-xingyu.png new file mode 100644 index 000000000..5e4b6017d Binary files /dev/null and b/apps/web-antd/public/wx-xingyu.png differ diff --git a/apps/web-antd/src/adapter/component/index.ts b/apps/web-antd/src/adapter/component/index.ts index 9cc430135..71133647c 100644 --- a/apps/web-antd/src/adapter/component/index.ts +++ b/apps/web-antd/src/adapter/component/index.ts @@ -76,8 +76,8 @@ const withDefaultPlaceholder = ( componentProps: Recordable = {}, ) => { return defineComponent({ - inheritAttrs: false, name: component.name, + inheritAttrs: false, setup: (props: any, { attrs, expose, slots }) => { const placeholder = props?.placeholder || @@ -142,20 +142,34 @@ async function initComponentAdapter() { // 如果你的组件体积比较大,可以使用异步加载 // Button: () => // import('xxx').then((res) => res.Button), - ApiSelect: withDefaultPlaceholder(ApiComponent, 'select', { - component: Select, - loadingSlot: 'suffixIcon', - visibleEvent: 'onDropdownVisibleChange', - modelPropName: 'value', - }), - ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', { - component: TreeSelect, - fieldNames: { label: 'label', value: 'value', children: 'children' }, - loadingSlot: 'suffixIcon', - modelPropName: 'value', - optionsPropName: 'treeData', - visibleEvent: 'onVisibleChange', - }), + ApiSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiSelect', + }, + 'select', + { + component: Select, + loadingSlot: 'suffixIcon', + visibleEvent: 'onDropdownVisibleChange', + modelPropName: 'value', + }, + ), + ApiTreeSelect: withDefaultPlaceholder( + { + ...ApiComponent, + name: 'ApiTreeSelect', + }, + 'select', + { + component: TreeSelect, + fieldNames: { label: 'label', value: 'value', children: 'children' }, + loadingSlot: 'suffixIcon', + modelPropName: 'value', + optionsPropName: 'treeData', + visibleEvent: 'onVisibleChange', + }, + ), AutoComplete, Checkbox, CheckboxGroup, diff --git a/apps/web-antd/src/api/core/auth.ts b/apps/web-antd/src/api/core/auth.ts index c71f5f598..ccb6da340 100644 --- a/apps/web-antd/src/api/core/auth.ts +++ b/apps/web-antd/src/api/core/auth.ts @@ -119,39 +119,39 @@ export async function checkCaptcha(data: any) { } /** 获取登录验证码 */ -export const sendSmsCode = (data: AuthApi.SmsCodeParams) => { +export async function sendSmsCode(data: AuthApi.SmsCodeParams) { return requestClient.post('/system/auth/send-sms-code', data); -}; +} /** 短信验证码登录 */ -export const smsLogin = (data: AuthApi.SmsLoginParams) => { +export async function smsLogin(data: AuthApi.SmsLoginParams) { return requestClient.post('/system/auth/sms-login', data); -}; +} /** 注册 */ -export const register = (data: AuthApi.RegisterParams) => { +export async function register(data: AuthApi.RegisterParams) { return requestClient.post('/system/auth/register', data); -}; +} /** 通过短信重置密码 */ -export const smsResetPassword = (data: AuthApi.ResetPasswordParams) => { +export async function smsResetPassword(data: AuthApi.ResetPasswordParams) { return requestClient.post('/system/auth/reset-password', data); -}; +} /** 社交授权的跳转 */ -export const socialAuthRedirect = (type: number, redirectUri: string) => { +export async function socialAuthRedirect(type: number, redirectUri: string) { return requestClient.get('/system/auth/social-auth-redirect', { params: { type, redirectUri, }, }); -}; +} /** 社交快捷登录 */ -export const socialLogin = (data: AuthApi.SocialLoginParams) => { +export async function socialLogin(data: AuthApi.SocialLoginParams) { return requestClient.post( '/system/auth/social-login', data, ); -}; +} diff --git a/apps/web-antd/src/api/infra/file-config/index.ts b/apps/web-antd/src/api/infra/file-config/index.ts index e8371814e..a16cf2bc0 100644 --- a/apps/web-antd/src/api/infra/file-config/index.ts +++ b/apps/web-antd/src/api/infra/file-config/index.ts @@ -15,6 +15,7 @@ export namespace InfraFileConfigApi { bucket?: string; accessKey?: string; accessSecret?: string; + pathStyle?: boolean; domain: string; } diff --git a/apps/web-antd/src/bootstrap.ts b/apps/web-antd/src/bootstrap.ts index e4aaf4057..43fd96288 100644 --- a/apps/web-antd/src/bootstrap.ts +++ b/apps/web-antd/src/bootstrap.ts @@ -1,4 +1,5 @@ import { createApp, watchEffect } from 'vue'; +import VueDOMPurifyHTML from 'vue-dompurify-html'; import { registerAccessDirective } from '@vben/access'; import { registerLoadingDirective } from '@vben/common-ui/es/loading'; @@ -10,11 +11,14 @@ import '@vben/styles/antd'; import { useTitle } from '@vueuse/core'; import { $t, setupI18n } from '#/locales'; +import { setupFormCreate } from '#/plugins/form-create'; import { initComponentAdapter } from './adapter/component'; import App from './app.vue'; import { router } from './router'; +import 'vxe-table/styles/cssvar.scss'; // TODO @puhui999:这个必须导入哇?我看 use-vxe-grid.vue 已经导入了 + async function bootstrap(namespace: string) { // 初始化组件适配器 await initComponentAdapter(); @@ -39,7 +43,7 @@ async function bootstrap(namespace: string) { // 国际化 i18n 配置 await setupI18n(app); - // 配置 pinia-tore + // 配置 pinia-store await initStores(app, { namespace }); // 安装权限指令 @@ -52,6 +56,13 @@ async function bootstrap(namespace: string) { // 配置路由及路由守卫 app.use(router); + // formCreate + setupFormCreate(app); + + // vue-dompurify-html + // TODO @dhb52:VueDOMPurifyHTML 是不是不用引入哈? + app.use(VueDOMPurifyHTML); + // 配置Motion插件 const { MotionPlugin } = await import('@vben/plugins/motion'); app.use(MotionPlugin); diff --git a/apps/web-antd/src/components/content-wrap/content-wrap.vue b/apps/web-antd/src/components/content-wrap/content-wrap.vue new file mode 100644 index 000000000..306e0fa6d --- /dev/null +++ b/apps/web-antd/src/components/content-wrap/content-wrap.vue @@ -0,0 +1,29 @@ + + + + diff --git a/apps/web-antd/src/components/content-wrap/index.ts b/apps/web-antd/src/components/content-wrap/index.ts new file mode 100644 index 000000000..d4f95fddb --- /dev/null +++ b/apps/web-antd/src/components/content-wrap/index.ts @@ -0,0 +1 @@ +export { default as ContentWrap } from './content-wrap.vue'; diff --git a/apps/web-antd/src/components/description/description.vue b/apps/web-antd/src/components/description/description.vue new file mode 100644 index 000000000..25dab3cec --- /dev/null +++ b/apps/web-antd/src/components/description/description.vue @@ -0,0 +1,80 @@ + diff --git a/apps/web-antd/src/components/description/index.ts b/apps/web-antd/src/components/description/index.ts new file mode 100644 index 000000000..a707c4865 --- /dev/null +++ b/apps/web-antd/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-antd/src/components/description/typing.ts b/apps/web-antd/src/components/description/typing.ts new file mode 100644 index 000000000..c1628d248 --- /dev/null +++ b/apps/web-antd/src/components/description/typing.ts @@ -0,0 +1,27 @@ +import type { DescriptionsProps } from 'ant-design-vue'; + +import type { CSSProperties, VNode } from 'vue'; + +// TODO @puhui999:【content】这个纠结下;1)vben2.0 是 render;https://doc.vvbin.cn/components/desc.html#usage 2) +// TODO @puhui999:vben2.0 还有 sapn【done】、labelMinWidth、contentMinWidth +// TODO @puhui999:【hidden】这个纠结下;1)vben2.0 是 show; +export interface DescriptionItemSchema { + label: string | VNode; // 内容的描述 + field?: string; // 对应 data 中的字段名 + content?: ((data: any) => string | VNode) | string | VNode; // 自定义需要展示的内容,比如说 dict-tag + span?: number; // 包含列的数量 + labelStyle?: CSSProperties; // 自定义标签样式 + contentStyle?: CSSProperties; // 自定义内容样式 + hidden?: ((data: any) => boolean) | boolean; // 是否显示 +} + +// TODO @puhui999:vben2.0 还有 title【done】、bordered【done】d、useCollapse、collapseOptions +// TODO @puhui999:from 5.0:bordered 默认为 true +// TODO @puhui999:from 5.0:column 默认为 lg: 3, md: 3, sm: 2, xl: 3, xs: 1, xxl: 4 +// TODO @puhui999:from 5.0:size 默认为 small;有 'default', 'middle', 'small', undefined +// TODO @puhui999:from 5.0:useCollapse 默认为 true +export interface DescriptionsOptions { + data?: Record; // 数据 + schema?: DescriptionItemSchema[]; // 描述项配置 + componentProps?: DescriptionsProps; // antd Descriptions 组件参数 +} diff --git a/apps/web-antd/src/components/description/use-description.ts b/apps/web-antd/src/components/description/use-description.ts new file mode 100644 index 000000000..5140a88c1 --- /dev/null +++ b/apps/web-antd/src/components/description/use-description.ts @@ -0,0 +1,71 @@ +import type { DescriptionsOptions } from './typing'; + +import { defineComponent, h, isReactive, reactive, watch } from 'vue'; + +import { Description } from './index'; + +/** 描述列表 api 定义 */ +class DescriptionApi { + private state = reactive>({}); + + constructor(options: DescriptionsOptions) { + this.state = { ...options }; + } + + getState(): DescriptionsOptions { + return this.state as DescriptionsOptions; + } + + // TODO @puhui999:【setState】纠结下:1)vben2.0 是 data https://doc.vvbin.cn/components/desc.html#usage; + setState(newState: Partial) { + this.state = { ...this.state, ...newState }; + } +} + +export type ExtendedDescriptionApi = DescriptionApi; + +export function useDescription(options: DescriptionsOptions) { + const IS_REACTIVE = isReactive(options); + const api = new DescriptionApi(options); + // 扩展API + const extendedApi: ExtendedDescriptionApi = api as never; + const Desc = defineComponent({ + name: 'UseDescription', + inheritAttrs: false, + setup(_, { attrs, slots }) { + // 合并props和attrs到state + api.setState({ ...attrs }); + + return () => + h( + Description, + { + ...api.getState(), + ...attrs, + }, + slots, + ); + }, + }); + + // 响应式支持 + if (IS_REACTIVE) { + watch( + () => options.schema, + (newSchema) => { + api.setState({ schema: newSchema }); + }, + { immediate: true, deep: true }, + ); + + watch( + () => options.data, + (newData) => { + api.setState({ data: newData }); + }, + { immediate: true, deep: true }, + ); + } + + return [Desc, extendedApi] as const; +} diff --git a/apps/web-antd/src/components/dict-tag/dict-tag.vue b/apps/web-antd/src/components/dict-tag/dict-tag.vue index d000b6669..9e1825e08 100644 --- a/apps/web-antd/src/components/dict-tag/dict-tag.vue +++ b/apps/web-antd/src/components/dict-tag/dict-tag.vue @@ -41,17 +41,14 @@ const dictTag = computed(() => { switch (colorType) { case 'danger': { colorType = 'error'; - break; } case 'info': { colorType = 'default'; - break; } case 'primary': { colorType = 'processing'; - break; } default: { diff --git a/apps/web-antd/src/components/form-create/components/dict-select.vue b/apps/web-antd/src/components/form-create/components/dict-select.vue new file mode 100644 index 000000000..e6e198f06 --- /dev/null +++ b/apps/web-antd/src/components/form-create/components/dict-select.vue @@ -0,0 +1,75 @@ + + + + diff --git a/apps/web-antd/src/components/form-create/components/use-api-select.tsx b/apps/web-antd/src/components/form-create/components/use-api-select.tsx new file mode 100644 index 000000000..a70fa6018 --- /dev/null +++ b/apps/web-antd/src/components/form-create/components/use-api-select.tsx @@ -0,0 +1,290 @@ +import type { ApiSelectProps } from '#/components/form-create/typing'; + +import { defineComponent, onMounted, ref, useAttrs } from 'vue'; + +import { isEmpty } from '@vben/utils'; + +import { + Checkbox, + CheckboxGroup, + Radio, + RadioGroup, + Select, + SelectOption, +} from 'ant-design-vue'; + +import { requestClient } from '#/api/request'; + +export const useApiSelect = (option: ApiSelectProps) => { + return defineComponent({ + name: option.name, + props: { + // 选项标签 + labelField: { + type: String, + default: () => option.labelField ?? 'label', + }, + // 选项的值 + valueField: { + type: String, + default: () => option.valueField ?? 'value', + }, + // api 接口 + url: { + type: String, + default: () => option.url ?? '', + }, + // 请求类型 + method: { + type: String, + default: 'GET', + }, + // 选项解析函数 + parseFunc: { + type: String, + default: '', + }, + // 请求参数 + data: { + type: String, + default: '', + }, + // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio + selectType: { + type: String, + default: 'select', + }, + // 是否多选 + multiple: { + type: Boolean, + default: false, + }, + // 是否远程搜索 + remote: { + type: Boolean, + default: false, + }, + // 远程搜索时携带的参数 + remoteField: { + type: String, + default: 'label', + }, + }, + setup(props) { + const attrs = useAttrs(); + const options = ref([]); // 下拉数据 + const loading = ref(false); // 是否正在从远程获取数据 + const queryParam = ref(); // 当前输入的值 + const getOptions = async () => { + options.value = []; + // 接口选择器 + if (isEmpty(props.url)) { + return; + } + + switch (props.method) { + case 'GET': { + let url: string = props.url; + if (props.remote && queryParam.value !== undefined) { + url = url.includes('?') + ? `${url}&${props.remoteField}=${queryParam.value}` + : `${url}?${props.remoteField}=${queryParam.value}`; + } + parseOptions(await requestClient.get(url)); + break; + } + case 'POST': { + const data: any = JSON.parse(props.data); + if (props.remote) { + data[props.remoteField] = queryParam.value; + } + parseOptions(await requestClient.post(props.url, data)); + break; + } + } + }; + + function parseOptions(data: any) { + // 情况一:如果有自定义解析函数优先使用自定义解析 + if (!isEmpty(props.parseFunc)) { + options.value = parseFunc()?.(data); + return; + } + // 情况二:返回的直接是一个列表 + if (Array.isArray(data)) { + parseOptions0(data); + return; + } + // 情况二:返回的是分页数据,尝试读取 list + data = data.list; + if (!!data && Array.isArray(data)) { + parseOptions0(data); + return; + } + // 情况三:不是 yudao-vue-pro 标准返回 + console.warn( + `接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理`, + ); + } + + function parseOptions0(data: any[]) { + if (Array.isArray(data)) { + options.value = data.map((item: any) => ({ + label: parseExpression(item, props.labelField), + value: parseExpression(item, props.valueField), + })); + return; + } + console.warn(`接口[${props.url}] 返回结果不是一个数组`); + } + + function parseFunc() { + let parse: any = null; + if (props.parseFunc) { + // 解析字符串函数 + // eslint-disable-next-line no-new-func + parse = new Function(`return ${props.parseFunc}`)(); + } + return parse; + } + + function parseExpression(data: any, template: string) { + // 检测是否使用了表达式 + if (!template.includes('${')) { + return data[template]; + } + // 正则表达式匹配模板字符串中的 ${...} + const pattern = /\$\{([^}]*)\}/g; + // 使用replace函数配合正则表达式和回调函数来进行替换 + return template.replaceAll(pattern, (_, expr) => { + // expr 是匹配到的 ${} 内的表达式(这里是属性名),从 data 中获取对应的值 + const result = data[expr.trim()]; // 去除前后空白,以防用户输入带空格的属性名 + if (!result) { + console.warn( + `接口选择器选项模版[${template}][${expr.trim()}] 解析值失败结果为[${result}], 请检查属性名称是否存在于接口返回值中,存在则忽略此条!!!`, + ); + } + return result; + }); + } + + const remoteMethod = async (query: any) => { + if (!query) { + return; + } + loading.value = true; + try { + queryParam.value = query; + await getOptions(); + } finally { + loading.value = false; + } + }; + + onMounted(async () => { + await getOptions(); + }); + + const buildSelect = () => { + if (props.multiple) { + // fix:多写此步是为了解决 multiple 属性问题 + return ( + + ); + } + return ( + + ); + }; + const buildCheckbox = () => { + if (isEmpty(options.value)) { + options.value = [ + { label: '选项1', value: '选项1' }, + { label: '选项2', value: '选项2' }, + ]; + } + return ( + + {options.value.map( + (item: { label: any; value: any }, index: any) => ( + + {item.label} + + ), + )} + + ); + }; + const buildRadio = () => { + if (isEmpty(options.value)) { + options.value = [ + { label: '选项1', value: '选项1' }, + { label: '选项2', value: '选项2' }, + ]; + } + return ( + + {options.value.map( + (item: { label: any; value: any }, index: any) => ( + + {item.label} + + ), + )} + + ); + }; + return () => ( + <> + {(() => { + switch (props.selectType) { + case 'checkbox': { + return buildCheckbox(); + } + case 'radio': { + return buildRadio(); + } + case 'select': { + return buildSelect(); + } + default: { + return buildSelect(); + } + } + })()} + + ); + }, + }); +}; diff --git a/apps/web-antd/src/components/form-create/components/use-images-upload.tsx b/apps/web-antd/src/components/form-create/components/use-images-upload.tsx new file mode 100644 index 000000000..08b27c597 --- /dev/null +++ b/apps/web-antd/src/components/form-create/components/use-images-upload.tsx @@ -0,0 +1,25 @@ +import { defineComponent } from 'vue'; + +import ImageUpload from '#/components/upload/image-upload.vue'; + +export const useImagesUpload = () => { + return defineComponent({ + props: { + multiple: { + type: Boolean, + default: true, + }, + maxNumber: { + type: Number, + default: 5, + }, + }, + setup() { + // TODO: @dhb52 其实还是靠 props 默认参数起作用,没能从 formCreate 传递 + return (props: { maxNumber?: number; multiple?: boolean }) => ( + + ); + }, + name: 'ImagesUpload', + }); +}; diff --git a/apps/web-antd/src/components/form-create/helpers.ts b/apps/web-antd/src/components/form-create/helpers.ts new file mode 100644 index 000000000..c647711c4 --- /dev/null +++ b/apps/web-antd/src/components/form-create/helpers.ts @@ -0,0 +1,182 @@ +import type { Ref } from 'vue'; + +import type { Menu } from '#/components/form-create/typing'; + +import { nextTick, onMounted } from 'vue'; + +import { apiSelectRule } from '#/components/form-create/rules/data'; + +import { + useDictSelectRule, + useEditorRule, + useSelectRule, + useUploadFileRule, + useUploadImageRule, + useUploadImagesRule, +} from './rules'; + +export function makeRequiredRule() { + return { + type: 'Required', + field: 'formCreate$required', + title: '是否必填', + }; +} + +export const localeProps = ( + t: (msg: string) => any, + prefix: string, + rules: any[], +) => { + return rules.map((rule: { field: string; title: any }) => { + if (rule.field === 'formCreate$required') { + rule.title = t('props.required') || rule.title; + } else if (rule.field && rule.field !== '_optionType') { + rule.title = t(`components.${prefix}.${rule.field}`) || rule.title; + } + return rule; + }); +}; + +/** + * 解析表单组件的 field, title 等字段(递归,如果组件包含子组件) + * + * @param rule 组件的生成规则 https://www.form-create.com/v3/guide/rule + * @param fields 解析后表单组件字段 + * @param parentTitle 如果是子表单,子表单的标题,默认为空 + */ +export const parseFormFields = ( + rule: Record, + fields: Array> = [], + parentTitle: string = '', +) => { + const { type, field, $required, title: tempTitle, children } = rule; + if (field && tempTitle) { + let title = tempTitle; + if (parentTitle) { + title = `${parentTitle}.${tempTitle}`; + } + let required = false; + if ($required) { + required = true; + } + fields.push({ + field, + title, + type, + required, + }); + // TODO 子表单 需要处理子表单字段 + // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) { + // // 解析子表单的字段 + // rule.props.rule.forEach((item) => { + // parseFields(item, fieldsPermission, title) + // }) + // } + } + if (children && Array.isArray(children)) { + children.forEach((rule) => { + parseFormFields(rule, fields); + }); + } +}; + +/** + * 表单设计器增强 hook + * 新增 + * - 文件上传 + * - 单图上传 + * - 多图上传 + * - 字典选择器 + * - 用户选择器 + * - 部门选择器 + * - 富文本 + */ +export const useFormCreateDesigner = async (designer: Ref) => { + const editorRule = useEditorRule(); + const uploadFileRule = useUploadFileRule(); + const uploadImageRule = useUploadImageRule(); + const uploadImagesRule = useUploadImagesRule(); + + /** + * 构建表单组件 + */ + const buildFormComponents = () => { + // 移除自带的上传组件规则,使用 uploadFileRule、uploadImgRule、uploadImgsRule 替代 + designer.value?.removeMenuItem('upload'); + // 移除自带的富文本组件规则,使用 editorRule 替代 + designer.value?.removeMenuItem('fc-editor'); + const components = [ + editorRule, + uploadFileRule, + uploadImageRule, + uploadImagesRule, + ]; + components.forEach((component) => { + // 插入组件规则 + designer.value?.addComponent(component); + // 插入拖拽按钮到 `main` 分类下 + designer.value?.appendMenuItem('main', { + icon: component.icon, + name: component.name, + label: component.label, + }); + }); + }; + + const userSelectRule = useSelectRule({ + name: 'UserSelect', + label: '用户选择器', + icon: 'icon-eye', + }); + const deptSelectRule = useSelectRule({ + name: 'DeptSelect', + label: '部门选择器', + icon: 'icon-tree', + }); + const dictSelectRule = useDictSelectRule(); + const apiSelectRule0 = useSelectRule({ + name: 'ApiSelect', + label: '接口选择器', + icon: 'icon-json', + props: [...apiSelectRule], + event: ['click', 'change', 'visibleChange', 'clear', 'blur', 'focus'], + }); + + /** + * 构建系统字段菜单 + */ + const buildSystemMenu = () => { + // 移除自带的下拉选择器组件,使用 currencySelectRule 替代 + // designer.value?.removeMenuItem('select') + // designer.value?.removeMenuItem('radio') + // designer.value?.removeMenuItem('checkbox') + const components = [ + userSelectRule, + deptSelectRule, + dictSelectRule, + apiSelectRule0, + ]; + const menu: Menu = { + name: 'system', + title: '系统字段', + list: components.map((component) => { + // 插入组件规则 + designer.value?.addComponent(component); + // 插入拖拽按钮到 `system` 分类下 + return { + icon: component.icon, + name: component.name, + label: component.label, + }; + }), + }; + designer.value?.addMenu(menu); + }; + + onMounted(async () => { + await nextTick(); + buildFormComponents(); + buildSystemMenu(); + }); +}; diff --git a/apps/web-antd/src/components/form-create/index.ts b/apps/web-antd/src/components/form-create/index.ts new file mode 100644 index 000000000..b311e79e6 --- /dev/null +++ b/apps/web-antd/src/components/form-create/index.ts @@ -0,0 +1,3 @@ +export { useApiSelect } from './components/use-api-select'; + +export { useFormCreateDesigner } from './helpers'; diff --git a/apps/web-antd/src/components/form-create/rules/data.ts b/apps/web-antd/src/components/form-create/rules/data.ts new file mode 100644 index 000000000..2c6cee2ce --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/data.ts @@ -0,0 +1,182 @@ +/* eslint-disable no-template-curly-in-string */ +const selectRule = [ + { + type: 'select', + field: 'selectType', + title: '选择器类型', + value: 'select', + options: [ + { label: '下拉框', value: 'select' }, + { label: '单选框', value: 'radio' }, + { label: '多选框', value: 'checkbox' }, + ], + // 参考 https://www.form-create.com/v3/guide/control 组件联动,单选框和多选框不需要多选属性 + control: [ + { + value: 'select', + condition: '==', + method: 'hidden', + rule: [ + 'multiple', + 'clearable', + 'collapseTags', + 'multipleLimit', + 'allowCreate', + 'filterable', + 'noMatchText', + 'remote', + 'remoteMethod', + 'reserveKeyword', + 'defaultFirstOption', + 'automaticDropdown', + ], + }, + ], + }, + { + type: 'switch', + field: 'filterable', + title: '是否可搜索', + }, + { type: 'switch', field: 'multiple', title: '是否多选' }, + { + type: 'switch', + field: 'disabled', + title: '是否禁用', + }, + { type: 'switch', field: 'clearable', title: '是否可以清空选项' }, + { + type: 'switch', + field: 'collapseTags', + title: '多选时是否将选中值按文字的形式展示', + }, + { + type: 'inputNumber', + field: 'multipleLimit', + title: '多选时用户最多可以选择的项目数,为 0 则不限制', + props: { min: 0 }, + }, + { + type: 'input', + field: 'autocomplete', + title: 'autocomplete 属性', + }, + { type: 'input', field: 'placeholder', title: '占位符' }, + { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' }, + { + type: 'input', + field: 'noMatchText', + title: '搜索条件无匹配时显示的文字', + }, + { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' }, + { + type: 'switch', + field: 'reserveKeyword', + title: '多选且可搜索时,是否在选中一个选项后保留当前的搜索关键词', + }, + { + type: 'switch', + field: 'defaultFirstOption', + title: '在输入框按下回车,选择第一个匹配项', + }, + { + type: 'switch', + field: 'popperAppendToBody', + title: '是否将弹出框插入至 body 元素', + value: true, + }, + { + type: 'switch', + field: 'automaticDropdown', + title: '对于不可搜索的 Select,是否在输入框获得焦点后自动弹出选项菜单', + }, +]; + +const apiSelectRule = [ + { + type: 'input', + field: 'url', + title: 'url 地址', + props: { + placeholder: '/system/user/simple-list', + }, + }, + { + type: 'select', + field: 'method', + title: '请求类型', + value: 'GET', + options: [ + { label: 'GET', value: 'GET' }, + { label: 'POST', value: 'POST' }, + ], + control: [ + { + value: 'GET', + condition: '!=', + method: 'hidden', + rule: [ + { + type: 'input', + field: 'data', + title: '请求参数 JSON 格式', + props: { + autosize: true, + type: 'textarea', + placeholder: '{"type": 1}', + }, + }, + ], + }, + ], + }, + { + type: 'input', + field: 'labelField', + title: 'label 属性', + info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}', + props: { + placeholder: 'nickname', + }, + }, + { + type: 'input', + field: 'valueField', + title: 'value 属性', + info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}', + props: { + placeholder: 'id', + }, + }, + { + type: 'input', + field: 'parseFunc', + title: '选项解析函数', + info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表 + (data: any)=>{ label: string; value: any }[]`, + props: { + autosize: true, + rows: { minRows: 2, maxRows: 6 }, + type: 'textarea', + placeholder: ` + function (data) { + console.log(data) + return data.list.map(item=> ({label: item.nickname,value: item.id})) + }`, + }, + }, + { + type: 'switch', + field: 'remote', + info: '是否可搜索', + title: '其中的选项是否从服务器远程加载', + }, + { + type: 'input', + field: 'remoteField', + title: '请求参数', + info: '远程请求时请求携带的参数名称,如:name', + }, +]; + +export { apiSelectRule, selectRule }; diff --git a/apps/web-antd/src/components/form-create/rules/index.ts b/apps/web-antd/src/components/form-create/rules/index.ts new file mode 100644 index 000000000..db306da35 --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/index.ts @@ -0,0 +1,6 @@ +export { useDictSelectRule } from './use-dict-select'; +export { useEditorRule } from './use-editor-rule'; +export { useSelectRule } from './use-select-rule'; +export { useUploadFileRule } from './use-upload-file-rule'; +export { useUploadImageRule } from './use-upload-image-rule'; +export { useUploadImagesRule } from './use-upload-images-rule'; diff --git a/apps/web-antd/src/components/form-create/rules/use-dict-select.ts b/apps/web-antd/src/components/form-create/rules/use-dict-select.ts new file mode 100644 index 000000000..c9c438e8b --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/use-dict-select.ts @@ -0,0 +1,69 @@ +import { onMounted, ref } from 'vue'; + +import { buildUUID, cloneDeep } from '@vben/utils'; + +import * as DictDataApi from '#/api/system/dict/type'; +import { + localeProps, + makeRequiredRule, +} from '#/components/form-create/helpers'; +import { selectRule } from '#/components/form-create/rules/data'; + +/** + * 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule + */ +export const useDictSelectRule = () => { + const label = '字典选择器'; + const name = 'DictSelect'; + const rules = cloneDeep(selectRule); + const dictOptions = ref<{ label: string; value: string }[]>([]); // 字典类型下拉数据 + onMounted(async () => { + const data = await DictDataApi.getSimpleDictTypeList(); + if (!data || data.length === 0) { + return; + } + dictOptions.value = + data?.map((item: DictDataApi.SystemDictTypeApi.DictType) => ({ + label: item.name, + value: item.type, + })) ?? []; + }); + return { + icon: 'icon-descriptions', + label, + name, + rule() { + return { + type: name, + field: buildUUID(), + title: label, + info: '', + $required: false, + }; + }, + props(_: any, { t }: any) { + return localeProps(t, `${name}.props`, [ + makeRequiredRule(), + { + type: 'select', + field: 'dictType', + title: '字典类型', + value: '', + options: dictOptions.value, + }, + { + type: 'select', + field: 'valueType', + title: '字典值类型', + value: 'str', + options: [ + { label: '数字', value: 'int' }, + { label: '字符串', value: 'str' }, + { label: '布尔值', value: 'bool' }, + ], + }, + ...rules, + ]); + }, + }; +}; diff --git a/apps/web-antd/src/components/form-create/rules/use-editor-rule.ts b/apps/web-antd/src/components/form-create/rules/use-editor-rule.ts new file mode 100644 index 000000000..556baf0a1 --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/use-editor-rule.ts @@ -0,0 +1,36 @@ +import { buildUUID } from '@vben/utils'; + +import { + localeProps, + makeRequiredRule, +} from '#/components/form-create/helpers'; + +export const useEditorRule = () => { + const label = '富文本'; + const name = 'Tinymce'; + return { + icon: 'icon-editor', + label, + name, + rule() { + return { + type: name, + field: buildUUID(), + title: label, + info: '', + $required: false, + }; + }, + props(_: any, { t }: any) { + return localeProps(t, `${name}.props`, [ + makeRequiredRule(), + { + type: 'input', + field: 'height', + title: '高度', + }, + { type: 'switch', field: 'readonly', title: '是否只读' }, + ]); + }, + }; +}; diff --git a/apps/web-antd/src/components/form-create/rules/use-select-rule.ts b/apps/web-antd/src/components/form-create/rules/use-select-rule.ts new file mode 100644 index 000000000..fd4213783 --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/use-select-rule.ts @@ -0,0 +1,45 @@ +import type { SelectRuleOption } from '#/components/form-create/typing'; + +import { buildUUID, cloneDeep } from '@vben/utils'; + +import { + localeProps, + makeRequiredRule, +} from '#/components/form-create/helpers'; +import { selectRule } from '#/components/form-create/rules/data'; + +/** + * 通用选择器规则 hook + * + * @param option 规则配置 + */ +export const useSelectRule = (option: SelectRuleOption) => { + const label = option.label; + const name = option.name; + const rules = cloneDeep(selectRule); + return { + icon: option.icon, + label, + name, + event: option.event, + rule() { + return { + type: name, + field: buildUUID(), + title: label, + info: '', + $required: false, + }; + }, + props(_: any, { t }: any) { + if (!option.props) { + option.props = []; + } + return localeProps(t, `${name}.props`, [ + makeRequiredRule(), + ...option.props, + ...rules, + ]); + }, + }; +}; diff --git a/apps/web-antd/src/components/form-create/rules/use-upload-file-rule.ts b/apps/web-antd/src/components/form-create/rules/use-upload-file-rule.ts new file mode 100644 index 000000000..55f5bea33 --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/use-upload-file-rule.ts @@ -0,0 +1,84 @@ +import { buildUUID } from '@vben/utils'; + +import { + localeProps, + makeRequiredRule, +} from '#/components/form-create/helpers'; + +export const useUploadFileRule = () => { + const label = '文件上传'; + const name = 'FileUpload'; + return { + icon: 'icon-upload', + label, + name, + rule() { + return { + type: name, + field: buildUUID(), + title: label, + info: '', + $required: false, + }; + }, + props(_: any, { t }: any) { + return localeProps(t, `${name}.props`, [ + makeRequiredRule(), + { + type: 'select', + field: 'fileType', + title: '文件类型', + value: ['doc', 'xls', 'ppt', 'txt', 'pdf'], + options: [ + { label: 'doc', value: 'doc' }, + { label: 'xls', value: 'xls' }, + { label: 'ppt', value: 'ppt' }, + { label: 'txt', value: 'txt' }, + { label: 'pdf', value: 'pdf' }, + ], + props: { + multiple: true, + }, + }, + { + type: 'switch', + field: 'autoUpload', + title: '是否在选取文件后立即进行上传', + value: true, + }, + { + type: 'switch', + field: 'drag', + title: '拖拽上传', + value: false, + }, + { + type: 'switch', + field: 'isShowTip', + title: '是否显示提示', + value: true, + }, + { + type: 'inputNumber', + field: 'fileSize', + title: '大小限制(MB)', + value: 5, + props: { min: 0 }, + }, + { + type: 'inputNumber', + field: 'limit', + title: '数量限制', + value: 5, + props: { min: 0 }, + }, + { + type: 'switch', + field: 'disabled', + title: '是否禁用', + value: false, + }, + ]); + }, + }; +}; diff --git a/apps/web-antd/src/components/form-create/rules/use-upload-image-rule.ts b/apps/web-antd/src/components/form-create/rules/use-upload-image-rule.ts new file mode 100644 index 000000000..70760b061 --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/use-upload-image-rule.ts @@ -0,0 +1,93 @@ +import { buildUUID } from '@vben/utils'; + +import { + localeProps, + makeRequiredRule, +} from '#/components/form-create/helpers'; + +export const useUploadImageRule = () => { + const label = '单图上传'; + const name = 'ImageUpload'; + return { + icon: 'icon-image', + label, + name, + rule() { + return { + type: name, + field: buildUUID(), + title: label, + info: '', + $required: false, + }; + }, + props(_: any, { t }: any) { + return localeProps(t, `${name}.props`, [ + makeRequiredRule(), + { + type: 'switch', + field: 'drag', + title: '拖拽上传', + value: false, + }, + { + type: 'select', + field: 'fileType', + title: '图片类型限制', + value: ['image/jpeg', 'image/png', 'image/gif'], + options: [ + { label: 'image/apng', value: 'image/apng' }, + { label: 'image/bmp', value: 'image/bmp' }, + { label: 'image/gif', value: 'image/gif' }, + { label: 'image/jpeg', value: 'image/jpeg' }, + { label: 'image/pjpeg', value: 'image/pjpeg' }, + { label: 'image/svg+xml', value: 'image/svg+xml' }, + { label: 'image/tiff', value: 'image/tiff' }, + { label: 'image/webp', value: 'image/webp' }, + { label: 'image/x-icon', value: 'image/x-icon' }, + ], + props: { + multiple: false, + }, + }, + { + type: 'inputNumber', + field: 'fileSize', + title: '大小限制(MB)', + value: 5, + props: { min: 0 }, + }, + { + type: 'input', + field: 'height', + title: '组件高度', + value: '150px', + }, + { + type: 'input', + field: 'width', + title: '组件宽度', + value: '150px', + }, + { + type: 'input', + field: 'borderradius', + title: '组件边框圆角', + value: '8px', + }, + { + type: 'switch', + field: 'disabled', + title: '是否显示删除按钮', + value: true, + }, + { + type: 'switch', + field: 'showBtnText', + title: '是否显示按钮文字', + value: true, + }, + ]); + }, + }; +}; diff --git a/apps/web-antd/src/components/form-create/rules/use-upload-images-rule.ts b/apps/web-antd/src/components/form-create/rules/use-upload-images-rule.ts new file mode 100644 index 000000000..c18a7b49d --- /dev/null +++ b/apps/web-antd/src/components/form-create/rules/use-upload-images-rule.ts @@ -0,0 +1,89 @@ +import { buildUUID } from '@vben/utils'; + +import { + localeProps, + makeRequiredRule, +} from '#/components/form-create/helpers'; + +export const useUploadImagesRule = () => { + const label = '多图上传'; + const name = 'ImagesUpload'; + return { + icon: 'icon-image', + label, + name, + rule() { + return { + type: name, + field: buildUUID(), + title: label, + info: '', + $required: false, + }; + }, + props(_: any, { t }: any) { + return localeProps(t, `${name}.props`, [ + makeRequiredRule(), + { + type: 'switch', + field: 'drag', + title: '拖拽上传', + value: false, + }, + { + type: 'select', + field: 'fileType', + title: '图片类型限制', + value: ['image/jpeg', 'image/png', 'image/gif'], + options: [ + { label: 'image/apng', value: 'image/apng' }, + { label: 'image/bmp', value: 'image/bmp' }, + { label: 'image/gif', value: 'image/gif' }, + { label: 'image/jpeg', value: 'image/jpeg' }, + { label: 'image/pjpeg', value: 'image/pjpeg' }, + { label: 'image/svg+xml', value: 'image/svg+xml' }, + { label: 'image/tiff', value: 'image/tiff' }, + { label: 'image/webp', value: 'image/webp' }, + { label: 'image/x-icon', value: 'image/x-icon' }, + ], + props: { + multiple: true, + maxNumber: 5, + }, + }, + { + type: 'inputNumber', + field: 'fileSize', + title: '大小限制(MB)', + value: 5, + props: { min: 0 }, + }, + { + type: 'inputNumber', + field: 'limit', + title: '数量限制', + value: 5, + props: { min: 0 }, + }, + { + type: 'input', + field: 'height', + title: '组件高度', + value: '150px', + }, + { + type: 'input', + field: 'width', + title: '组件宽度', + value: '150px', + }, + { + type: 'input', + field: 'borderradius', + title: '组件边框圆角', + value: '8px', + }, + ]); + }, + }; +}; diff --git a/apps/web-antd/src/components/form-create/typing.ts b/apps/web-antd/src/components/form-create/typing.ts new file mode 100644 index 000000000..b89e6f780 --- /dev/null +++ b/apps/web-antd/src/components/form-create/typing.ts @@ -0,0 +1,60 @@ +import type { Rule } from '@form-create/ant-design-vue'; + +/** 数据字典 Select 选择器组件 Props 类型 */ +export interface DictSelectProps { + dictType: string; // 字典类型 + valueType?: 'bool' | 'int' | 'str'; // 字典值类型 TODO @芋艿:'boolean' | 'number' | 'string';需要和 vue3 一起统一! + selectType?: 'checkbox' | 'radio' | 'select'; // 选择器类型,下拉框 select、多选框 checkbox、单选框 radio + formCreateInject?: any; +} + +/** 左侧拖拽按钮 */ +export interface MenuItem { + label: string; + name: string; + icon: string; +} + +/** 左侧拖拽按钮分类 */ +export interface Menu { + title: string; + name: string; + list: MenuItem[]; +} + +export type MenuList = Array; + +// TODO @dhb52:MenuList、Menu、MenuItem、DragRule 这几个,是不是没用到呀? +// 拖拽组件的规则 +export interface DragRule { + icon: string; + name: string; + label: string; + children?: string; + inside?: true; + drag?: string | true; + dragBtn?: false; + mask?: false; + + rule(): Rule; + + props(v: any, v1: any): Rule[]; +} + +/** 通用 API 下拉组件 Props 类型 */ +export interface ApiSelectProps { + name: string; // 组件名称 + labelField?: string; // 选项标签 + valueField?: string; // 选项的值 + url?: string; // url 接口 + isDict?: boolean; // 是否字典选择器 +} + +/** 选择组件规则配置类型 */ +export interface SelectRuleOption { + label: string; // label 名称 + name: string; // 组件名称 + icon: string; // 组件图标 + props?: any[]; // 组件规则 + event?: any[]; // 事件配置 +} diff --git a/apps/web-antd/src/components/tinymce/editor.vue b/apps/web-antd/src/components/tinymce/editor.vue index 3b0f25ee8..cb874f6c9 100644 --- a/apps/web-antd/src/components/tinymce/editor.vue +++ b/apps/web-antd/src/components/tinymce/editor.vue @@ -33,7 +33,7 @@ import { type InitOptions = IPropTypes['init']; -defineOptions({ inheritAttrs: false }); +defineOptions({ name: 'Tinymce', inheritAttrs: false }); const props = defineProps({ options: { @@ -157,7 +157,6 @@ const initOptions = computed((): InitOptions => { const { httpRequest } = useUpload(); httpRequest(file) .then((url) => { - console.log('tinymce 上传图片成功:', url); resolve(url); }) .catch((error) => { diff --git a/apps/web-antd/src/components/upload/image-upload.vue b/apps/web-antd/src/components/upload/image-upload.vue index 6d1d576fc..412d93560 100644 --- a/apps/web-antd/src/components/upload/image-upload.vue +++ b/apps/web-antd/src/components/upload/image-upload.vue @@ -4,6 +4,8 @@ import type { UploadRequestOption } from 'ant-design-vue/lib/vc-upload/interface import type { AxiosResponse } from '@vben/request'; +import type { UploadListType } from './typing'; + import type { AxiosProgressEvent } from '#/api/infra/file'; import { ref, toRefs, watch } from 'vue'; @@ -30,7 +32,7 @@ const props = withDefaults( ) => Promise>; disabled?: boolean; helpText?: string; - listType?: ListType; + listType?: UploadListType; // 最大数量的文件,Infinity不限制 maxNumber?: number; // 文件最大多少MB @@ -58,7 +60,6 @@ const props = withDefaults( }, ); const emit = defineEmits(['change', 'update:value', 'delete']); -type ListType = 'picture' | 'picture-card' | 'text'; const { accept, helpText, maxNumber, maxSize } = toRefs(props); const isInnerOperate = ref(false); const { getStringAccept } = useUploadType({ diff --git a/apps/web-antd/src/components/upload/typing.ts b/apps/web-antd/src/components/upload/typing.ts index c48d5fd69..a6b54d479 100644 --- a/apps/web-antd/src/components/upload/typing.ts +++ b/apps/web-antd/src/components/upload/typing.ts @@ -4,3 +4,5 @@ export enum UploadResultStatus { SUCCESS = 'success', UPLOADING = 'uploading', } + +export type UploadListType = 'picture' | 'picture-card' | 'text'; diff --git a/apps/web-antd/src/layouts/basic.vue b/apps/web-antd/src/layouts/basic.vue index 733c4a6a3..86f444661 100644 --- a/apps/web-antd/src/layouts/basic.vue +++ b/apps/web-antd/src/layouts/basic.vue @@ -3,7 +3,7 @@ import type { NotificationItem } from '@vben/layouts'; import { computed, onMounted, ref, watch } from 'vue'; -import { AuthenticationLoginExpiredModal } from '@vben/common-ui'; +import { AuthenticationLoginExpiredModal, useVbenModal } from '@vben/common-ui'; import { VBEN_DOC_URL, VBEN_GITHUB_URL } from '@vben/constants'; import { useWatermark } from '@vben/hooks'; import { @@ -33,6 +33,8 @@ import { router } from '#/router'; import { useAuthStore } from '#/store'; import LoginForm from '#/views/_core/authentication/login.vue'; +import Help from './components/help.vue'; + const userStore = useUserStore(); const authStore = useAuthStore(); const accessStore = useAccessStore(); @@ -42,6 +44,10 @@ const notifications = ref([]); const unreadCount = ref(0); const showDot = computed(() => unreadCount.value > 0); +const [HelpModal, helpModalApi] = useVbenModal({ + connectedComponent: Help, +}); + const menus = computed(() => [ { handler: () => { @@ -70,9 +76,7 @@ const menus = computed(() => [ }, { handler: () => { - openWindow(`${VBEN_GITHUB_URL}/issues`, { - target: '_blank', - }); + helpModalApi.open(); }, icon: CircleHelp, text: $t('ui.widgets.qa'), @@ -210,4 +214,5 @@ watch( + diff --git a/apps/web-antd/src/layouts/components/help.vue b/apps/web-antd/src/layouts/components/help.vue new file mode 100644 index 000000000..4d19a8e92 --- /dev/null +++ b/apps/web-antd/src/layouts/components/help.vue @@ -0,0 +1,93 @@ + + diff --git a/apps/web-antd/src/plugins/form-create/index.ts b/apps/web-antd/src/plugins/form-create/index.ts new file mode 100644 index 000000000..78ded848c --- /dev/null +++ b/apps/web-antd/src/plugins/form-create/index.ts @@ -0,0 +1,51 @@ +import type { App } from 'vue'; + +// import install from '@form-create/ant-design-vue/auto-import'; +import FcDesigner from '@form-create/antd-designer'; +import Antd from 'ant-design-vue'; + +// ======================= 自定义组件 ======================= +import { useApiSelect } from '#/components/form-create'; +import DictSelect from '#/components/form-create/components/dict-select.vue'; +import { useImagesUpload } from '#/components/form-create/components/use-images-upload'; +import { Tinymce } from '#/components/tinymce'; +import { FileUpload, ImageUpload } from '#/components/upload'; + +const UserSelect = useApiSelect({ + name: 'UserSelect', + labelField: 'nickname', + valueField: 'id', + url: '/system/user/simple-list', +}); +const DeptSelect = useApiSelect({ + name: 'DeptSelect', + labelField: 'name', + valueField: 'id', + url: '/system/dept/simple-list', +}); +const ApiSelect = useApiSelect({ + name: 'ApiSelect', +}); +const ImagesUpload = useImagesUpload(); + +const components = [ + ImageUpload, + ImagesUpload, + FileUpload, + Tinymce, + DictSelect, + UserSelect, + DeptSelect, + ApiSelect, +]; + +// TODO: @dhb52 按需导入,而不是app.use(Antd); +// 参考 http://www.form-create.com/v3/ant-design-vue/auto-import.html 文档 +export const setupFormCreate = (app: App) => { + components.forEach((component) => { + app.component(component.name as string, component); + }); + app.use(Antd); + app.use(FcDesigner); + app.use(FcDesigner.formCreate); +}; diff --git a/apps/web-antd/src/router/index.ts b/apps/web-antd/src/router/index.ts index 3d0d43654..5acec55ea 100644 --- a/apps/web-antd/src/router/index.ts +++ b/apps/web-antd/src/router/index.ts @@ -7,8 +7,8 @@ import { import { resetStaticRoutes } from '@vben/utils'; import { createRouterGuard } from './guard'; -import { setupBaiduTongJi } from './tongji'; import { routes } from './routes'; +import { setupBaiduTongJi } from './tongji'; /** * @zh_CN 创建vue-router实例 diff --git a/apps/web-antd/src/router/routes/core.ts b/apps/web-antd/src/router/routes/core.ts index 5058a2d5b..1f6f28f8e 100644 --- a/apps/web-antd/src/router/routes/core.ts +++ b/apps/web-antd/src/router/routes/core.ts @@ -92,7 +92,8 @@ const coreRoutes: RouteRecordRaw[] = [ { name: 'SocialLogin', path: 'social-login', - component: () => import('#/views/_core/authentication/social-login.vue'), + component: () => + import('#/views/_core/authentication/social-login.vue'), meta: { title: $t('page.auth.login'), }, @@ -104,7 +105,7 @@ const coreRoutes: RouteRecordRaw[] = [ meta: { title: $t('page.auth.login'), }, - } + }, ], }, ]; diff --git a/apps/web-antd/src/router/routes/index.ts b/apps/web-antd/src/router/routes/index.ts index 4b5c0ecd3..738f9d3d9 100644 --- a/apps/web-antd/src/router/routes/index.ts +++ b/apps/web-antd/src/router/routes/index.ts @@ -44,4 +44,4 @@ const componentKeys: string[] = Object.keys( const path = v.replace('../../views/', '/'); return path.endsWith('.vue') ? path.slice(0, -4) : path; }); -export { accessRoutes, coreRouteNames, routes, componentKeys }; +export { accessRoutes, componentKeys, coreRouteNames, routes }; diff --git a/apps/web-antd/src/router/routes/modules/system.ts b/apps/web-antd/src/router/routes/modules/system.ts index c75059b29..47e6b1682 100644 --- a/apps/web-antd/src/router/routes/modules/system.ts +++ b/apps/web-antd/src/router/routes/modules/system.ts @@ -13,4 +13,4 @@ const routes: RouteRecordRaw[] = [ }, ]; -export default routes; +export default routes; diff --git a/apps/web-antd/src/utils/dict.ts b/apps/web-antd/src/utils/dict.ts index adef41445..ded8ea473 100644 --- a/apps/web-antd/src/utils/dict.ts +++ b/apps/web-antd/src/utils/dict.ts @@ -6,7 +6,28 @@ import { isObject } from '@vben/utils'; import { useDictStore } from '#/store'; -const dictStore = useDictStore(); +// TODO @dhb52:top-level 调用 导致:"getActivePinia()" was called but there was no active Pinia +// 先临时移入到方法中 +// const dictStore = useDictStore(); + +// TODO @dhb: antd 组件的 color 类型 +type ColorType = 'error' | 'info' | 'success' | 'warning'; + +export interface DictDataType { + dictType: string; + label: string; + value: boolean | number | string; + colorType: ColorType; + cssClass: string; +} + +export interface NumberDictDataType extends DictDataType { + value: number; +} + +export interface StringDictDataType extends DictDataType { + value: string; +} /** * 获取字典标签 @@ -16,6 +37,7 @@ const dictStore = useDictStore(); * @returns 字典标签 */ function getDictLabel(dictType: string, value: any) { + const dictStore = useDictStore(); const dictObj = dictStore.getDictData(dictType, value); return isObject(dictObj) ? dictObj.label : ''; } @@ -28,6 +50,7 @@ function getDictLabel(dictType: string, value: any) { * @returns 字典对象 */ function getDictObj(dictType: string, value: any) { + const dictStore = useDictStore(); const dictObj = dictStore.getDictData(dictType, value); return isObject(dictObj) ? dictObj : null; } @@ -36,12 +59,15 @@ function getDictObj(dictType: string, value: any) { * 获取字典数组 用于select radio 等 * * @param dictType 字典类型 + * @param valueType 字典值类型,默认 string 类型 * @returns 字典数组 */ +// TODO @puhui999:貌似可以定义一个类型?不使用 any[] function getDictOptions( dictType: string, valueType: 'boolean' | 'number' | 'string' = 'string', -) { +): any[] { + const dictStore = useDictStore(); const dictOpts = dictStore.getDictOptions(dictType); const dictOptions: DefaultOptionType = []; if (dictOpts.length > 0) { @@ -71,6 +97,51 @@ function getDictOptions( return dictOptions.length > 0 ? dictOptions : []; } +// TODO @dhb52:下面的一系列方法,看看能不能复用 getDictOptions 方法 +export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { + // 获得通用的 DictDataType 列表 + const dictOptions = getDictOptions(dictType) as DictDataType[]; + // 转换成 number 类型的 NumberDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getIntDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: NumberDictDataType[] = []; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: Number.parseInt(`${dict.value}`), + }); + }); + return dictOption; +}; + +// TODO @dhb52:下面的一系列方法,看看能不能复用 getDictOptions 方法 +export const getStrDictOptions = (dictType: string) => { + // 获得通用的 DictDataType 列表 + const dictOptions = getDictOptions(dictType) as DictDataType[]; + // 转换成 string 类型的 StringDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: StringDictDataType[] = []; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: `${dict.value}`, + }); + }); + return dictOption; +}; + +// TODO @dhb52:下面的一系列方法,看看能不能复用 getDictOptions 方法 +export const getBoolDictOptions = (dictType: string) => { + const dictOption: DictDataType[] = []; + const dictOptions = getDictOptions(dictType) as DictDataType[]; + dictOptions.forEach((dict: DictDataType) => { + dictOption.push({ + ...dict, + value: `${dict.value}` === 'true', + }); + }); + return dictOption; +}; + enum DICT_TYPE { AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式 AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态 diff --git a/apps/web-antd/src/views/_core/authentication/code-login.vue b/apps/web-antd/src/views/_core/authentication/code-login.vue index 3df82e0e7..1862abf8c 100644 --- a/apps/web-antd/src/views/_core/authentication/code-login.vue +++ b/apps/web-antd/src/views/_core/authentication/code-login.vue @@ -30,7 +30,7 @@ const loginRef = ref(); /** 获取租户列表,并默认选中 */ const tenantList = ref([]); // 租户列表 -const fetchTenantList = async () => { +async function fetchTenantList() { if (!tenantEnable) { return; } @@ -56,11 +56,11 @@ const fetchTenantList = async () => { // 设置选中的租户编号 accessStore.setTenantId(tenantId); - loginRef.value.getFormApi().setFieldValue('tenantId', tenantId); + loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString()); } catch (error) { console.error('获取租户列表失败:', error); } -}; +} /** 组件挂载时获取租户信息 */ onMounted(() => { @@ -74,19 +74,19 @@ const formSchema = computed((): VbenFormSchema[] => { componentProps: { options: tenantList.value.map((item) => ({ label: item.name, - value: item.id, + value: item.id.toString(), })), placeholder: $t('authentication.tenantTip'), }, fieldName: 'tenantId', label: $t('authentication.tenant'), - rules: z.number().positive(), + rules: z.string().min(1, { message: $t('authentication.tenantTip') }), dependencies: { triggerFields: ['tenantId'], if: tenantEnable, trigger(values) { if (values.tenantId) { - accessStore.setTenantId(values.tenantId); + accessStore.setTenantId(Number(values.tenantId)); } }, }, diff --git a/apps/web-antd/src/views/_core/authentication/forget-password.vue b/apps/web-antd/src/views/_core/authentication/forget-password.vue index 52f65b15b..2ef4b0ff1 100644 --- a/apps/web-antd/src/views/_core/authentication/forget-password.vue +++ b/apps/web-antd/src/views/_core/authentication/forget-password.vue @@ -29,7 +29,7 @@ const forgetPasswordRef = ref(); /** 获取租户列表,并默认选中 */ const tenantList = ref([]); // 租户列表 -const fetchTenantList = async () => { +async function fetchTenantList() { if (!tenantEnable) { return; } @@ -55,11 +55,13 @@ const fetchTenantList = async () => { // 设置选中的租户编号 accessStore.setTenantId(tenantId); - forgetPasswordRef.value.getFormApi().setFieldValue('tenantId', tenantId); + forgetPasswordRef.value + .getFormApi() + .setFieldValue('tenantId', tenantId?.toString()); } catch (error) { console.error('获取租户列表失败:', error); } -}; +} /** 组件挂载时获取租户信息 */ onMounted(() => { @@ -73,19 +75,19 @@ const formSchema = computed((): VbenFormSchema[] => { componentProps: { options: tenantList.value.map((item) => ({ label: item.name, - value: item.id, + value: item.id.toString(), })), placeholder: $t('authentication.tenantTip'), }, fieldName: 'tenantId', label: $t('authentication.tenant'), - rules: z.number().positive(), + rules: z.string().min(1, { message: $t('authentication.tenantTip') }), dependencies: { triggerFields: ['tenantId'], if: tenantEnable, trigger(values) { if (values.tenantId) { - accessStore.setTenantId(values.tenantId); + accessStore.setTenantId(Number(values.tenantId)); } }, }, diff --git a/apps/web-antd/src/views/_core/authentication/login.vue b/apps/web-antd/src/views/_core/authentication/login.vue index 926926f90..aa1db4c53 100644 --- a/apps/web-antd/src/views/_core/authentication/login.vue +++ b/apps/web-antd/src/views/_core/authentication/login.vue @@ -35,7 +35,7 @@ const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWo /** 获取租户列表,并默认选中 */ const tenantList = ref([]); // 租户列表 -const fetchTenantList = async () => { +async function fetchTenantList() { if (!tenantEnable) { return; } @@ -61,26 +61,25 @@ const fetchTenantList = async () => { // 设置选中的租户编号 accessStore.setTenantId(tenantId); - loginRef.value.getFormApi().setFieldValue('tenantId', tenantId); + loginRef.value.getFormApi().setFieldValue('tenantId', tenantId?.toString()); } catch (error) { console.error('获取租户列表失败:', error); } -}; +} /** 处理登录 */ -const handleLogin = async (values: any) => { +async function handleLogin(values: any) { // 如果开启验证码,则先验证验证码 if (captchaEnable) { verifyRef.value.show(); return; } - // 无验证码,直接登录 await authStore.authLogin('username', values); -}; +} /** 验证码通过,执行登录 */ -const handleVerifySuccess = async ({ captchaVerification }: any) => { +async function handleVerifySuccess({ captchaVerification }: any) { try { await authStore.authLogin('username', { ...(await loginRef.value.getFormApi().getValues()), @@ -89,11 +88,11 @@ const handleVerifySuccess = async ({ captchaVerification }: any) => { } catch (error) { console.error('Error in handleLogin:', error); } -}; +} /** 处理第三方登录 */ const redirect = query?.redirect; -const handleThirdLogin = async (type: number) => { +async function handleThirdLogin(type: number) { if (type <= 0) { return; } @@ -111,7 +110,7 @@ const handleThirdLogin = async (type: number) => { } catch (error) { console.error('第三方登录处理失败:', error); } -}; +} /** 组件挂载时获取租户信息 */ onMounted(() => { @@ -125,19 +124,19 @@ const formSchema = computed((): VbenFormSchema[] => { componentProps: { options: tenantList.value.map((item) => ({ label: item.name, - value: item.id, + value: item.id.toString(), })), placeholder: $t('authentication.tenantTip'), }, fieldName: 'tenantId', label: $t('authentication.tenant'), - rules: z.number().positive(), + rules: z.string().min(1, { message: $t('authentication.tenantTip') }), dependencies: { triggerFields: ['tenantId'], if: tenantEnable, trigger(values) { if (values.tenantId) { - accessStore.setTenantId(values.tenantId); + accessStore.setTenantId(Number(values.tenantId)); } }, }, diff --git a/apps/web-antd/src/views/_core/authentication/register.vue b/apps/web-antd/src/views/_core/authentication/register.vue index 567bb04c8..734f4c27d 100644 --- a/apps/web-antd/src/views/_core/authentication/register.vue +++ b/apps/web-antd/src/views/_core/authentication/register.vue @@ -34,7 +34,7 @@ const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWo /** 获取租户列表,并默认选中 */ const tenantList = ref([]); // 租户列表 -const fetchTenantList = async () => { +async function fetchTenantList() { if (!tenantEnable) { return; } @@ -60,14 +60,16 @@ const fetchTenantList = async () => { // 设置选中的租户编号 accessStore.setTenantId(tenantId); - registerRef.value.getFormApi().setFieldValue('tenantId', tenantId); + registerRef.value + .getFormApi() + .setFieldValue('tenantId', tenantId?.toString()); } catch (error) { console.error('获取租户列表失败:', error); } -}; +} /** 执行注册 */ -const handleRegister = async (values: any) => { +async function handleRegister(values: any) { // 如果开启验证码,则先验证验证码 if (captchaEnable) { verifyRef.value.show(); @@ -76,7 +78,7 @@ const handleRegister = async (values: any) => { // 无验证码,直接登录 await authStore.authLogin('register', values); -}; +} /** 验证码通过,执行注册 */ const handleVerifySuccess = async ({ captchaVerification }: any) => { @@ -108,13 +110,13 @@ const formSchema = computed((): VbenFormSchema[] => { }, fieldName: 'tenantId', label: $t('authentication.tenant'), - rules: z.number().positive(), + rules: z.string().min(1, { message: $t('authentication.tenantTip') }), dependencies: { triggerFields: ['tenantId'], if: tenantEnable, trigger(values) { if (values.tenantId) { - accessStore.setTenantId(values.tenantId); + accessStore.setTenantId(Number(values.tenantId)); } }, }, diff --git a/apps/web-antd/src/views/_core/authentication/social-login.vue b/apps/web-antd/src/views/_core/authentication/social-login.vue index c4826a535..e75a9c0d4 100644 --- a/apps/web-antd/src/views/_core/authentication/social-login.vue +++ b/apps/web-antd/src/views/_core/authentication/social-login.vue @@ -35,7 +35,7 @@ const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWo /** 获取租户列表,并默认选中 */ const tenantList = ref([]); // 租户列表 -const fetchTenantList = async () => { +async function fetchTenantList() { if (!tenantEnable) { return; } @@ -66,14 +66,14 @@ const fetchTenantList = async () => { } catch (error) { console.error('获取租户列表失败:', error); } -}; +} /** 尝试登录:当账号已经绑定,socialLogin 会直接获得 token */ const socialType = Number(getUrlValue('type')); const redirect = getUrlValue('redirect'); const socialCode = query?.code as string; const socialState = query?.state as string; -const tryLogin = async () => { +async function tryLogin() { // 用于登录后,基于 redirect 的重定向 if (redirect) { await router.replace({ @@ -90,10 +90,10 @@ const tryLogin = async () => { code: socialCode, state: socialState, }); -}; +} /** 处理登录 */ -const handleLogin = async (values: any) => { +async function handleLogin(values: any) { // 如果开启验证码,则先验证验证码 if (captchaEnable) { verifyRef.value.show(); @@ -107,10 +107,10 @@ const handleLogin = async (values: any) => { socialCode, socialState, }); -}; +} /** 验证码通过,执行登录 */ -const handleVerifySuccess = async ({ captchaVerification }: any) => { +async function handleVerifySuccess({ captchaVerification }: any) { try { await authStore.authLogin('username', { ...(await loginRef.value.getFormApi().getValues()), @@ -122,7 +122,7 @@ const handleVerifySuccess = async ({ captchaVerification }: any) => { } catch (error) { console.error('Error in handleLogin:', error); } -}; +} /** tricky: 配合 login.vue 中,redirectUri 需要对参数进行 encode,需要在回调后进行decode */ function getUrlValue(key: string): string { @@ -144,19 +144,19 @@ const formSchema = computed((): VbenFormSchema[] => { componentProps: { options: tenantList.value.map((item) => ({ label: item.name, - value: item.id, + value: item.id.toString(), })), placeholder: $t('authentication.tenantTip'), }, fieldName: 'tenantId', label: $t('authentication.tenant'), - rules: z.number().positive(), + rules: z.string().min(1, { message: $t('authentication.tenantTip') }), dependencies: { triggerFields: ['tenantId'], if: tenantEnable, trigger(values) { if (values.tenantId) { - accessStore.setTenantId(values.tenantId); + accessStore.setTenantId(Number(values.tenantId)); } }, }, diff --git a/apps/web-antd/src/views/_core/authentication/sso-login.vue b/apps/web-antd/src/views/_core/authentication/sso-login.vue index ae5ac89c5..1bc7ad9b1 100644 --- a/apps/web-antd/src/views/_core/authentication/sso-login.vue +++ b/apps/web-antd/src/views/_core/authentication/sso-login.vue @@ -29,7 +29,7 @@ const queryParams = reactive({ const loading = ref(false); // 表单是否提交中 /** 初始化授权信息 */ -const init = async () => { +async function init() { // 防止在没有登录的情况下循环弹窗 if (query.client_id === undefined) { return; @@ -75,10 +75,10 @@ const init = async () => { 'scopes', scopes.filter((scope) => scope.value).map((scope) => scope.key), ); -}; +} /** 处理授权的提交 */ -const handleSubmit = async (approved: boolean) => { +async function handleSubmit(approved: boolean) { // 计算 checkedScopes + uncheckedScopes let checkedScopes: string[]; let uncheckedScopes: string[]; @@ -107,7 +107,7 @@ const handleSubmit = async (approved: boolean) => { } finally { loading.value = false; } -}; +} /** 调用授权 API 接口 */ const doAuthorize = ( @@ -127,7 +127,7 @@ const doAuthorize = ( }; /** 格式化 scope 文本 */ -const formatScope = (scope: string) => { +function formatScope(scope: string) { // 格式化 scope 授权范围,方便用户理解。 // 这里仅仅是一个 demo,可以考虑录入到字典数据中,例如说字典类型 "system_oauth2_scope",它的每个 scope 都是一条字典数据。 switch (scope) { @@ -141,7 +141,7 @@ const formatScope = (scope: string) => { return scope; } } -}; +} const formSchema = computed((): VbenFormSchema[] => { return [ diff --git a/apps/web-antd/src/views/_core/profile/index.vue b/apps/web-antd/src/views/_core/profile/index.vue index 4566b2c8a..8c8035733 100644 --- a/apps/web-antd/src/views/_core/profile/index.vue +++ b/apps/web-antd/src/views/_core/profile/index.vue @@ -1,17 +1,20 @@ diff --git a/apps/web-antd/src/views/bpm/group/index.vue b/apps/web-antd/src/views/bpm/group/index.vue index d30707d92..98675eed7 100644 --- a/apps/web-antd/src/views/bpm/group/index.vue +++ b/apps/web-antd/src/views/bpm/group/index.vue @@ -1,18 +1,31 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/oa/leave/index.vue b/apps/web-antd/src/views/bpm/oa/leave/index.vue index 249ffd90c..c35b0c4a3 100644 --- a/apps/web-antd/src/views/bpm/oa/leave/index.vue +++ b/apps/web-antd/src/views/bpm/oa/leave/index.vue @@ -1,18 +1,34 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/processExpression/index.vue b/apps/web-antd/src/views/bpm/processExpression/index.vue index bd3f4e00e..80bf15bf0 100644 --- a/apps/web-antd/src/views/bpm/processExpression/index.vue +++ b/apps/web-antd/src/views/bpm/processExpression/index.vue @@ -1,18 +1,31 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/processInstance/index.vue b/apps/web-antd/src/views/bpm/processInstance/index.vue index 5386849f1..bf472bc1c 100644 --- a/apps/web-antd/src/views/bpm/processInstance/index.vue +++ b/apps/web-antd/src/views/bpm/processInstance/index.vue @@ -1,18 +1,34 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/processInstance/manager/index.vue b/apps/web-antd/src/views/bpm/processInstance/manager/index.vue index 619f0d93d..9dd48e7cb 100644 --- a/apps/web-antd/src/views/bpm/processInstance/manager/index.vue +++ b/apps/web-antd/src/views/bpm/processInstance/manager/index.vue @@ -1,18 +1,31 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/processListener/index.vue b/apps/web-antd/src/views/bpm/processListener/index.vue index 5556b0f40..6e88768d1 100644 --- a/apps/web-antd/src/views/bpm/processListener/index.vue +++ b/apps/web-antd/src/views/bpm/processListener/index.vue @@ -1,18 +1,34 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/task/copy/index.vue b/apps/web-antd/src/views/bpm/task/copy/index.vue index b26b7d8ee..3f5e2b5ed 100644 --- a/apps/web-antd/src/views/bpm/task/copy/index.vue +++ b/apps/web-antd/src/views/bpm/task/copy/index.vue @@ -1,18 +1,34 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/task/done/index.vue b/apps/web-antd/src/views/bpm/task/done/index.vue index b23eef1e5..bd5ffef00 100644 --- a/apps/web-antd/src/views/bpm/task/done/index.vue +++ b/apps/web-antd/src/views/bpm/task/done/index.vue @@ -1,21 +1,40 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/task/manager/index.vue b/apps/web-antd/src/views/bpm/task/manager/index.vue index d564ebbd7..0da6999bf 100644 --- a/apps/web-antd/src/views/bpm/task/manager/index.vue +++ b/apps/web-antd/src/views/bpm/task/manager/index.vue @@ -1,18 +1,31 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/bpm/task/todo/index.vue b/apps/web-antd/src/views/bpm/task/todo/index.vue index 976ddc010..9829b2eec 100644 --- a/apps/web-antd/src/views/bpm/task/todo/index.vue +++ b/apps/web-antd/src/views/bpm/task/todo/index.vue @@ -1,21 +1,40 @@ \ No newline at end of file + diff --git a/apps/web-antd/src/views/dashboard/workspace/index.vue b/apps/web-antd/src/views/dashboard/workspace/index.vue index f80e449a7..01a21c6e5 100644 --- a/apps/web-antd/src/views/dashboard/workspace/index.vue +++ b/apps/web-antd/src/views/dashboard/workspace/index.vue @@ -149,7 +149,7 @@ const todoItems = ref([ content: `国内使用最广泛的快速开发平台,远超 10w+ 企业使用`, date: '2024-07-10 11:15:00', title: '广泛企业认可', - } + }, ]); const trendItems: WorkbenchTrendItem[] = [ { diff --git a/apps/web-antd/src/views/infra/apiAccessLog/index.vue b/apps/web-antd/src/views/infra/apiAccessLog/index.vue index 3cb232a8c..3cc0ee5b4 100644 --- a/apps/web-antd/src/views/infra/apiAccessLog/index.vue +++ b/apps/web-antd/src/views/infra/apiAccessLog/index.vue @@ -88,7 +88,9 @@ const [Grid, gridApi] = useVbenVxeGrid({