commit
						6794f1a7f7
					
				|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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:" | ||||
|  |  | |||
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 28 KiB | 
|  | @ -76,8 +76,8 @@ const withDefaultPlaceholder = <T extends Component>( | |||
|   componentProps: Recordable<any> = {}, | ||||
| ) => { | ||||
|   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', { | ||||
|     ApiSelect: withDefaultPlaceholder( | ||||
|       { | ||||
|         ...ApiComponent, | ||||
|         name: 'ApiSelect', | ||||
|       }, | ||||
|       'select', | ||||
|       { | ||||
|         component: Select, | ||||
|         loadingSlot: 'suffixIcon', | ||||
|         visibleEvent: 'onDropdownVisibleChange', | ||||
|         modelPropName: 'value', | ||||
|     }), | ||||
|     ApiTreeSelect: withDefaultPlaceholder(ApiComponent, 'select', { | ||||
|       }, | ||||
|     ), | ||||
|     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, | ||||
|  |  | |||
|  | @ -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<AuthApi.LoginResult>( | ||||
|     '/system/auth/social-login', | ||||
|     data, | ||||
|   ); | ||||
| }; | ||||
| } | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ export namespace InfraFileConfigApi { | |||
|     bucket?: string; | ||||
|     accessKey?: string; | ||||
|     accessSecret?: string; | ||||
|     pathStyle?: boolean; | ||||
|     domain: string; | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -0,0 +1,29 @@ | |||
| <!-- | ||||
|   参考自 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/components/ContentWrap/src/ContentWrap.vue | ||||
|   保证和 yudao-ui-admin-vue3 功能的一致性 | ||||
| --> | ||||
| <script lang="ts" setup> | ||||
| import type { CSSProperties } from 'vue'; | ||||
| 
 | ||||
| import { Card } from 'ant-design-vue'; | ||||
| 
 | ||||
| defineOptions({ name: 'ContentWrap' }); | ||||
| 
 | ||||
| withDefaults( | ||||
|   defineProps<{ | ||||
|     bodyStyle?: CSSProperties; | ||||
|     title?: string; | ||||
|   }>(), | ||||
|   { | ||||
|     bodyStyle: () => ({ padding: '10px' }), | ||||
|     title: '', | ||||
|   }, | ||||
| ); | ||||
| // TODO @puhui999:这个功能,和 vue3 貌似没对全哇? | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Card :body-style="bodyStyle" :title="title" class="mb-4"> | ||||
|     <slot></slot> | ||||
|   </Card> | ||||
| </template> | ||||
|  | @ -0,0 +1 @@ | |||
| export { default as ContentWrap } from './content-wrap.vue'; | ||||
|  | @ -0,0 +1,80 @@ | |||
| <script lang="tsx"> | ||||
| import type { DescriptionsProps } from 'ant-design-vue'; | ||||
| 
 | ||||
| import type { PropType } from 'vue'; | ||||
| 
 | ||||
| import type { DescriptionItemSchema, DescriptionsOptions } from './typing'; | ||||
| 
 | ||||
| import { defineComponent } from 'vue'; | ||||
| 
 | ||||
| import { Descriptions, DescriptionsItem } from 'ant-design-vue'; | ||||
| 
 | ||||
| /** 对 Descriptions 进行二次封装 */ | ||||
| const Description = defineComponent({ | ||||
|   name: 'Descriptions', | ||||
|   props: { | ||||
|     data: { | ||||
|       type: Object as PropType<Record<string, any>>, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|     schema: { | ||||
|       type: Array as PropType<DescriptionItemSchema[]>, | ||||
|       default: () => [], | ||||
|     }, | ||||
|     // Descriptions 原生 props | ||||
|     componentProps: { | ||||
|       type: Object as PropType<DescriptionsProps>, | ||||
|       default: () => ({}), | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|   setup(props: DescriptionsOptions) { | ||||
|     // TODO @puhui999:每个 field 的 slot 的考虑 | ||||
|     // TODO @puhui999:from 5.0:extra: () => getSlot(slots, 'extra') | ||||
|     /** 过滤掉不需要展示的 */ | ||||
|     const shouldShowItem = (item: DescriptionItemSchema) => { | ||||
|       if (item.hidden === undefined) return true; | ||||
|       return typeof item.hidden === 'function' | ||||
|         ? !item.hidden(props.data) | ||||
|         : !item.hidden; | ||||
|     }; | ||||
|     /** 渲染内容 */ | ||||
|     const renderContent = (item: DescriptionItemSchema) => { | ||||
|       if (item.content) { | ||||
|         return typeof item.content === 'function' | ||||
|           ? item.content(props.data) | ||||
|           : item.content; | ||||
|       } | ||||
|       return item.field ? props.data?.[item.field] : null; | ||||
|     }; | ||||
| 
 | ||||
|     return () => ( | ||||
|       <Descriptions | ||||
|         {...props} | ||||
|         bordered={props.componentProps?.bordered} | ||||
|         colon={props.componentProps?.colon} | ||||
|         column={props.componentProps?.column} | ||||
|         extra={props.componentProps?.extra} | ||||
|         layout={props.componentProps?.layout} | ||||
|         size={props.componentProps?.size} | ||||
|         title={props.componentProps?.title} | ||||
|       > | ||||
|         {props.schema?.filter(shouldShowItem).map((item) => ( | ||||
|           <DescriptionsItem | ||||
|             contentStyle={item.contentStyle} | ||||
|             key={item.field || String(item.label)} | ||||
|             label={item.label} | ||||
|             labelStyle={item.labelStyle} | ||||
|             span={item.span} | ||||
|           > | ||||
|             {renderContent(item)} | ||||
|           </DescriptionsItem> | ||||
|         ))} | ||||
|       </Descriptions> | ||||
|     ); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| // TODO @puhui999:from 5.0:emits: ['register'] 事件 | ||||
| export default Description; | ||||
| </script> | ||||
|  | @ -0,0 +1,3 @@ | |||
| export { default as Description } from './description.vue'; | ||||
| export * from './typing'; | ||||
| export { useDescription } from './use-description'; | ||||
|  | @ -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<string, any>; // 数据
 | ||||
|   schema?: DescriptionItemSchema[]; // 描述项配置
 | ||||
|   componentProps?: DescriptionsProps; // antd Descriptions 组件参数
 | ||||
| } | ||||
|  | @ -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<Record<string, any>>({}); | ||||
| 
 | ||||
|   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<DescriptionsOptions>) { | ||||
|     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; | ||||
| } | ||||
|  | @ -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: { | ||||
|  |  | |||
|  | @ -0,0 +1,75 @@ | |||
| <!-- 数据字典 Select 选择器 --> | ||||
| <script lang="ts" setup> | ||||
| import type { DictSelectProps } from '../typing'; | ||||
| 
 | ||||
| import { computed, useAttrs } from 'vue'; | ||||
| 
 | ||||
| import { | ||||
|   Checkbox, | ||||
|   CheckboxGroup, | ||||
|   Radio, | ||||
|   RadioGroup, | ||||
|   Select, | ||||
|   SelectOption, | ||||
| } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { getDictObj, getIntDictOptions, getStrDictOptions } from '#/utils/dict'; | ||||
| 
 | ||||
| defineOptions({ name: 'DictSelect' }); | ||||
| 
 | ||||
| const props = withDefaults(defineProps<DictSelectProps>(), { | ||||
|   valueType: 'str', | ||||
|   selectType: 'select', | ||||
| }); | ||||
| 
 | ||||
| const attrs = useAttrs(); | ||||
| 
 | ||||
| // 获得字典配置 | ||||
| // TODO @dhb:可以使用 getDictOptions 替代么? | ||||
| const getDictOptions = computed(() => { | ||||
|   switch (props.valueType) { | ||||
|     case 'bool': { | ||||
|       return getDictObj(props.dictType, 'bool'); | ||||
|     } | ||||
|     case 'int': { | ||||
|       return getIntDictOptions(props.dictType); | ||||
|     } | ||||
|     case 'str': { | ||||
|       return getStrDictOptions(props.dictType); | ||||
|     } | ||||
|     default: { | ||||
|       return []; | ||||
|     } | ||||
|   } | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs"> | ||||
|     <SelectOption | ||||
|       v-for="(dict, index) in getDictOptions" | ||||
|       :key="index" | ||||
|       :value="dict.value" | ||||
|     > | ||||
|       {{ dict.label }} | ||||
|     </SelectOption> | ||||
|   </Select> | ||||
|   <RadioGroup v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs"> | ||||
|     <Radio | ||||
|       v-for="(dict, index) in getDictOptions" | ||||
|       :key="index" | ||||
|       :value="dict.value" | ||||
|     > | ||||
|       {{ dict.label }} | ||||
|     </Radio> | ||||
|   </RadioGroup> | ||||
|   <CheckboxGroup v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs"> | ||||
|     <Checkbox | ||||
|       v-for="(dict, index) in getDictOptions" | ||||
|       :key="index" | ||||
|       :value="dict.value" | ||||
|     > | ||||
|       {{ dict.label }} | ||||
|     </Checkbox> | ||||
|   </CheckboxGroup> | ||||
| </template> | ||||
|  | @ -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<any[]>([]); // 下拉数据
 | ||||
|       const loading = ref(false); // 是否正在从远程获取数据
 | ||||
|       const queryParam = ref<any>(); // 当前输入的值
 | ||||
|       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 ( | ||||
|             <Select | ||||
|               class="w-1/1" | ||||
|               loading={loading.value} | ||||
|               mode="multiple" | ||||
|               {...attrs} | ||||
|               // TODO: remote 对等实现
 | ||||
|               // remote={props.remote}
 | ||||
|               {...(props.remote && { remoteMethod })} | ||||
|             > | ||||
|               {options.value.map( | ||||
|                 (item: { label: any; value: any }, index: any) => ( | ||||
|                   <SelectOption key={index} value={item.value}> | ||||
|                     {item.label} | ||||
|                   </SelectOption> | ||||
|                 ), | ||||
|               )} | ||||
|             </Select> | ||||
|           ); | ||||
|         } | ||||
|         return ( | ||||
|           <Select | ||||
|             class="w-1/1" | ||||
|             loading={loading.value} | ||||
|             {...attrs} | ||||
|             // TODO: @dhb52 remote 对等实现, 还是说没作用
 | ||||
|             // remote={props.remote}
 | ||||
|             {...(props.remote && { remoteMethod })} | ||||
|           > | ||||
|             {options.value.map( | ||||
|               (item: { label: any; value: any }, index: any) => ( | ||||
|                 <SelectOption key={index} value={item.value}> | ||||
|                   {item.label} | ||||
|                 </SelectOption> | ||||
|               ), | ||||
|             )} | ||||
|           </Select> | ||||
|         ); | ||||
|       }; | ||||
|       const buildCheckbox = () => { | ||||
|         if (isEmpty(options.value)) { | ||||
|           options.value = [ | ||||
|             { label: '选项1', value: '选项1' }, | ||||
|             { label: '选项2', value: '选项2' }, | ||||
|           ]; | ||||
|         } | ||||
|         return ( | ||||
|           <CheckboxGroup class="w-1/1" {...attrs}> | ||||
|             {options.value.map( | ||||
|               (item: { label: any; value: any }, index: any) => ( | ||||
|                 <Checkbox key={index} value={item.value}> | ||||
|                   {item.label} | ||||
|                 </Checkbox> | ||||
|               ), | ||||
|             )} | ||||
|           </CheckboxGroup> | ||||
|         ); | ||||
|       }; | ||||
|       const buildRadio = () => { | ||||
|         if (isEmpty(options.value)) { | ||||
|           options.value = [ | ||||
|             { label: '选项1', value: '选项1' }, | ||||
|             { label: '选项2', value: '选项2' }, | ||||
|           ]; | ||||
|         } | ||||
|         return ( | ||||
|           <RadioGroup class="w-1/1" {...attrs}> | ||||
|             {options.value.map( | ||||
|               (item: { label: any; value: any }, index: any) => ( | ||||
|                 <Radio key={index} value={item.value}> | ||||
|                   {item.label} | ||||
|                 </Radio> | ||||
|               ), | ||||
|             )} | ||||
|           </RadioGroup> | ||||
|         ); | ||||
|       }; | ||||
|       return () => ( | ||||
|         <> | ||||
|           {(() => { | ||||
|             switch (props.selectType) { | ||||
|               case 'checkbox': { | ||||
|                 return buildCheckbox(); | ||||
|               } | ||||
|               case 'radio': { | ||||
|                 return buildRadio(); | ||||
|               } | ||||
|               case 'select': { | ||||
|                 return buildSelect(); | ||||
|               } | ||||
|               default: { | ||||
|                 return buildSelect(); | ||||
|               } | ||||
|             } | ||||
|           })()} | ||||
|         </> | ||||
|       ); | ||||
|     }, | ||||
|   }); | ||||
| }; | ||||
|  | @ -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 }) => ( | ||||
|         <ImageUpload maxNumber={props.maxNumber} multiple={props.multiple} /> | ||||
|       ); | ||||
|     }, | ||||
|     name: 'ImagesUpload', | ||||
|   }); | ||||
| }; | ||||
|  | @ -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<string, any>, | ||||
|   fields: Array<Record<string, any>> = [], | ||||
|   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(); | ||||
|   }); | ||||
| }; | ||||
|  | @ -0,0 +1,3 @@ | |||
| export { useApiSelect } from './components/use-api-select'; | ||||
| 
 | ||||
