| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # å端å¼åææ¡£ |
| | | |
| | | æ¬ææ¡£å®ä¹äºé¡¹ç®çå¼åè§èãç»ä»¶å¤ç¨æåå代ç 飿 ¼è¦æ±ï¼ç¡®ä¿å¢éå¼å飿 ¼ç»ä¸ã |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãå¼åè§è |
| | | |
| | | ### 1.1 ç»ä»¶å¼åè§è |
| | | |
| | | #### ç»ä»¶å½å |
| | | |
| | | ```typescript |
| | | // â
æ£ç¡®ï¼ç»ä»¶æä»¶ä½¿ç¨ PascalCase |
| | | UserList.vue |
| | | DictTag.vue |
| | | TableAction.vue |
| | | |
| | | // â é误ï¼ä½¿ç¨ kebab-case æå
¶ä»å½å |
| | | user-list.vue |
| | | dict_tag.vue |
| | | ``` |
| | | |
| | | #### ç»ä»¶å®ä¹ |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | // â
æ£ç¡®ï¼ç»ä¸ä½¿ç¨ <script setup lang="ts"> |
| | | defineOptions({ name: 'UserList' }); |
| | | |
| | | // Props å®ä¹ |
| | | interface Props { |
| | | userId: number; |
| | | userName?: string; |
| | | } |
| | | |
| | | const props = withDefaults(defineProps<Props>(), { |
| | | userName: '', |
| | | }); |
| | | |
| | | // Emits å®ä¹ |
| | | interface Emits { |
| | | (e: 'update', value: string): void; |
| | | (e: 'delete', id: number): void; |
| | | } |
| | | |
| | | const emit = defineEmits<Emits>(); |
| | | </script> |
| | | ``` |
| | | |
| | | #### TypeScript è§è |
| | | |
| | | ```typescript |
| | | // â
æ£ç¡®ï¼ç¦æ¢ä½¿ç¨ anyï¼ä½¿ç¨å
·ä½ç±»å |
| | | interface UserInfo { |
| | | id: number; |
| | | name: string; |
| | | email?: string; |
| | | } |
| | | |
| | | function getUser(id: number): Promise<UserInfo> { |
| | | return request.get(`/user/${id}`); |
| | | } |
| | | |
| | | // â é误ï¼ä½¿ç¨ any |
| | | function getUser(id: any): any { |
| | | return request.get(`/user/${id}`); |
| | | } |
| | | |
| | | // â
æ£ç¡®ï¼ä½¿ç¨ import type 导å
¥ç±»å |
| | | import type { UserInfo } from '#/api/system/user'; |
| | | |
| | | // â éè¯¯ï¼æ®é导å
¥ç±»å |
| | | import { UserInfo } from '#/api/system/user'; |
| | | ``` |
| | | |
| | | ### 1.2 ç®å½ç»æè§è |
| | | |
| | | ``` |
| | | src/ |
| | | âââ api/ # API æ¥å£å®ä¹ï¼ææ¨¡ååå |
| | | â âââ system/ # ç³»ç»æ¨¡å |
| | | â â âââ user/ |
| | | â â â âââ index.ts |
| | | â â âââ dept/ |
| | | â â âââ index.ts |
| | | â âââ ... |
| | | âââ components/ # å
Œ
±ç»ä»¶ï¼å
¨å±æ³¨åï¼ |
| | | âââ views/ # 页é¢ç»ä»¶ï¼æä¸å¡æ¨¡ååå |
| | | â âââ system/ |
| | | â âââ user/ |
| | | â âââ index.vue # å表页 |
| | | â âââ data.ts # æ°æ®å®ä¹ |
| | | â âââ modules/ # åç»ä»¶/å¼¹çª |
| | | â âââ form.vue |
| | | âââ locales/ # å½é
å |
| | | âââ router/ # è·¯ç±é
ç½® |
| | | âââ stores/ # Pinia ç¶æç®¡ç |
| | | âââ utils/ # å·¥å
·å½æ° |
| | | âââ adapter/ # éé
å¨é
ç½® |
| | | ``` |
| | | |
| | | ### 1.3 API æ¥å£è§è |
| | | |
| | | ```typescript |
| | | // src/api/system/user/index.ts |
| | | |
| | | import { request } from '#/utils/request'; |
| | | |
| | | export namespace SystemUserApi { |
| | | /** ç¨æ·ä¿¡æ¯ */ |
| | | export interface User { |
| | | id: number; |
| | | username: string; |
| | | nickname: string; |
| | | email?: string; |
| | | status: number; |
| | | deptId?: number; |
| | | createTime: string; |
| | | } |
| | | |
| | | /** ç¨æ·å页æ¥è¯¢åæ° */ |
| | | export interface PageParams { |
| | | pageNo: number; |
| | | pageSize: number; |
| | | username?: string; |
| | | status?: number; |
| | | deptId?: number; |
| | | } |
| | | |
| | | /** åé¡µç»æ */ |
| | | export interface PageResult { |
| | | list: User[]; |
| | | total: number; |
| | | } |
| | | } |
| | | |
| | | /** è·åç¨æ·å页å表 */ |
| | | export function getUserPage(params: SystemUserApi.PageParams) { |
| | | return request.get<SystemUserApi.PageResult>('/system/user/page', { params }); |
| | | } |
| | | |
| | | /** è·åç¨æ·è¯¦æ
*/ |
| | | export function getUser(id: number) { |
| | | return request.get<SystemUserApi.User>(`/system/user/get?id=${id}`); |
| | | } |
| | | |
| | | /** åå»ºç¨æ· */ |
| | | export function createUser(data: SystemUserApi.User) { |
| | | return request.post('/system/user/create', data); |
| | | } |
| | | |
| | | /** æ´æ°ç¨æ· */ |
| | | export function updateUser(data: SystemUserApi.User) { |
| | | return request.put('/system/user/update', data); |
| | | } |
| | | |
| | | /** å é¤ç¨æ· */ |
| | | export function deleteUser(id: number) { |
| | | return request.delete(`/system/user/delete?id=${id}`); |
| | | } |
| | | ``` |
| | | |
| | | ### 1.4 æ ·å¼è§è |
| | | |
| | | ```vue |
| | | <template> |
| | | <div class="user-list"> |
| | | <!-- ä¼å
ä½¿ç¨ Tailwind CSS ç±» --> |
| | | <div class="flex items-center justify-between p-4 mb-2 bg-white rounded-lg"> |
| | | <span class="text-base font-medium text-gray-900">æ é¢</span> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <style lang="scss" scoped> |
| | | /* ç»ä»¶æ ·å¼ä½¿ç¨ scoped */ |
| | | .user-list { |
| | | /* é¢è²ä½¿ç¨ CSS åé */ |
| | | background-color: var(--component-background); |
| | | |
| | | /* å¿
è¦æ¶è¦çç»ä»¶åºæ ·å¼ */ |
| | | :deep(.ant-table) { |
| | | border-radius: 8px; |
| | | } |
| | | } |
| | | </style> |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## äºãæ ¸å¿ç»ä»¶å¤ç¨æå |
| | | |
| | | ### 2.1 è¡¨æ ¼ç»ä»¶ (useVbenVxeGrid) |
| | | |
| | | 项ç®ä½¿ç¨ VxeTable å°è£
ç `useVbenVxeGrid` ä½ä¸ºæ ¸å¿è¡¨æ ¼ç»ä»¶ã |
| | | |
| | | #### åºç¡ç¨æ³ |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { SystemUserApi } from '#/api/system/user'; |
| | | |
| | | import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table'; |
| | | import { getUserPage } from '#/api/system/user'; |
| | | |
| | | // è¡¨æ ¼åé
ç½® |
| | | const columns: VxeTableGridOptions['columns'] = [ |
| | | { type: 'checkbox', width: 50 }, |
| | | { field: 'username', title: 'ç¨æ·å', width: 120 }, |
| | | { field: 'nickname', title: 'æµç§°', width: 120 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { |
| | | name: 'CellDict', |
| | | props: { type: 'common_status' }, |
| | | }, |
| | | }, |
| | | { field: 'createTime', title: 'å建æ¶é´', width: 180 }, |
| | | { field: 'action', title: 'æä½', width: 200, slots: { default: 'actions' } }, |
| | | ]; |
| | | |
| | | // è¡¨æ ¼å®ä¾ |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: [ |
| | | { |
| | | fieldName: 'username', |
| | | label: 'ç¨æ·å', |
| | | component: 'Input', |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions('common_status'), |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | gridOptions: { |
| | | columns, |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getUserPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { |
| | | keyField: 'id', |
| | | isHover: true, |
| | | }, |
| | | } as VxeTableGridOptions<SystemUserApi.User>, |
| | | }); |
| | | |
| | | // å·æ°è¡¨æ ¼ |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Grid table-title="ç¨æ·å表"> |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['system:user:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['system:user:delete'], |
| | | popConfirm: { |
| | | title: `确认å é¤ã${row.username}ãåï¼`, |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </template> |
| | | ``` |
| | | |
| | | #### è¡¨æ ¼åæ¸²æå¨ |
| | | |
| | | | 渲æå¨ | ç¨é | ç¤ºä¾ | |
| | | |--------|------|------| |
| | | | `CellImage` | åå¾å±ç¤º | `cellRender: { name: 'CellImage' }` | |
| | | | `CellImages` | å¤å¾å±ç¤º | `cellRender: { name: 'CellImages' }` | |
| | | | `CellLink` | 龿¥æé® | `cellRender: { name: 'CellLink', props: { text: 'æ¥ç' } }` | |
| | | | `CellTag` | æ ç¾å±ç¤º | `cellRender: { name: 'CellTag', props: { color: 'blue' } }` | |
| | | | `CellDict` | åå
¸æ ç¾ | `cellRender: { name: 'CellDict', props: { type: 'common_status' } }` | |
| | | | `CellSwitch` | å¼å
³åæ¢ | `cellRender: { name: 'CellSwitch', attrs: { beforeChange } }` | |
| | | | `CellOperation` | æä½æé® | `cellRender: { name: 'CellOperation', options: ['edit', 'delete'] }` | |
| | | |
| | | ### 2.2 表åå¼¹çª (useVbenModal) |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import Form from './modules/form.vue'; |
| | | |
| | | // å¼¹çªå®ä¾ |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | // æå¼æ°å¢å¼¹çª |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | // æå¼ç¼è¾å¼¹çª |
| | | function handleEdit(row: any) { |
| | | formModalApi.setData(row).open(); |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <FormModal @success="handleRefresh" /> |
| | | </template> |
| | | ``` |
| | | |
| | | 表åç»ä»¶ç¤ºä¾ï¼ |
| | | |
| | | ```vue |
| | | <!-- modules/form.vue --> |
| | | <script lang="ts" setup> |
| | | import type { SystemUserApi } from '#/api/system/user'; |
| | | |
| | | import { computed, ref } from 'vue'; |
| | | import { useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { useVbenForm } from '#/adapter/form'; |
| | | import { createUser, updateUser } from '#/api/system/user'; |
| | | |
| | | const emit = defineEmits(['success']); |
| | | |
| | | const [Modal, modalApi] = useVbenModal({ |
| | | async onConfirm() { |
| | | const values = await formApi.getValues(); |
| | | if (formData.value?.id) { |
| | | await updateUser({ ...values, id: formData.value.id }); |
| | | } else { |
| | | await createUser(values); |
| | | } |
| | | emit('success'); |
| | | modalApi.close(); |
| | | }, |
| | | }); |
| | | |
| | | const formData = ref<SystemUserApi.User>(); |
| | | |
| | | const [Form, formApi] = useVbenForm({ |
| | | schema: [ |
| | | { |
| | | fieldName: 'username', |
| | | label: 'ç¨æ·å', |
| | | rules: 'required', |
| | | component: 'Input', |
| | | }, |
| | | { |
| | | fieldName: 'nickname', |
| | | label: 'æµç§°', |
| | | rules: 'required', |
| | | component: 'Input', |
| | | }, |
| | | { |
| | | fieldName: 'email', |
| | | label: 'é®ç®±', |
| | | rules: 'email', |
| | | component: 'Input', |
| | | }, |
| | | { |
| | | fieldName: 'deptId', |
| | | label: 'é¨é¨', |
| | | component: 'TreeSelect', |
| | | }, |
| | | ], |
| | | }); |
| | | |
| | | // çå¬å¼¹çªæå¼ |
| | | modalApi.onOpen((data) => { |
| | | formData.value = data; |
| | | if (data) { |
| | | formApi.setValues(data); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <template> |
| | | <Modal title="ç¨æ·ä¿¡æ¯"> |
| | | <Form /> |
| | | </Modal> |
| | | </template> |
| | | ``` |
| | | |
| | | ### 2.3 åå
¸ç»ä»¶ |
| | | |
| | | #### DictTag - åå
¸æ ç¾ |
| | | |
| | | ç¨äºå±ç¤ºæ°æ®åå
¸å¼çæ ç¾å½¢å¼ã |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import DictTag from '#/components/dict-tag/dict-tag.vue'; |
| | | </script> |
| | | |
| | | <template> |
| | | <DictTag type="common_status" :value="1" /> |
| | | </template> |
| | | ``` |
| | | |
| | | #### DictSelect - åå
¸éæ©å¨ |
| | | |
| | | ç¨äºè¡¨åä¸ä»åå
¸éæ©å¼ã |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import DictSelect from '#/components/form-create/components/dict-select.vue'; |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- 䏿鿩 --> |
| | | <DictSelect v-model="form.status" dict-type="common_status" /> |
| | | |
| | | <!-- åéæé® --> |
| | | <DictSelect |
| | | v-model="form.status" |
| | | dict-type="common_status" |
| | | select-type="radio" |
| | | /> |
| | | |
| | | <!-- å¤éæ¡ --> |
| | | <DictSelect |
| | | v-model="form.types" |
| | | dict-type="common_status" |
| | | select-type="checkbox" |
| | | /> |
| | | </template> |
| | | ``` |
| | | |
| | | #### åå
¸å·¥å
·å½æ° |
| | | |
| | | ```typescript |
| | | import { getDictOptions, getDictLabel, getDictObj } from '#/packages/effects/hooks/src'; |
| | | |
| | | // è·ååå
¸é项å表ï¼ç¨äº Select/Radio çç»ä»¶ï¼ |
| | | const options = getDictOptions('common_status', 'number'); |
| | | |
| | | // è·ååå
¸æ ç¾ææ¬ |
| | | const label = getDictLabel('common_status', 1); // "å¼å¯" |
| | | |
| | | // è·ååå
¸å®æ´å¯¹è±¡ |
| | | const dictObj = getDictObj('common_status', 1); |
| | | // { label: 'å¼å¯', value: 1, colorType: 'success', cssClass: '' } |
| | | ``` |
| | | |
| | | ### 2.4 è¡¨æ ¼æä½ç»ä»¶ (TableAction) |
| | | |
| | | ç¨äºè¡¨æ ¼ä¸çæä½æé®ï¼æ¯ææéæ§å¶åäºæ¬¡ç¡®è®¤ã |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import { TableAction, ACTION_ICON } from '#/adapter/vxe-table'; |
| | | </script> |
| | | |
| | | <template> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['system:user:update'], |
| | | onClick: handleEdit, |
| | | }, |
| | | { |
| | | label: 'å é¤', |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['system:user:delete'], |
| | | popConfirm: { |
| | | title: '确认å é¤åï¼', |
| | | confirm: handleDelete, |
| | | }, |
| | | }, |
| | | ]" |
| | | :drop-down-actions="[ |
| | | { |
| | | label: 'åé
è§è²', |
| | | auth: ['system:permission:assign-user-role'], |
| | | onClick: handleAssignRole, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | ``` |
| | | |
| | | ### 2.5 æä»¶ä¸ä¼ ç»ä»¶ |
| | | |
| | | #### ImageUpload - å¾çä¸ä¼ |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import ImageUpload from '#/components/upload/image-upload.vue'; |
| | | |
| | | const imageUrl = ref(''); |
| | | const imageList = ref<string[]>([]); |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- åå¾ä¸ä¼ --> |
| | | <ImageUpload v-model="imageUrl" :max-number="1" /> |
| | | |
| | | <!-- å¤å¾ä¸ä¼ --> |
| | | <ImageUpload v-model="imageList" :max-number="5" /> |
| | | |
| | | <!-- éå¶å¤§å°åæ ¼å¼ --> |
| | | <ImageUpload |
| | | v-model="imageUrl" |
| | | :max-size="5" |
| | | :accept="['.jpg', '.jpeg', '.png']" |
| | | /> |
| | | </template> |
| | | ``` |
| | | |
| | | #### FileUpload - æä»¶ä¸ä¼ |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import FileUpload from '#/components/upload/file-upload.vue'; |
| | | |
| | | const fileList = ref<string[]>([]); |
| | | </script> |
| | | |
| | | <template> |
| | | <FileUpload |
| | | v-model="fileList" |
| | | :max-number="3" |
| | | :max-size="10" |
| | | :accept="['.pdf', '.doc', '.docx']" |
| | | /> |
| | | </template> |
| | | ``` |
| | | |
| | | ### 2.6 æè¿°å表ç»ä»¶ (Description) |
| | | |
| | | ç¨äºè¯¦æ
页å±ç¤ºæ°æ®ã |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import Description from '#/components/description/description.vue'; |
| | | |
| | | const data = { |
| | | username: 'admin', |
| | | nickname: '管çå', |
| | | email: 'admin@example.com', |
| | | status: 0, |
| | | createTime: '2024-01-01 12:00:00', |
| | | }; |
| | | |
| | | const schema = [ |
| | | { field: 'username', label: 'ç¨æ·å' }, |
| | | { field: 'nickname', label: 'æµç§°' }, |
| | | { field: 'email', label: 'é®ç®±' }, |
| | | { |
| | | field: 'status', |
| | | label: 'ç¶æ', |
| | | render: (value) => h(DictTag, { type: 'common_status', value }), |
| | | }, |
| | | { field: 'createTime', label: 'å建æ¶é´' }, |
| | | ]; |
| | | </script> |
| | | |
| | | <template> |
| | | <Description title="åºæ¬ä¿¡æ¯" :data="data" :schema="schema" /> |
| | | </template> |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãç¶æç®¡ç |
| | | |
| | | ### 3.1 ä½¿ç¨ Pinia Store |
| | | |
| | | 项ç®ä½¿ç¨ Pinia è¿è¡ç¶æç®¡çï¼Store å®ä¹å¨ `src/packages/stores/src` ä¸ã |
| | | |
| | | #### å¸¸ç¨ Store |
| | | |
| | | | Store | ç¨é | 导å
¥è·¯å¾ | |
| | | |-------|------|----------| |
| | | | `useUserStore` | ç¨æ·ä¿¡æ¯ | `#/packages/stores/src` | |
| | | | `useAccessStore` | æé/Token | `#/packages/stores/src` | |
| | | | `useDictStore` | æ°æ®åå
¸ | `#/packages/stores/src` | |
| | | |
| | | #### ç¤ºä¾ |
| | | |
| | | ```typescript |
| | | import { useUserStore, useAccessStore } from '#/packages/stores/src'; |
| | | |
| | | const userStore = useUserStore(); |
| | | const accessStore = useAccessStore(); |
| | | |
| | | // è·åç¨æ·ä¿¡æ¯ |
| | | const userInfo = userStore.userInfo; |
| | | |
| | | // æ£æ¥æé |
| | | const { hasAccessByCodes } = useAccess(); |
| | | const canEdit = hasAccessByCodes(['system:user:update']); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## åãæéæ§å¶ |
| | | |
| | | ### 4.1 æé®æé |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import { useAccess } from '#/packages/effects/access/src'; |
| | | |
| | | const { hasAccessByCodes } = useAccess(); |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- æ¹å¼ä¸ï¼éè¿ TableAction ç auth 屿§ --> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: 'ç¼è¾', |
| | | auth: ['system:user:update'], |
| | | onClick: handleEdit, |
| | | }, |
| | | ]" |
| | | /> |
| | | |
| | | <!-- æ¹å¼äºï¼éè¿ v-if æå¨å¤æ --> |
| | | <Button v-if="hasAccessByCodes(['system:user:create'])" type="primary"> |
| | | æ°å¢ |
| | | </Button> |
| | | </template> |
| | | ``` |
| | | |
| | | ### 4.2 è·¯ç±æé |
| | | |
| | | å¨è·¯ç±é
ç½®ä¸éè¿ `meta.authorities` 设置æéï¼ |
| | | |
| | | ```typescript |
| | | { |
| | | path: 'user', |
| | | name: 'SystemUser', |
| | | component: () => import('#/views/system/user/index.vue'), |
| | | meta: { |
| | | title: 'ç¨æ·ç®¡ç', |
| | | authorities: ['system:user:visit'], |
| | | }, |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## äºãå½é
å |
| | | |
| | | ### 5.1 ä½¿ç¨æ¹å¼ |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import { $t } from '#/locales'; |
| | | </script> |
| | | |
| | | <template> |
| | | <Button>{{ $t('common.save') }}</Button> |
| | | <span>{{ $t('ui.actionMessage.deleteSuccess', ['ç¨æ·']) }}</span> |
| | | </template> |
| | | ``` |
| | | |
| | | ### 5.2 æ·»å å½é
åææ¬ |
| | | |
| | | å¨ `src/locales` ç®å½ä¸çè¯è¨æä»¶ä¸æ·»å ï¼ |
| | | |
| | | ```json |
| | | // src/locales/zh-CN.json |
| | | { |
| | | "common": { |
| | | "save": "ä¿å", |
| | | "cancel": "åæ¶" |
| | | }, |
| | | "ui": { |
| | | "actionMessage": { |
| | | "deleteSuccess": "å é¤ã{0}ãæå" |
| | | } |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## å
ãå¸¸ç¨ Hooks |
| | | |
| | | ### 6.1 usePagination - å页 |
| | | |
| | | ```typescript |
| | | import { usePagination } from '#/packages/effects/hooks/src'; |
| | | |
| | | const { page, pageSize, total, setPage, setTotal } = usePagination(); |
| | | ``` |
| | | |
| | | ### 6.2 useTabs - æ ç¾é¡µæä½ |
| | | |
| | | ```typescript |
| | | import { useTabs } from '#/packages/effects/hooks/src'; |
| | | |
| | | const { closeOtherTabs, closeTab, refreshTab } = useTabs(); |
| | | |
| | | // å
³éå
¶ä»æ ç¾é¡µ |
| | | await closeOtherTabs(); |
| | | |
| | | // å·æ°å½å页 |
| | | await refreshTab(); |
| | | ``` |
| | | |
| | | ### 6.3 useWatermark - æ°´å° |
| | | |
| | | ```typescript |
| | | import { useWatermark } from '#/packages/effects/hooks/src'; |
| | | |
| | | const { updateWatermark, destroyWatermark } = useWatermark(); |
| | | |
| | | // è®¾ç½®æ°´å° |
| | | await updateWatermark({ |
| | | content: 'ç¨æ·å', |
| | | }); |
| | | |
| | | // éæ¯æ°´å° |
| | | destroyWatermark(); |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¸ãå·¥å
·å½æ° |
| | | |
| | | ### 7.1 常ç¨å·¥å
· |
| | | |
| | | ```typescript |
| | | import { |
| | | formatDateTime, // æ¥ææ ¼å¼å |
| | | formatDate, // æ¥ææ ¼å¼åï¼ä¸å«æ¶é´ï¼ |
| | | downloadFileFromBlobPart, // æä»¶ä¸è½½ |
| | | isEmpty, // å¤ç©º |
| | | isFunction, // 夿彿° |
| | | isString, // 夿å符串 |
| | | } from '#/packages/utils/src'; |
| | | |
| | | // æ¥ææ ¼å¼å |
| | | formatDateTime('2024-01-01T12:00:00'); // "2024-01-01 12:00:00" |
| | | |
| | | // æä»¶ä¸è½½ |
| | | downloadFileFromBlobPart({ |
| | | fileName: 'ç¨æ·å表.xlsx', |
| | | source: blobData, |
| | | }); |
| | | |
| | | // å¤ç©º |
| | | isEmpty([]); // true |
| | | isEmpty({}); // true |
| | | isEmpty(null); // true |
| | | ``` |
| | | |
| | | ### 7.2 æ°åæ ¼å¼å |
| | | |
| | | ```typescript |
| | | import { |
| | | erpNumberFormatter, // æ°åæ ¼å¼å |
| | | fenToYuan, // å转å
|
| | | formatFileSize, // æä»¶å¤§å°æ ¼å¼å |
| | | } from '#/packages/utils/src'; |
| | | |
| | | // ä¿ç两ä½å°æ° |
| | | erpNumberFormatter(1234.567, 2); // "1,234.57" |
| | | |
| | | // å转å
|
| | | fenToYuan(12345); // 123.45 |
| | | |
| | | // æä»¶å¤§å° |
| | | formatFileSize(1024 * 1024); // "1.00 MB" |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## å
«ã代ç 示ä¾ï¼æ å CRUD é¡µé¢ |
| | | |
| | | ```vue |
| | | <script lang="ts" setup> |
| | | import type { VxeTableGridOptions } from '#/adapter/vxe-table'; |
| | | import type { SystemUserApi } from '#/api/system/user'; |
| | | |
| | | import { ref } from 'vue'; |
| | | |
| | | import { confirm, Page, useVbenModal } from '#/packages/effects/common-ui/src'; |
| | | import { getDictOptions } from '#/packages/effects/hooks/src'; |
| | | import { downloadFileFromBlobPart, isEmpty } from '#/packages/utils/src'; |
| | | |
| | | import { message } from 'ant-design-vue'; |
| | | |
| | | import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table'; |
| | | import { |
| | | getUserPage, |
| | | deleteUser, |
| | | createUser, |
| | | updateUser, |
| | | exportUser, |
| | | } from '#/api/system/user'; |
| | | import { $t } from '#/locales'; |
| | | |
| | | import Form from './modules/form.vue'; |
| | | |
| | | // ========== å¼¹çªå®ä¹ ========== |
| | | const [FormModal, formModalApi] = useVbenModal({ |
| | | connectedComponent: Form, |
| | | destroyOnClose: true, |
| | | }); |
| | | |
| | | // ========== è¡¨æ ¼å®ä¹ ========== |
| | | const checkedIds = ref<number[]>([]); |
| | | |
| | | const [Grid, gridApi] = useVbenVxeGrid({ |
| | | formOptions: { |
| | | schema: [ |
| | | { |
| | | fieldName: 'username', |
| | | label: 'ç¨æ·å', |
| | | component: 'Input', |
| | | componentProps: { placeholder: '请è¾å
¥ç¨æ·å' }, |
| | | }, |
| | | { |
| | | fieldName: 'status', |
| | | label: 'ç¶æ', |
| | | component: 'Select', |
| | | componentProps: { |
| | | options: getDictOptions('common_status', 'number'), |
| | | placeholder: 'è¯·éæ©ç¶æ', |
| | | }, |
| | | }, |
| | | ], |
| | | }, |
| | | gridOptions: { |
| | | columns: [ |
| | | { type: 'checkbox', width: 50 }, |
| | | { field: 'username', title: 'ç¨æ·å', width: 120 }, |
| | | { field: 'nickname', title: 'æµç§°', width: 120 }, |
| | | { |
| | | field: 'status', |
| | | title: 'ç¶æ', |
| | | width: 100, |
| | | cellRender: { name: 'CellDict', props: { type: 'common_status' } }, |
| | | }, |
| | | { field: 'createTime', title: 'å建æ¶é´', width: 180 }, |
| | | { field: 'action', title: 'æä½', width: 180, slots: { default: 'actions' } }, |
| | | ], |
| | | height: 'auto', |
| | | keepSource: true, |
| | | proxyConfig: { |
| | | ajax: { |
| | | query: async ({ page }, formValues) => { |
| | | return await getUserPage({ |
| | | pageNo: page.currentPage, |
| | | pageSize: page.pageSize, |
| | | ...formValues, |
| | | }); |
| | | }, |
| | | }, |
| | | }, |
| | | rowConfig: { keyField: 'id', isHover: true }, |
| | | toolbarConfig: { refresh: true, search: true }, |
| | | } as VxeTableGridOptions<SystemUserApi.User>, |
| | | gridEvents: { |
| | | checkboxAll: ({ records }) => { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | }, |
| | | checkboxChange: ({ records }) => { |
| | | checkedIds.value = records.map((item) => item.id!); |
| | | }, |
| | | }, |
| | | }); |
| | | |
| | | // ========== æä½æ¹æ³ ========== |
| | | function handleRefresh() { |
| | | gridApi.query(); |
| | | } |
| | | |
| | | async function handleExport() { |
| | | const data = await exportUser(await gridApi.formApi.getValues()); |
| | | downloadFileFromBlobPart({ fileName: 'ç¨æ·.xls', source: data }); |
| | | } |
| | | |
| | | function handleCreate() { |
| | | formModalApi.setData(null).open(); |
| | | } |
| | | |
| | | function handleEdit(row: SystemUserApi.User) { |
| | | formModalApi.setData(row).open(); |
| | | } |
| | | |
| | | async function handleDelete(row: SystemUserApi.User) { |
| | | await deleteUser(row.id!); |
| | | message.success($t('ui.actionMessage.deleteSuccess', [row.username])); |
| | | handleRefresh(); |
| | | } |
| | | |
| | | async function handleDeleteBatch() { |
| | | await confirm($t('ui.actionMessage.deleteBatchConfirm')); |
| | | // ... æ¹éå é¤é»è¾ |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <Page auto-content-height> |
| | | <FormModal @success="handleRefresh" /> |
| | | |
| | | <Grid table-title="ç¨æ·å表"> |
| | | <template #toolbar-tools> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('ui.actionTitle.create', ['ç¨æ·']), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.ADD, |
| | | auth: ['system:user:create'], |
| | | onClick: handleCreate, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.export'), |
| | | type: 'primary', |
| | | icon: ACTION_ICON.DOWNLOAD, |
| | | auth: ['system:user:export'], |
| | | onClick: handleExport, |
| | | }, |
| | | { |
| | | label: $t('ui.actionTitle.deleteBatch'), |
| | | type: 'primary', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | disabled: isEmpty(checkedIds), |
| | | auth: ['system:user:delete'], |
| | | onClick: handleDeleteBatch, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | |
| | | <template #actions="{ row }"> |
| | | <TableAction |
| | | :actions="[ |
| | | { |
| | | label: $t('common.edit'), |
| | | type: 'link', |
| | | icon: ACTION_ICON.EDIT, |
| | | auth: ['system:user:update'], |
| | | onClick: handleEdit.bind(null, row), |
| | | }, |
| | | { |
| | | label: $t('common.delete'), |
| | | type: 'link', |
| | | danger: true, |
| | | icon: ACTION_ICON.DELETE, |
| | | auth: ['system:user:delete'], |
| | | popConfirm: { |
| | | title: $t('ui.actionMessage.deleteConfirm', [row.username]), |
| | | confirm: handleDelete.bind(null, row), |
| | | }, |
| | | }, |
| | | ]" |
| | | /> |
| | | </template> |
| | | </Grid> |
| | | </Page> |
| | | </template> |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## ä¹ã注æäºé¡¹ |
| | | |
| | | 1. **è·¯å¾å«å**ï¼`#/*` æå `./src/*`ï¼å¯¼å
¥æ¶ä½¿ç¨ `#/components/xxx` èéç¸å¯¹è·¯å¾ |
| | | 2. **表åéªè¯**ï¼ä½¿ç¨ VeeValidate + Zodï¼å¨ schema ä¸éè¿ `rules` 屿§å®ä¹ |
| | | 3. **HTML å®å
¨**ï¼ç¨æ·è¾å
¥ç HTML ä½¿ç¨ `v-dompurify-html` å¤ç |
| | | 4. **æææ°æ®**ï¼ä½¿ç¨ `secure-ls` å å¯åå¨ |
| | | 5. **ä»£ç æ³¨é**ï¼åªå¨ WHY ä¸ææ¾æ¶æ·»å 注éï¼é¿å
æ æä¹ç注é |
| | | 6. **ç»ä»¶å½å**ï¼å¿
é¡»ä½¿ç¨ `defineOptions({ name: 'ComponentName' })` å®ä¹ç»ä»¶å |
| | | |
| | | --- |
| | | |
| | | ## åãç¸å
³èµæº |
| | | |
| | | - [Vue 3 ææ¡£](https://vuejs.org/) |
| | | - [Ant Design Vue ææ¡£](https://antdv.com/) |
| | | - [VxeTable ææ¡£](https://vxetable.cn/) |
| | | - [Tailwind CSS ææ¡£](https://tailwindcss.com/) |
| | | - [é¡¹ç® CLAUDE.md](./CLAUDE.md) |