gaoluyang
2026-06-24 0ce68379718b7c751b8a84aa4b28cbaf072e6c8f
开发文档及规范
已添加1个文件
1020 ■■■■■ 文件已修改
FRONTEND_DEVELOPMENT.md 1020 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
FRONTEND_DEVELOPMENT.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1020 @@
# å‰ç«¯å¼€å‘文档
本文档定义了项目的开发规范、组件复用指南和代码风格要求,确保团队开发风格统一。
---
## ä¸€ã€å¼€å‘规范
### 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)