| export { useFormCreateDesigner } from './helpers'; | ||||
|  | @ -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 }; | ||||
|  | @ -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'; | ||||
|  | @ -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, | ||||
|       ]); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | @ -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: '是否只读' }, | ||||
|       ]); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | @ -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, | ||||
|       ]); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | @ -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, | ||||
|         }, | ||||
|       ]); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | @ -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, | ||||
|         }, | ||||
|       ]); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | @ -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', | ||||
|         }, | ||||
|       ]); | ||||
|     }, | ||||
|   }; | ||||
| }; | ||||
|  | @ -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<Menu>; | ||||
| 
 | ||||
| // 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[]; // 事件配置
 | ||||
| } | ||||
|  | @ -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) => { | ||||
|  |  | |||
|  | @ -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<AxiosResponse<any>>; | ||||
|     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<boolean>(false); | ||||
| const { getStringAccept } = useUploadType({ | ||||
|  |  | |||
|  | @ -4,3 +4,5 @@ export enum UploadResultStatus { | |||
|   SUCCESS = 'success', | ||||
|   UPLOADING = 'uploading', | ||||
| } | ||||
| 
 | ||||
| export type UploadListType = 'picture' | 'picture-card' | 'text'; | ||||
|  |  | |||
|  | @ -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<NotificationItem[]>([]); | |||
| 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( | |||
|       <LockScreen :avatar @to-login="handleLogout" /> | ||||
|     </template> | ||||
|   </BasicLayout> | ||||
|   <HelpModal /> | ||||
| </template> | ||||
|  |  | |||
|  | @ -0,0 +1,93 @@ | |||
| <script lang="ts" setup> | ||||
| import { useVbenModal, VbenButton, VbenButtonGroup } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Image, Tag } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { $t } from '#/locales'; | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   draggable: true, | ||||
|   overlayBlur: 5, | ||||
|   footer: false, | ||||
|   onCancel() { | ||||
|     modalApi.close(); | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| function openWindow(url: string) { | ||||
|   window.open(url, '_blank'); | ||||
| } | ||||
| </script> | ||||
| <template> | ||||
|   <Modal class="w-[40%]" :title="$t('ui.widgets.qa')"> | ||||
|     <div class="mt-2 flex flex-col"> | ||||
|       <div class="mt-2 flex flex-row"> | ||||
|         <VbenButtonGroup class="basis-1/3" :gap="2" border size="large"> | ||||
|           <p class="p-2">项目地址:</p> | ||||
|           <VbenButton | ||||
|             variant="link" | ||||
|             @click=" | ||||
|               openWindow('https://gitee.com/yudaocode/yudao-ui-admin-vben') | ||||
|             " | ||||
|           > | ||||
|             Gitee | ||||
|           </VbenButton> | ||||
|           <VbenButton | ||||
|             variant="link" | ||||
|             @click=" | ||||
|               openWindow('https://github.com/yudaocode/yudao-ui-admin-vben') | ||||
|             " | ||||
|           > | ||||
|             Github | ||||
|           </VbenButton> | ||||
|         </VbenButtonGroup> | ||||
| 
 | ||||
|         <VbenButtonGroup class="basis-1/3" :gap="2" border size="large"> | ||||
|           <p class="p-2">issues:</p> | ||||
|           <VbenButton | ||||
|             variant="link" | ||||
|             @click=" | ||||
|               openWindow( | ||||
|                 'https://gitee.com/yudaocode/yudao-ui-admin-vben/issues', | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             Gitee | ||||
|           </VbenButton> | ||||
|           <VbenButton | ||||
|             variant="link" | ||||
|             @click=" | ||||
|               openWindow( | ||||
|                 'https://github.com/yudaocode/yudao-ui-admin-vben/issues', | ||||
|               ) | ||||
|             " | ||||
|           > | ||||
|             Github | ||||
|           </VbenButton> | ||||
|         </VbenButtonGroup> | ||||
| 
 | ||||
|         <VbenButtonGroup class="basis-1/3" :gap="2" border size="large"> | ||||
|           <p class="p-2">开发文档:</p> | ||||
|           <VbenButton | ||||
|             variant="link" | ||||
|             @click="openWindow('https://doc.iocoder.cn/quick-start/')" | ||||
|           > | ||||
|             项目文档 | ||||
|           </VbenButton> | ||||
|           <VbenButton variant="link" @click="openWindow('https://antdv.com/')"> | ||||
|             antdv 文档 | ||||
|           </VbenButton> | ||||
|         </VbenButtonGroup> | ||||
|       </div> | ||||
|       <p class="mt-2 flex justify-center"> | ||||
|         <span> | ||||
|           <Image src="../../../public/wx-xingyu.png" alt="数舵科技" /> | ||||
|         </span> | ||||
|       </p> | ||||
|       <p class="mt-2 flex justify-center pt-4 text-sm italic"> | ||||
|         本项目采用<Tag color="blue">MIT</Tag>开源协议,个人与企业可100% | ||||
|         免费使用。 | ||||
|       </p> | ||||
|     </div> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -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); | ||||
| }; | ||||
|  | @ -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实例 | ||||
|  |  | |||
|  | @ -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'), | ||||
|         }, | ||||
|       } | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|  |  | |||
|  | @ -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 }; | ||||
|  |  | |||
|  | @ -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 图片状态
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ const loginRef = ref(); | |||
| 
 | ||||
| /** 获取租户列表,并默认选中 */ | ||||
| const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表 | ||||
| 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)); | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|  |  | |||
|  | @ -29,7 +29,7 @@ const forgetPasswordRef = ref(); | |||
| 
 | ||||
| /** 获取租户列表,并默认选中 */ | ||||
| const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表 | ||||
| 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)); | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWo | |||
| 
 | ||||
| /** 获取租户列表,并默认选中 */ | ||||
| const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表 | ||||
| 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)); | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWo | |||
| 
 | ||||
| /** 获取租户列表,并默认选中 */ | ||||
| const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表 | ||||
| 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)); | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ const captchaType = 'blockPuzzle'; // 验证码类型:'blockPuzzle' | 'clickWo | |||
| 
 | ||||
| /** 获取租户列表,并默认选中 */ | ||||
| const tenantList = ref<AuthApi.TenantResult[]>([]); // 租户列表 | ||||
| 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)); | ||||
|           } | ||||
|         }, | ||||
|       }, | ||||
|  |  | |||
|  | @ -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 [ | ||||
|  |  | |||
|  | @ -1,17 +1,20 @@ | |||
| <script setup lang="ts"> | ||||
| import type { SystemUserProfileApi } from '#/api/system/user/profile'; | ||||
| 
 | ||||
| import { Card, Tabs } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| import ProfileUser from './modules/profile-user.vue'; | ||||
| import BaseInfo from './modules/base-info.vue'; | ||||
| import ResetPwd from './modules/reset-pwd.vue'; | ||||
| import UserSocial from './modules/user-social.vue'; | ||||
| 
 | ||||
| import { onMounted, ref } from 'vue'; | ||||
| 
 | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Card, Tabs } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { getUserProfile } from '#/api/system/user/profile'; | ||||
| import { useAuthStore } from '#/store'; | ||||
| 
 | ||||
| import BaseInfo from './modules/base-info.vue'; | ||||
| import ProfileUser from './modules/profile-user.vue'; | ||||
| import ResetPwd from './modules/reset-pwd.vue'; | ||||
| import UserSocial from './modules/user-social.vue'; | ||||
| 
 | ||||
| const authStore = useAuthStore(); | ||||
| const activeName = ref('basicInfo'); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,16 +1,21 @@ | |||
| <script setup lang="ts"> | ||||
| import type { Recordable } from '@vben/types'; | ||||
| 
 | ||||
| import type { SystemUserProfileApi } from '#/api/system/user/profile'; | ||||
| 
 | ||||
| import { watch } from 'vue'; | ||||
| 
 | ||||
| import { $t } from '@vben/locales'; | ||||
| 
 | ||||
| import { message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { watch } from 'vue'; | ||||
| import { useVbenForm, z } from '#/adapter/form'; | ||||
| import { DICT_TYPE, getDictOptions } from '#/utils/dict'; | ||||
| import { updateUserProfile } from '#/api/system/user/profile'; | ||||
| import { $t } from '@vben/locales'; | ||||
| import { DICT_TYPE, getDictOptions } from '#/utils/dict'; | ||||
| 
 | ||||
| const props = defineProps<{ profile?: SystemUserProfileApi.UserProfileRespVO }>(); | ||||
| const props = defineProps<{ | ||||
|   profile?: SystemUserProfileApi.UserProfileRespVO; | ||||
| }>(); | ||||
| const emit = defineEmits<{ | ||||
|   (e: 'success'): void; | ||||
| }>(); | ||||
|  | @ -87,11 +92,15 @@ async function handleSubmit(values: Recordable<any>) { | |||
| } | ||||
| 
 | ||||
| /** 监听 profile 变化 */ | ||||
| watch(() => props.profile, (newProfile) => { | ||||
| watch( | ||||
|   () => props.profile, | ||||
|   (newProfile) => { | ||||
|     if (newProfile) { | ||||
|       formApi.setValues(newProfile); | ||||
|     } | ||||
| }, { immediate: true }); | ||||
|   }, | ||||
|   { immediate: true }, | ||||
| ); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|  |  | |||
|  | @ -63,7 +63,6 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         min: 0, | ||||
|         class: 'w-full', | ||||
|         controlsPosition: 'right', | ||||
|         placeholder: '请输入分类排序', | ||||
|       }, | ||||
|  |  | |||
|  | @ -1,18 +1,34 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="审批接入(流程表单)" url="https://doc.iocoder.cn/bpm/use-bpm-form/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <DocAlert | ||||
|       title="审批接入(流程表单)" | ||||
|       url="https://doc.iocoder.cn/bpm/use-bpm-form/" | ||||
|     /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/form/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,31 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/group/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,34 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="审批接入(业务表单)" url="https://doc.iocoder.cn/bpm/use-business-form/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <DocAlert | ||||
|       title="审批接入(业务表单)" | ||||
|       url="https://doc.iocoder.cn/bpm/use-business-form/" | ||||
|     /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/oa/leave/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,31 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="流程表达式" url="https://doc.iocoder.cn/bpm/expression/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processExpression/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,34 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <DocAlert | ||||
|       title="流程发起、取消、重新发起" | ||||
|       url="https://doc.iocoder.cn/bpm/process-instance/" | ||||
|     /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,31 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processInstance/manager/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,34 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="执行监听器、任务监听器" url="https://doc.iocoder.cn/bpm/listener/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <DocAlert | ||||
|       title="执行监听器、任务监听器" | ||||
|       url="https://doc.iocoder.cn/bpm/listener/" | ||||
|     /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/processListener/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,34 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <DocAlert | ||||
|       title="审批转办、委派、抄送" | ||||
|       url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" | ||||
|     /> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/copy/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,21 +1,40 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" /> | ||||
|     <DocAlert | ||||
|       title="审批通过、不通过、驳回" | ||||
|       url="https://doc.iocoder.cn/bpm/task-todo-done/" | ||||
|     /> | ||||
|     <DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> | ||||
|     <DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" /> | ||||
|     <DocAlert | ||||
|       title="审批转办、委派、抄送" | ||||
|       url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" | ||||
|     /> | ||||
|     <DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/done/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,18 +1,31 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="工作流手册" url="https://doc.iocoder.cn/bpm/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/manager/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -1,21 +1,40 @@ | |||
| <script lang="ts" setup> | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { Button } from 'ant-design-vue'; | ||||
| import { Page } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { Button } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <DocAlert title="审批通过、不通过、驳回" url="https://doc.iocoder.cn/bpm/task-todo-done/" /> | ||||
|     <DocAlert | ||||
|       title="审批通过、不通过、驳回" | ||||
|       url="https://doc.iocoder.cn/bpm/task-todo-done/" | ||||
|     /> | ||||
|     <DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> | ||||
|     <DocAlert title="审批转办、委派、抄送" url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" /> | ||||
|     <DocAlert | ||||
|       title="审批转办、委派、抄送" | ||||
|       url="https://doc.iocoder.cn/bpm/task-delegation-and-cc/" | ||||
|     /> | ||||
|     <DocAlert title="审批加签、减签" url="https://doc.iocoder.cn/bpm/sign/" /> | ||||
|     <Button danger type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3"> | ||||
|     <Button | ||||
|       danger | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3" | ||||
|     > | ||||
|       该功能支持 Vue3 + element-plus 版本! | ||||
|     </Button> | ||||
|     <br /> | ||||
|     <Button type="link" target="_blank" href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index"> | ||||
|       可参考 https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index 代码,pull request 贡献给我们! | ||||
|     <Button | ||||
|       type="link" | ||||
|       target="_blank" | ||||
|       href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index" | ||||
|     > | ||||
|       可参考 | ||||
|       https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/bpm/task/todo/index | ||||
|       代码,pull request 贡献给我们! | ||||
|     </Button> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -149,7 +149,7 @@ const todoItems = ref<WorkbenchTodoItem[]>([ | |||
|     content: `国内使用最广泛的快速开发平台,远超 10w+ 企业使用`, | ||||
|     date: '2024-07-10 11:15:00', | ||||
|     title: '广泛企业认可', | ||||
|   } | ||||
|   }, | ||||
| ]); | ||||
| const trendItems: WorkbenchTrendItem[] = [ | ||||
|   { | ||||
|  |  | |||
|  | @ -88,7 +88,9 @@ const [Grid, gridApi] = useVbenVxeGrid({ | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <DetailModal @success="onRefresh" /> | ||||
|     <Grid table-title="API 访问日志列表"> | ||||
|  |  | |||
|  | @ -115,7 +115,9 @@ const [Grid, gridApi] = useVbenVxeGrid({ | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="系统日志" url="https://doc.iocoder.cn/system-log/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <DetailModal @success="onRefresh" /> | ||||
|     <Grid table-title="API 错误日志列表"> | ||||
|  |  | |||
|  | @ -0,0 +1,182 @@ | |||
| <!-- eslint-disable no-useless-escape --> | ||||
| <script setup lang="ts"> | ||||
| import { onMounted, ref, unref } from 'vue'; | ||||
| 
 | ||||
| import { Page, useVbenModal } from '@vben/common-ui'; | ||||
| import { isString } from '@vben/utils'; | ||||
| 
 | ||||
| import formCreate from '@form-create/ant-design-vue'; | ||||
| import FcDesigner from '@form-create/antd-designer'; | ||||
| import { useClipboard } from '@vueuse/core'; | ||||
| import { Button, message } from 'ant-design-vue'; | ||||
| import hljs from 'highlight.js'; | ||||
| import xml from 'highlight.js/lib/languages/java'; | ||||
| import json from 'highlight.js/lib/languages/json'; | ||||
| 
 | ||||
| import { useFormCreateDesigner } from '#/components/form-create'; | ||||
| 
 | ||||
| import 'highlight.js/styles/github.css'; | ||||
| 
 | ||||
| defineOptions({ name: 'InfraBuild' }); | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal(); | ||||
| 
 | ||||
| const designer = ref(); // 表单设计器 | ||||
| 
 | ||||
| // 表单设计器配置 | ||||
| const designerConfig = ref({ | ||||
|   switchType: [], // 是否可以切换组件类型,或者可以相互切换的字段 | ||||
|   autoActive: true, // 是否自动选中拖入的组件 | ||||
|   useTemplate: false, // 是否生成vue2语法的模板组件 | ||||
|   formOptions: { | ||||
|     form: { | ||||
|       labelWidth: '100px', // 设置默认的 label 宽度为 100px | ||||
|     }, | ||||
|   }, // 定义表单配置默认值 | ||||
|   fieldReadonly: false, // 配置field是否可以编辑 | ||||
|   hiddenDragMenu: false, // 隐藏拖拽操作按钮 | ||||
|   hiddenDragBtn: false, // 隐藏拖拽按钮 | ||||
|   hiddenMenu: [], // 隐藏部分菜单 | ||||
|   hiddenItem: [], // 隐藏部分组件 | ||||
|   hiddenItemConfig: {}, // 隐藏组件的部分配置项 | ||||
|   disabledItemConfig: {}, // 禁用组件的部分配置项 | ||||
|   showSaveBtn: false, // 是否显示保存按钮 | ||||
|   showConfig: true, // 是否显示右侧的配置界面 | ||||
|   showBaseForm: true, // 是否显示组件的基础配置表单 | ||||
|   showControl: true, // 是否显示组件联动 | ||||
|   showPropsForm: true, // 是否显示组件的属性配置表单 | ||||
|   showEventForm: true, // 是否显示组件的事件配置表单 | ||||
|   showValidateForm: true, // 是否显示组件的验证配置表单 | ||||
|   showFormConfig: true, // 是否显示表单配置 | ||||
|   showInputData: true, // 是否显示录入按钮 | ||||
|   showDevice: true, // 是否显示多端适配选项 | ||||
|   appendConfigData: [], // 定义渲染规则所需的formData | ||||
| }); | ||||
| 
 | ||||
| const dialogVisible = ref(false); // 弹窗的是否展示 | ||||
| const dialogTitle = ref(''); // 弹窗的标题 | ||||
| const formType = ref(-1); // 表单的类型:0 - 生成 JSON;1 - 生成 Options;2 - 生成组件 | ||||
| const formData = ref(''); // 表单数据 | ||||
| useFormCreateDesigner(designer); // 表单设计器增强 | ||||
| 
 | ||||
| /** 打开弹窗 */ | ||||
| const openModel = (title: string) => { | ||||
|   dialogVisible.value = true; | ||||
|   dialogTitle.value = title; | ||||
|   modalApi.open(); | ||||
| }; | ||||
| 
 | ||||
| /** 生成 JSON */ | ||||
| const showJson = () => { | ||||
|   openModel('生成 JSON'); | ||||
|   formType.value = 0; | ||||
|   formData.value = designer.value.getRule(); | ||||
| }; | ||||
| 
 | ||||
| /** 生成 Options */ | ||||
| const showOption = () => { | ||||
|   openModel('生成 Options'); | ||||
|   formType.value = 1; | ||||
|   formData.value = designer.value.getOption(); | ||||
| }; | ||||
| 
 | ||||
| /** 生成组件 */ | ||||
| const showTemplate = () => { | ||||
|   openModel('生成组件'); | ||||
|   formType.value = 2; | ||||
|   formData.value = makeTemplate(); | ||||
| }; | ||||
| 
 | ||||
| const makeTemplate = () => { | ||||
|   const rule = designer.value.getRule(); | ||||
|   const opt = designer.value.getOption(); | ||||
|   return `<template> | ||||
|     <form-create | ||||
|       v-model:api="fApi" | ||||
|       :rule="rule" | ||||
|       :option="option" | ||||
|       @submit="onSubmit" | ||||
|     ></form-create> | ||||
|   </template> | ||||
|   <script setup lang=ts> | ||||
|     const faps = ref(null) | ||||
|     const rule = ref('') | ||||
|     const option = ref('') | ||||
|     const init = () => { | ||||
|       rule.value = formCreate.parseJson('${formCreate.toJson(rule).replaceAll('\\', '\\\\')}') | ||||
|       option.value = formCreate.parseJson('${JSON.stringify(opt, null, 2)}') | ||||
|     } | ||||
|     const onSubmit = (formData) => { | ||||
|       //todo 提交表单 | ||||
|     } | ||||
|     init() | ||||
|   <\/script>`; | ||||
| }; | ||||
| 
 | ||||
| /** 复制 */ | ||||
| const copy = async (text: string) => { | ||||
|   const textToCopy = JSON.stringify(text, null, 2); | ||||
|   const { copy, copied, isSupported } = useClipboard({ source: textToCopy }); | ||||
|   if (isSupported) { | ||||
|     await copy(); | ||||
|     if (unref(copied)) { | ||||
|       message.success('复制成功'); | ||||
|     } | ||||
|   } else { | ||||
|     message.error('复制失败'); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * 代码高亮 | ||||
|  */ | ||||
| const highlightedCode = (code: string) => { | ||||
|   // 处理语言和代码 | ||||
|   let language = 'json'; | ||||
|   if (formType.value === 2) { | ||||
|     language = 'xml'; | ||||
|   } | ||||
|   // debugger | ||||
|   if (!isString(code)) { | ||||
|     code = JSON.stringify(code, null, 2); | ||||
|   } | ||||
|   // 高亮 | ||||
|   const result = hljs.highlight(code, { language, ignoreIllegals: true }); | ||||
|   return result.value || ' '; | ||||
| }; | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(async () => { | ||||
|   // 注册代码高亮的各种语言 | ||||
|   hljs.registerLanguage('xml', xml); | ||||
|   hljs.registerLanguage('json', json); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <FcDesigner ref="designer" height="90vh" :config="designerConfig"> | ||||
|       <template #handle> | ||||
|         <Button size="small" type="primary" ghost @click="showJson"> | ||||
|           生成JSON | ||||
|         </Button> | ||||
|         <Button size="small" type="primary" ghost @click="showOption"> | ||||
|           生成Options | ||||
|         </Button> | ||||
|         <Button size="small" type="primary" ghost @click="showTemplate"> | ||||
|           生成组件 | ||||
|         </Button> | ||||
|       </template> | ||||
|     </FcDesigner> | ||||
| 
 | ||||
|     <!-- 弹窗:表单预览 --> | ||||
|     <Modal :title="dialogTitle" :footer="false" :fullscreen-button="false"> | ||||
|       <div> | ||||
|         <Button style="float: right" @click="copy(formData)"> 复制 </Button> | ||||
|         <div> | ||||
|           <pre><code v-dompurify-html="highlightedCode(formData)" class="hljs"></code></pre> | ||||
|         </div> | ||||
|       </div> | ||||
|     </Modal> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -6,11 +6,12 @@ import type { | |||
| import type { InfraCodegenApi } from '#/api/infra/codegen'; | ||||
| import type { InfraDataSourceConfigApi } from '#/api/infra/data-source-config'; | ||||
| 
 | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import ImportTable from './modules/import-table.vue'; | ||||
| import PreviewCode from './modules/preview-code.vue'; | ||||
| import { ref } from 'vue'; | ||||
| import { useRouter } from 'vue-router'; | ||||
| 
 | ||||
| import { Page, useVbenModal } from '@vben/common-ui'; | ||||
| import { Plus } from '@vben/icons'; | ||||
| 
 | ||||
| import { Button, message } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
|  | @ -21,12 +22,12 @@ import { | |||
|   syncCodegenFromDB, | ||||
| } from '#/api/infra/codegen'; | ||||
| import { getDataSourceConfigList } from '#/api/infra/data-source-config'; | ||||
| import { DocAlert } from '#/components/doc-alert'; | ||||
| import { $t } from '#/locales'; | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| import { useGridColumns, useGridFormSchema } from './data'; | ||||
| 
 | ||||
| import { useRouter } from 'vue-router'; | ||||
| import ImportTable from './modules/import-table.vue'; | ||||
| import PreviewCode from './modules/preview-code.vue'; | ||||
| 
 | ||||
| const router = useRouter(); | ||||
| const dataSourceConfigList = ref<InfraDataSourceConfigApi.DataSourceConfig[]>( | ||||
|  | @ -139,14 +140,14 @@ function onActionClick({ | |||
|   row, | ||||
| }: OnActionClickParams<InfraCodegenApi.CodegenTable>) { | ||||
|   switch (code) { | ||||
|     case 'edit': { | ||||
|       onEdit(row); | ||||
|       break; | ||||
|     } | ||||
|     case 'delete': { | ||||
|       onDelete(row); | ||||
|       break; | ||||
|     } | ||||
|     case 'edit': { | ||||
|       onEdit(row); | ||||
|       break; | ||||
|     } | ||||
|     case 'generate': { | ||||
|       onGenerate(row); | ||||
|       break; | ||||
|  | @ -205,6 +206,7 @@ initDataSourceConfig(); | |||
| </script> | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert | ||||
|         title="代码生成(单表)" | ||||
|         url="https://doc.iocoder.cn/new-feature/" | ||||
|  | @ -218,6 +220,7 @@ initDataSourceConfig(); | |||
|         url="https://doc.iocoder.cn/new-feature/master-sub/" | ||||
|       /> | ||||
|       <DocAlert title="单元测试" url="https://doc.iocoder.cn/unit-test/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <ImportModal @success="onRefresh" /> | ||||
|     <PreviewModal /> | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| <script lang="ts" setup> | ||||
| import type { InfraCodegenApi } from '#/api/infra/codegen'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| import { watch } from 'vue'; | ||||
| 
 | ||||
| import { useVbenForm } from '#/adapter/form'; | ||||
| 
 | ||||
| import { useBasicInfoFormSchema } from '../data'; | ||||
| 
 | ||||
| const props = defineProps<{ | ||||
|  |  | |||
|  | @ -2,11 +2,12 @@ | |||
| import type { InfraCodegenApi } from '#/api/infra/codegen'; | ||||
| import type { SystemDictTypeApi } from '#/api/system/dict/type'; | ||||
| 
 | ||||
| import { nextTick, onMounted, ref, watch } from 'vue'; | ||||
| 
 | ||||
| import { Checkbox, Input, Select } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { useVbenVxeGrid } from '#/adapter/vxe-table'; | ||||
| import { getSimpleDictTypeList } from '#/api/system/dict/type'; | ||||
| import { nextTick, onMounted, ref, watch } from 'vue'; | ||||
| 
 | ||||
| import { useCodegenColumnTableColumns } from '../data'; | ||||
| 
 | ||||
|  |  | |||
|  | @ -22,6 +22,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -26,6 +26,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; | ||||
| 
 | ||||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { OnActionClickFn } from '#/adapter/vxe-table'; | ||||
| import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; | ||||
| 
 | ||||
| import { useAccess } from '@vben/access'; | ||||
|  |  | |||
|  | @ -26,6 +26,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; | ||||
| 
 | ||||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { OnActionClickFn } from '#/adapter/vxe-table'; | ||||
| import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { Demo02CategoryApi } from '#/api/infra/demo/demo02'; | ||||
| 
 | ||||
| import { useAccess } from '@vben/access'; | ||||
|  | @ -36,7 +34,6 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|           }); | ||||
|           return handleTree(data); | ||||
|         }, | ||||
|         class: 'w-full', | ||||
|         labelField: 'name', | ||||
|         valueField: 'id', | ||||
|         childrenField: 'children', | ||||
|  |  | |||
|  | @ -31,6 +31,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; | ||||
| 
 | ||||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { OnActionClickFn } from '#/adapter/vxe-table'; | ||||
| import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { Demo03StudentApi } from '#/api/infra/demo/demo03/erp'; | ||||
| 
 | ||||
| import { useAccess } from '@vben/access'; | ||||
|  |  | |||
|  | @ -26,6 +26,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useDemo03CourseFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -26,6 +26,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useDemo03GradeFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -26,6 +26,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; | ||||
| 
 | ||||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { OnActionClickFn } from '#/adapter/vxe-table'; | ||||
| import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { Demo03StudentApi } from '#/api/infra/demo/demo03/inner'; | ||||
| 
 | ||||
| import { useAccess } from '@vben/access'; | ||||
|  |  | |||
|  | @ -33,6 +33,13 @@ const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>(); | |||
| const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>(); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -1,7 +1,5 @@ | |||
| import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; | ||||
| 
 | ||||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { OnActionClickFn } from '#/adapter/vxe-table'; | ||||
| import type { OnActionClickFn, VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { Demo03StudentApi } from '#/api/infra/demo/demo03/normal'; | ||||
| 
 | ||||
| import { useAccess } from '@vben/access'; | ||||
|  |  | |||
|  | @ -33,6 +33,13 @@ const demo03CourseFormRef = ref<InstanceType<typeof Demo03CourseForm>>(); | |||
| const demo03GradeFormRef = ref<InstanceType<typeof Demo03GradeForm>>(); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -0,0 +1,267 @@ | |||
| <script lang="ts" setup> | ||||
| import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; | ||||
| 
 | ||||
| import { h, onMounted, reactive, ref } from 'vue'; | ||||
| 
 | ||||
| import { Page, useVbenModal } from '@vben/common-ui'; | ||||
| import { Download, Plus, RefreshCw, Search } from '@vben/icons'; | ||||
| import { cloneDeep, formatDateTime } from '@vben/utils'; | ||||
| 
 | ||||
| import { | ||||
|   Button, | ||||
|   Form, | ||||
|   Input, | ||||
|   message, | ||||
|   Pagination, | ||||
|   RangePicker, | ||||
|   Select, | ||||
| } from 'ant-design-vue'; | ||||
| import { VxeColumn, VxeTable } from 'vxe-table'; | ||||
| 
 | ||||
| import { | ||||
|   deleteDemo01Contact, | ||||
|   exportDemo01Contact, | ||||
|   getDemo01ContactPage, | ||||
| } from '#/api/infra/demo/demo01'; | ||||
| import { ContentWrap } from '#/components/content-wrap'; | ||||
| import { DictTag } from '#/components/dict-tag'; | ||||
| import { $t } from '#/locales'; | ||||
| import { getRangePickerDefaultProps } from '#/utils/date'; | ||||
| import { DICT_TYPE, getDictOptions } from '#/utils/dict'; | ||||
| import { downloadByData } from '#/utils/download'; | ||||
| 
 | ||||
| import Demo01ContactForm from './modules/form.vue'; | ||||
| 
 | ||||
| const loading = ref(true); // 列表的加载中 | ||||
| const list = ref<Demo01ContactApi.Demo01Contact[]>([]); // 列表的数据 | ||||
| const total = ref(0); // 列表的总页数 | ||||
| const queryParams = reactive({ | ||||
|   pageNo: 1, | ||||
|   pageSize: 10, | ||||
|   name: undefined, | ||||
|   sex: undefined, | ||||
|   createTime: undefined, | ||||
| }); | ||||
| const queryFormRef = ref(); // 搜索的表单 | ||||
| const exportLoading = ref(false); // 导出的加载中 | ||||
| 
 | ||||
| /** 查询列表 */ | ||||
| const getList = async () => { | ||||
|   loading.value = true; | ||||
|   try { | ||||
|     const params = cloneDeep(queryParams) as any; | ||||
|     if (params.createTime && Array.isArray(params.createTime)) { | ||||
|       params.createTime = (params.createTime as string[]).join(','); | ||||
|     } | ||||
|     const data = await getDemo01ContactPage(params); | ||||
|     list.value = data.list; | ||||
|     total.value = data.total; | ||||
|   } finally { | ||||
|     loading.value = false; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** 搜索按钮操作 */ | ||||
| const handleQuery = () => { | ||||
|   queryParams.pageNo = 1; | ||||
|   getList(); | ||||
| }; | ||||
| 
 | ||||
| /** 重置按钮操作 */ | ||||
| const resetQuery = () => { | ||||
|   queryFormRef.value.resetFields(); | ||||
|   handleQuery(); | ||||
| }; | ||||
| 
 | ||||
| const [FormModal, formModalApi] = useVbenModal({ | ||||
|   connectedComponent: Demo01ContactForm, | ||||
|   destroyOnClose: true, | ||||
| }); | ||||
| 
 | ||||
| /** 创建示例联系人 */ | ||||
| function onCreate() { | ||||
|   formModalApi.setData({}).open(); | ||||
| } | ||||
| 
 | ||||
| /** 编辑示例联系人 */ | ||||
| function onEdit(row: Demo01ContactApi.Demo01Contact) { | ||||
|   formModalApi.setData(row).open(); | ||||
| } | ||||
| 
 | ||||
| /** 删除示例联系人 */ | ||||
| async function onDelete(row: Demo01ContactApi.Demo01Contact) { | ||||
|   const hideLoading = message.loading({ | ||||
|     content: $t('ui.actionMessage.deleting', [row.id]), | ||||
|     duration: 0, | ||||
|     key: 'action_process_msg', | ||||
|   }); | ||||
|   try { | ||||
|     await deleteDemo01Contact(row.id as number); | ||||
|     message.success({ | ||||
|       content: $t('ui.actionMessage.deleteSuccess', [row.id]), | ||||
|       key: 'action_process_msg', | ||||
|     }); | ||||
|     await getList(); | ||||
|   } catch { | ||||
|     hideLoading(); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 导出表格 */ | ||||
| async function onExport() { | ||||
|   try { | ||||
|     exportLoading.value = true; | ||||
|     const data = await exportDemo01Contact(queryParams); | ||||
|     downloadByData(data, '示例联系人.xls'); | ||||
|   } finally { | ||||
|     exportLoading.value = false; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| /** 初始化 */ | ||||
| onMounted(() => { | ||||
|   getList(); | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <FormModal @success="getList" /> | ||||
| 
 | ||||
|     <ContentWrap> | ||||
|       <!-- 搜索工作栏 --> | ||||
|       <!-- TODO @puhui999:貌似 -mb-15px 没效果?可能和 ContentWrap 有关系? --> | ||||
|       <Form | ||||
|         class="-mb-15px" | ||||
|         :model="queryParams" | ||||
|         ref="queryFormRef" | ||||
|         layout="inline" | ||||
|       > | ||||
|         <Form.Item label="名字" name="name"> | ||||
|           <!-- TODO @puhui999:貌似不一定 240?看着和 schema 还是不太一样 --> | ||||
|           <Input | ||||
|             v-model:value="queryParams.name" | ||||
|             placeholder="请输入名字" | ||||
|             allow-clear | ||||
|             @press-enter="handleQuery" | ||||
|             class="!w-240px" | ||||
|           /> | ||||
|         </Form.Item> | ||||
|         <Form.Item label="性别" name="sex"> | ||||
|           <Select | ||||
|             v-model:value="queryParams.sex" | ||||
|             placeholder="请选择性别" | ||||
|             allow-clear | ||||
|             class="!w-240px" | ||||
|           > | ||||
|             <Select.Option | ||||
|               v-for="dict in getDictOptions( | ||||
|                 DICT_TYPE.SYSTEM_USER_SEX, | ||||
|                 'number', | ||||
|               )" | ||||
|               :key="dict.value" | ||||
|               :label="dict.label" | ||||
|               :value="dict.value" | ||||
|             /> | ||||
|           </Select> | ||||
|         </Form.Item> | ||||
|         <Form.Item label="创建时间" name="createTime"> | ||||
|           <!-- TODO @puhui999:这里有个红色的告警,看看有办法处理哇? --> | ||||
|           <RangePicker | ||||
|             v-model:value="queryParams.createTime" | ||||
|             v-bind="getRangePickerDefaultProps()" | ||||
|             class="!w-220px" | ||||
|           /> | ||||
|         </Form.Item> | ||||
|         <Form.Item> | ||||
|           <!-- TODO @puhui999:搜索和重置;貌似样子和位置不太一样,有木有办法一致 --> | ||||
|           <!-- TODO @puhui999:收齐、展开,好弄哇? --> | ||||
|           <Button class="ml-2" @click="handleQuery" :icon="h(Search)"> | ||||
|             搜索 | ||||
|           </Button> | ||||
|           <Button class="ml-2" @click="resetQuery" :icon="h(RefreshCw)"> | ||||
|             重置 | ||||
|           </Button> | ||||
|           <!-- TODO @puhui999:有办法放到 VxeTable 哪里么? --> | ||||
|           <Button | ||||
|             class="ml-2" | ||||
|             :icon="h(Plus)" | ||||
|             type="primary" | ||||
|             @click="onCreate" | ||||
|             v-access:code="['infra:demo01-contact:create']" | ||||
|           > | ||||
|             {{ $t('ui.actionTitle.create', ['示例联系人']) }} | ||||
|           </Button> | ||||
|           <Button | ||||
|             :icon="h(Download)" | ||||
|             type="primary" | ||||
|             class="ml-2" | ||||
|             :loading="exportLoading" | ||||
|             @click="onExport" | ||||
|             v-access:code="['infra:demo01-contact:export']" | ||||
|           > | ||||
|             {{ $t('ui.actionTitle.export') }} | ||||
|           </Button> | ||||
|         </Form.Item> | ||||
|       </Form> | ||||
|     </ContentWrap> | ||||
| 
 | ||||
|     <!-- 列表 --> | ||||
|     <!-- TODO @puhui999:title 要不还是假起来? --> | ||||
|     <ContentWrap> | ||||
|       <VxeTable :data="list" show-overflow :loading="loading"> | ||||
|         <VxeColumn field="id" title="编号" align="center" /> | ||||
|         <VxeColumn field="name" title="名字" align="center" /> | ||||
|         <VxeColumn field="sex" title="性别" align="center"> | ||||
|           <template #default="{ row }"> | ||||
|             <DictTag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="row.sex" /> | ||||
|           </template> | ||||
|         </VxeColumn> | ||||
|         <VxeColumn field="birthday" title="出生年" align="center"> | ||||
|           <template #default="{ row }"> | ||||
|             {{ formatDateTime(row.birthday) }} | ||||
|           </template> | ||||
|         </VxeColumn> | ||||
|         <VxeColumn field="description" title="简介" align="center" /> | ||||
|         <VxeColumn field="avatar" title="头像" align="center" /> | ||||
|         <VxeColumn field="createTime" title="创建时间" align="center"> | ||||
|           <template #default="{ row }"> | ||||
|             {{ formatDateTime(row.createTime) }} | ||||
|           </template> | ||||
|         </VxeColumn> | ||||
|         <VxeColumn field="operation" title="操作" align="center"> | ||||
|           <template #default="{ row }"> | ||||
|             <Button | ||||
|               size="small" | ||||
|               type="link" | ||||
|               @click="onEdit(row as any)" | ||||
|               v-access:code="['infra:demo01-contact:update']" | ||||
|             > | ||||
|               {{ $t('ui.actionTitle.edit') }} | ||||
|             </Button> | ||||
|             <Button | ||||
|               size="small" | ||||
|               type="link" | ||||
|               class="ml-2" | ||||
|               @click="onDelete(row as any)" | ||||
|               v-access:code="['infra:demo01-contact:delete']" | ||||
|             > | ||||
|               {{ $t('ui.actionTitle.delete') }} | ||||
|             </Button> | ||||
|           </template> | ||||
|         </VxeColumn> | ||||
|       </VxeTable> | ||||
|       <!-- 分页 --> | ||||
|       <div class="mt-2 flex justify-end"> | ||||
|         <!-- TODO @puhui999:这个分页,看着不太一致 --> | ||||
|         <Pagination | ||||
|           :total="total" | ||||
|           v-model:current="queryParams.pageNo" | ||||
|           v-model:page-size="queryParams.pageSize" | ||||
|           show-size-changer | ||||
|           @change="getList" | ||||
|         /> | ||||
|       </div> | ||||
|     </ContentWrap> | ||||
|   </Page> | ||||
| </template> | ||||
|  | @ -0,0 +1,150 @@ | |||
| <script lang="ts" setup> | ||||
| import type { Rule } from 'ant-design-vue/es/form'; | ||||
| 
 | ||||
| import type { Demo01ContactApi } from '#/api/infra/demo/demo01'; | ||||
| 
 | ||||
| import { computed, ref } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| 
 | ||||
| import { | ||||
|   DatePicker, | ||||
|   Form, | ||||
|   Input, | ||||
|   message, | ||||
|   Radio, | ||||
|   RadioGroup, | ||||
| } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { | ||||
|   createDemo01Contact, | ||||
|   getDemo01Contact, | ||||
|   updateDemo01Contact, | ||||
| } from '#/api/infra/demo/demo01'; | ||||
| import { Tinymce as RichTextarea } from '#/components/tinymce'; | ||||
| import { ImageUpload } from '#/components/upload'; | ||||
| import { $t } from '#/locales'; | ||||
| import { DICT_TYPE, getDictOptions } from '#/utils/dict'; | ||||
| 
 | ||||
| const emit = defineEmits(['success']); // TODO @puhui999:emit 和下面空一行? | ||||
| const formRef = ref(); | ||||
| // TODO @puhui999:labelCol、wrapperCol 直接写?不用单独定义变量, | ||||
| const labelCol = { span: 5 }; | ||||
| const wrapperCol = { span: 13 }; | ||||
| const formData = ref<Partial<Demo01ContactApi.Demo01Contact>>({ | ||||
|   id: undefined, | ||||
|   name: undefined, | ||||
|   sex: undefined, | ||||
|   birthday: undefined, | ||||
|   description: undefined, | ||||
|   avatar: undefined, | ||||
| }); | ||||
| const rules: Record<string, Rule[]> = { | ||||
|   name: [{ required: true, message: '名字不能为空', trigger: 'blur' }], | ||||
|   sex: [{ required: true, message: '性别不能为空', trigger: 'blur' }], | ||||
|   birthday: [{ required: true, message: '出生年不能为空', trigger: 'blur' }], | ||||
|   description: [{ required: true, message: '简介不能为空', trigger: 'blur' }], | ||||
| }; | ||||
| const getTitle = computed(() => { | ||||
|   return formData.value?.id | ||||
|     ? $t('ui.actionTitle.edit', ['示例联系人']) | ||||
|     : $t('ui.actionTitle.create', ['示例联系人']); | ||||
| }); | ||||
| 
 | ||||
| /** 重置表单 */ | ||||
| const resetForm = () => { | ||||
|   formData.value = { | ||||
|     id: undefined, | ||||
|     name: undefined, | ||||
|     sex: undefined, | ||||
|     birthday: undefined, | ||||
|     description: undefined, | ||||
|     avatar: undefined, | ||||
|   }; | ||||
|   formRef.value?.resetFields(); | ||||
| }; | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|   async onConfirm() { | ||||
|     await formRef.value?.validate(); | ||||
|     modalApi.lock(); | ||||
|     // 提交表单 | ||||
|     const data = formData.value as Demo01ContactApi.Demo01Contact; | ||||
|     try { | ||||
|       await (formData.value?.id | ||||
|         ? updateDemo01Contact(data) | ||||
|         : createDemo01Contact(data)); | ||||
|       // 关闭并提示 | ||||
|       await modalApi.close(); | ||||
|       emit('success'); | ||||
|       message.success({ | ||||
|         content: $t('ui.actionMessage.operationSuccess'), | ||||
|         key: 'action_process_msg', | ||||
|       }); | ||||
|     } finally { | ||||
|       modalApi.lock(false); | ||||
|     } | ||||
|   }, | ||||
|   async onOpenChange(isOpen: boolean) { | ||||
|     if (!isOpen) { | ||||
|       resetForm(); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     // 加载数据 TODO @puhui999:这里不用空行 | ||||
|     let data = modalApi.getData<Demo01ContactApi.Demo01Contact>(); | ||||
|     if (!data) { | ||||
|       return; | ||||
|     } | ||||
|     if (data.id) { | ||||
|       modalApi.lock(); | ||||
|       try { | ||||
|         data = await getDemo01Contact(data.id); | ||||
|       } finally { | ||||
|         modalApi.lock(false); | ||||
|       } | ||||
|     } | ||||
|     formData.value = data; | ||||
|   }, | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <Modal :title="getTitle"> | ||||
|     <Form | ||||
|       ref="formRef" | ||||
|       :model="formData" | ||||
|       :rules="rules" | ||||
|       :label-col="labelCol" | ||||
|       :wrapper-col="wrapperCol" | ||||
|     > | ||||
|       <Form.Item label="名字" name="name"> | ||||
|         <Input v-model:value="formData.name" placeholder="请输入名字" /> | ||||
|       </Form.Item> | ||||
|       <Form.Item label="性别" name="sex"> | ||||
|         <RadioGroup v-model:value="formData.sex"> | ||||
|           <Radio | ||||
|             v-for="dict in getDictOptions(DICT_TYPE.SYSTEM_USER_SEX, 'number')" | ||||
|             :key="dict.value" | ||||
|             :value="dict.value" | ||||
|           > | ||||
|             {{ dict.label }} | ||||
|           </Radio> | ||||
|         </RadioGroup> | ||||
|       </Form.Item> | ||||
|       <Form.Item label="出生年" name="birthday"> | ||||
|         <DatePicker | ||||
|           v-model:value="formData.birthday" | ||||
|           value-format="x" | ||||
|           placeholder="选择出生年" | ||||
|         /> | ||||
|       </Form.Item> | ||||
|       <Form.Item label="简介" name="description"> | ||||
|         <RichTextarea v-model="formData.description" height="500px" /> | ||||
|       </Form.Item> | ||||
|       <Form.Item label="头像" name="avatar"> | ||||
|         <ImageUpload v-model:value="formData.avatar" /> | ||||
|       </Form.Item> | ||||
|     </Form> | ||||
|   </Modal> | ||||
| </template> | ||||
|  | @ -25,11 +25,13 @@ onMounted(async () => { | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="数据库 MyBatis" url="https://doc.iocoder.cn/mybatis/" /> | ||||
|       <DocAlert | ||||
|         title="多数据源(读写分离)" | ||||
|         url="https://doc.iocoder.cn/dynamic-datasource/" | ||||
|       /> | ||||
|     </template> | ||||
| 
 | ||||
|     <IFrame v-if="!loading" v-loading="loading" :src="src" /> | ||||
|   </Page> | ||||
|  |  | |||
|  | @ -14,12 +14,17 @@ import { useFormSchema } from '../data'; | |||
| const emit = defineEmits(['success']); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|     hideLabel: true, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema().map((item) => ({ ...item, label: '' })), // 去除label | ||||
|   showDefaultActions: false, | ||||
|   commonConfig: { | ||||
|     hideLabel: true, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|  |  | |||
|  | @ -86,7 +86,6 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|       component: 'InputNumber', | ||||
|       componentProps: { | ||||
|         min: 0, | ||||
|         class: 'w-full', | ||||
|         controlsPosition: 'right', | ||||
|         placeholder: '请输入主机端口', | ||||
|       }, | ||||
|  | @ -134,6 +133,8 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|           { label: '主动模式', value: 'Active' }, | ||||
|           { label: '被动模式', value: 'Passive' }, | ||||
|         ], | ||||
|         buttonStyle: 'solid', | ||||
|         optionType: 'button', | ||||
|       }, | ||||
|       rules: 'required', | ||||
|       dependencies: { | ||||
|  | @ -194,6 +195,25 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|         show: (formValues) => formValues.storage === 20, | ||||
|       }, | ||||
|     }, | ||||
|     { | ||||
|       fieldName: 'config.enablePathStyleAccess', | ||||
|       label: '是否 Path Style', | ||||
|       component: 'RadioGroup', | ||||
|       componentProps: { | ||||
|         options: [ | ||||
|           { label: '启用', value: true }, | ||||
|           { label: '禁用', value: false }, | ||||
|         ], | ||||
|         buttonStyle: 'solid', | ||||
|         optionType: 'button', | ||||
|       }, | ||||
|       rules: 'required', | ||||
|       dependencies: { | ||||
|         triggerFields: ['storage'], | ||||
|         show: (formValues) => formValues.storage === 20, | ||||
|       }, | ||||
|       defaultValue: false, | ||||
|     }, | ||||
|     // 通用
 | ||||
|     { | ||||
|       fieldName: 'config.domain', | ||||
|  |  | |||
|  | @ -26,6 +26,13 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
|  | @ -65,7 +65,6 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|       componentProps: { | ||||
|         placeholder: '请输入重试次数。设置为 0 时,不进行重试', | ||||
|         min: 0, | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|  | @ -76,7 +75,6 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|       componentProps: { | ||||
|         placeholder: '请输入重试间隔,单位:毫秒。设置为 0 时,无需间隔', | ||||
|         min: 0, | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|       rules: 'required', | ||||
|     }, | ||||
|  | @ -87,7 +85,6 @@ export function useFormSchema(): VbenFormSchema[] { | |||
|       componentProps: { | ||||
|         placeholder: '请输入监控超时时间,单位:毫秒', | ||||
|         min: 0, | ||||
|         class: 'w-full', | ||||
|       }, | ||||
|     }, | ||||
|   ]; | ||||
|  | @ -196,7 +193,7 @@ export function useGridColumns<T = InfraJobApi.Job>( | |||
|             text: '暂停', | ||||
|             show: (row: any) => | ||||
|               hasAccessByCodes(['infra:job:update']) && | ||||
|               row.status == InfraJobStatusEnum.NORMAL, | ||||
|               row.status === InfraJobStatusEnum.NORMAL, | ||||
|           }, | ||||
|           { | ||||
|             code: 'trigger', | ||||
|  |  | |||
|  | @ -192,9 +192,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="定时任务" url="https://doc.iocoder.cn/job/" /> | ||||
|       <DocAlert title="异步任务" url="https://doc.iocoder.cn/async-task/" /> | ||||
|       <DocAlert title="消息队列" url="https://doc.iocoder.cn/message-queue/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <FormModal @success="onRefresh" /> | ||||
|     <DetailModal /> | ||||
|  |  | |||
|  | @ -88,9 +88,11 @@ const [Grid, gridApi] = useVbenVxeGrid({ | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="定时任务" url="https://doc.iocoder.cn/job/" /> | ||||
|       <DocAlert title="异步任务" url="https://doc.iocoder.cn/async-task/" /> | ||||
|       <DocAlert title="消息队列" url="https://doc.iocoder.cn/message-queue/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <DetailModal /> | ||||
|     <Grid table-title="任务日志列表"> | ||||
|  |  | |||
|  | @ -22,12 +22,16 @@ const getTitle = computed(() => { | |||
| }); | ||||
| 
 | ||||
| const [Form, formApi] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|   commonConfig: { | ||||
|     labelWidth: 140, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const [Modal, modalApi] = useVbenModal({ | ||||
|  |  | |||
|  | @ -32,8 +32,10 @@ onMounted(() => { | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="Redis 缓存" url="https://doc.iocoder.cn/redis-cache/" /> | ||||
|       <DocAlert title="本地缓存" url="https://doc.iocoder.cn/local-cache/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <Card class="mt-5" title="Redis 概览"> | ||||
|       <Info :redis-data="redisData" /> | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ defineProps<{ | |||
|       {{ redisData?.info?.redis_version }} | ||||
|     </Descriptions.Item> | ||||
|     <Descriptions.Item label="运行模式"> | ||||
|       {{ redisData?.info?.redis_mode == 'standalone' ? '单机' : '集群' }} | ||||
|       {{ redisData?.info?.redis_mode === 'standalone' ? '单机' : '集群' }} | ||||
|     </Descriptions.Item> | ||||
|     <Descriptions.Item label="端口"> | ||||
|       {{ redisData?.info?.tcp_port }} | ||||
|  | @ -44,7 +44,7 @@ defineProps<{ | |||
|       {{ redisData?.info?.maxmemory_human }} | ||||
|     </Descriptions.Item> | ||||
|     <Descriptions.Item label="AOF 是否开启"> | ||||
|       {{ redisData?.info?.aof_enabled == '0' ? '否' : '是' }} | ||||
|       {{ redisData?.info?.aof_enabled === '0' ? '否' : '是' }} | ||||
|     </Descriptions.Item> | ||||
|     <Descriptions.Item label="RDB 是否成功"> | ||||
|       {{ redisData?.info?.rdb_last_bgsave_status }} | ||||
|  |  | |||
|  | @ -28,7 +28,9 @@ onMounted(async () => { | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="服务监控" url="https://doc.iocoder.cn/server-monitor/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <IFrame v-if="!loading" v-loading="loading" :src="src" /> | ||||
|   </Page> | ||||
|  |  | |||
|  | @ -25,7 +25,9 @@ onMounted(async () => { | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="服务监控" url="https://doc.iocoder.cn/server-monitor/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <IFrame v-if="!loading" v-loading="loading" :src="src" /> | ||||
|   </Page> | ||||
|  |  | |||
|  | @ -26,7 +26,9 @@ onMounted(async () => { | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="接口文档" url="https://doc.iocoder.cn/api-doc/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <IFrame v-if="!loading" :src="src" /> | ||||
|   </Page> | ||||
|  |  | |||
|  | @ -175,10 +175,12 @@ onMounted(async () => { | |||
| 
 | ||||
| <template> | ||||
|   <Page> | ||||
|     <template #doc> | ||||
|       <DocAlert | ||||
|         title="WebSocket 实时通信" | ||||
|         url="https://doc.iocoder.cn/websocket/" | ||||
|       /> | ||||
|     </template> | ||||
| 
 | ||||
|     <div class="mt-4 flex flex-col gap-4 md:flex-row"> | ||||
|       <!-- 左侧:建立连接、发送消息 --> | ||||
|  |  | |||
|  | @ -1,6 +1,5 @@ | |||
| import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; | ||||
| 
 | ||||
| import type { VbenFormSchema } from '#/adapter/form'; | ||||
| import type { VxeTableGridOptions } from '#/adapter/vxe-table'; | ||||
| import type { SystemAreaApi } from '#/api/system/area'; | ||||
| 
 | ||||
| /** 查询 IP 的表单 */ | ||||
|  |  | |||
|  | @ -59,7 +59,9 @@ const [Grid, gridApi] = useVbenVxeGrid({ | |||
| 
 | ||||
| <template> | ||||
|   <Page auto-content-height> | ||||
|     <template #doc> | ||||
|       <DocAlert title="地区 & IP" url="https://doc.iocoder.cn/area-and-ip/" /> | ||||
|     </template> | ||||
| 
 | ||||
|     <FormModal @success="onRefresh" /> | ||||
|     <Grid table-title="地区列表"> | ||||
|  |  | |||
|  | @ -10,6 +10,13 @@ import { $t } from '#/locales'; | |||
| import { useFormSchema } from '../data'; | ||||
| 
 | ||||
| const [Form, { setFieldValue, validate, getValues }] = useVbenForm({ | ||||
|   commonConfig: { | ||||
|     componentProps: { | ||||
|       class: 'w-full', | ||||
|     }, | ||||
|     formItemClass: 'col-span-2', | ||||
|     labelWidth: 80, | ||||
|   }, | ||||
|   layout: 'horizontal', | ||||
|   schema: useFormSchema(), | ||||
|   showDefaultActions: false, | ||||
|  |  | |||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue
	
	 xingyu
						xingyu