FRONTEND_DEVELOPMENT.md
@@ -12,13 +12,13 @@
```typescript
// ✅ 正确:组件文件使用 PascalCase
UserList.vue
DictTag.vue
TableAction.vue
UserList.vue;
DictTag.vue;
TableAction.vue;
// ❌ 错误:使用 kebab-case 或其他命名
user-list.vue
dict_tag.vue
user - list.vue;
dict_tag.vue;
```
#### 组件定义
@@ -26,7 +26,7 @@
```vue
<script lang="ts" setup>
// ✅ 正确:统一使用 <script setup lang="ts">
defineOptions({ name: 'UserList' });
defineOptions({ name: "UserList" });
// Props 定义
interface Props {
@@ -35,13 +35,13 @@
}
const props = withDefaults(defineProps<Props>(), {
  userName: '',
  userName: "",
});
// Emits 定义
interface Emits {
  (e: 'update', value: string): void;
  (e: 'delete', id: number): void;
  (e: "update", value: string): void;
  (e: "delete", id: number): void;
}
const emit = defineEmits<Emits>();
@@ -68,10 +68,10 @@
}
// ✅ 正确:使用 import type 导入类型
import type { UserInfo } from '#/api/system/user';
import type { UserInfo } from "#/api/system/user";
// ❌ 错误:普通导入类型
import { UserInfo } from '#/api/system/user';
import { UserInfo } from "#/api/system/user";
```
### 1.2 目录结构规范
@@ -105,7 +105,7 @@
```typescript
// src/api/system/user/index.ts
import { request } from '#/utils/request';
import { request } from "#/utils/request";
export namespace SystemUserApi {
  /** 用户信息 */
@@ -137,7 +137,7 @@
/** 获取用户分页列表 */
export function getUserPage(params: SystemUserApi.PageParams) {
  return request.get<SystemUserApi.PageResult>('/system/user/page', { params });
  return request.get<SystemUserApi.PageResult>("/system/user/page", { params });
}
/** 获取用户详情 */
@@ -147,12 +147,12 @@
/** 创建用户 */
export function createUser(data: SystemUserApi.User) {
  return request.post('/system/user/create', data);
  return request.post("/system/user/create", data);
}
/** 更新用户 */
export function updateUser(data: SystemUserApi.User) {
  return request.put('/system/user/update', data);
  return request.put("/system/user/update", data);
}
/** 删除用户 */
@@ -199,28 +199,28 @@
```vue
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';
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';
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 },
const columns: VxeTableGridOptions["columns"] = [
  { type: "checkbox", width: 50 },
  { field: "username", title: "用户名", width: 120 },
  { field: "nickname", title: "昵称", width: 120 },
  {
    field: 'status',
    title: '状态',
    field: "status",
    title: "状态",
    width: 100,
    cellRender: {
      name: 'CellDict',
      props: { type: 'common_status' },
      name: "CellDict",
      props: { type: "common_status" },
    },
  },
  { field: 'createTime', title: '创建时间', width: 180 },
  { field: 'action', title: '操作', width: 200, slots: { default: 'actions' } },
  { field: "createTime", title: "创建时间", width: 180 },
  { field: "action", title: "操作", width: 200, slots: { default: "actions" } },
];
// 表格实例
@@ -228,23 +228,23 @@
  formOptions: {
    schema: [
      {
        fieldName: 'username',
        label: '用户名',
        component: 'Input',
        fieldName: "username",
        label: "用户名",
        component: "Input",
      },
      {
        fieldName: 'status',
        label: '状态',
        component: 'Select',
        fieldName: "status",
        label: "状态",
        component: "Select",
        componentProps: {
          options: getDictOptions('common_status'),
          options: getDictOptions("common_status"),
        },
      },
    ],
  },
  gridOptions: {
    columns,
    height: 'auto',
    height: "auto",
    keepSource: true,
    proxyConfig: {
      ajax: {
@@ -258,7 +258,7 @@
      },
    },
    rowConfig: {
      keyField: 'id',
      keyField: "id",
      isHover: true,
    },
  } as VxeTableGridOptions<SystemUserApi.User>,
@@ -302,22 +302,22 @@
#### 表格列渲染器
| 渲染器 | 用途 | 示例 |
|--------|------|------|
| `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 } }` |
| 渲染器          | 用途     | 示例                                                                 |
| --------------- | -------- | -------------------------------------------------------------------- |
| `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';
import { useVbenModal } from "#/packages/effects/common-ui/src";
import Form from "./modules/form.vue";
// 弹窗实例
const [FormModal, formModalApi] = useVbenModal({
@@ -346,14 +346,14 @@
```vue
<!-- modules/form.vue -->
<script lang="ts" setup>
import type { SystemUserApi } from '#/api/system/user';
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';
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 emit = defineEmits(["success"]);
const [Modal, modalApi] = useVbenModal({
  async onConfirm() {
@@ -363,7 +363,7 @@
    } else {
      await createUser(values);
    }
    emit('success');
    emit("success");
    modalApi.close();
  },
});
@@ -373,27 +373,27 @@
const [Form, formApi] = useVbenForm({
  schema: [
    {
      fieldName: 'username',
      label: '用户名',
      rules: 'required',
      component: 'Input',
      fieldName: "username",
      label: "用户名",
      rules: "required",
      component: "Input",
    },
    {
      fieldName: 'nickname',
      label: '昵称',
      rules: 'required',
      component: 'Input',
      fieldName: "nickname",
      label: "昵称",
      rules: "required",
      component: "Input",
    },
    {
      fieldName: 'email',
      label: '邮箱',
      rules: 'email',
      component: 'Input',
      fieldName: "email",
      label: "邮箱",
      rules: "email",
      component: "Input",
    },
    {
      fieldName: 'deptId',
      label: '部门',
      component: 'TreeSelect',
      fieldName: "deptId",
      label: "部门",
      component: "TreeSelect",
    },
  ],
});
@@ -422,7 +422,7 @@
```vue
<script lang="ts" setup>
import DictTag from '#/components/dict-tag/dict-tag.vue';
import DictTag from "#/components/dict-tag/dict-tag.vue";
</script>
<template>
@@ -436,7 +436,7 @@
```vue
<script lang="ts" setup>
import DictSelect from '#/components/form-create/components/dict-select.vue';
import DictSelect from "#/components/form-create/components/dict-select.vue";
</script>
<template>
@@ -462,16 +462,20 @@
#### 字典工具函数
```typescript
import { getDictOptions, getDictLabel, getDictObj } from '#/packages/effects/hooks/src';
import {
  getDictOptions,
  getDictLabel,
  getDictObj,
} from "#/packages/effects/hooks/src";
// 获取字典选项列表(用于 Select/Radio 等组件)
const options = getDictOptions('common_status', 'number');
const options = getDictOptions("common_status", "number");
// 获取字典标签文本
const label = getDictLabel('common_status', 1); // "开启"
const label = getDictLabel("common_status", 1); // "开启"
// 获取字典完整对象
const dictObj = getDictObj('common_status', 1);
const dictObj = getDictObj("common_status", 1);
// { label: '开启', value: 1, colorType: 'success', cssClass: '' }
```
@@ -481,7 +485,7 @@
```vue
<script lang="ts" setup>
import { TableAction, ACTION_ICON } from '#/adapter/vxe-table';
import { TableAction, ACTION_ICON } from "#/adapter/vxe-table";
</script>
<template>
@@ -523,9 +527,9 @@
```vue
<script lang="ts" setup>
import ImageUpload from '#/components/upload/image-upload.vue';
import ImageUpload from "#/components/upload/image-upload.vue";
const imageUrl = ref('');
const imageUrl = ref("");
const imageList = ref<string[]>([]);
</script>
@@ -549,7 +553,7 @@
```vue
<script lang="ts" setup>
import FileUpload from '#/components/upload/file-upload.vue';
import FileUpload from "#/components/upload/file-upload.vue";
const fileList = ref<string[]>([]);
</script>
@@ -570,26 +574,26 @@
```vue
<script lang="ts" setup>
import Description from '#/components/description/description.vue';
import Description from "#/components/description/description.vue";
const data = {
  username: 'admin',
  nickname: '管理员',
  email: 'admin@example.com',
  username: "admin",
  nickname: "管理员",
  email: "admin@example.com",
  status: 0,
  createTime: '2024-01-01 12:00:00',
  createTime: "2024-01-01 12:00:00",
};
const schema = [
  { field: 'username', label: '用户名' },
  { field: 'nickname', label: '昵称' },
  { field: 'email', label: '邮箱' },
  { field: "username", label: "用户名" },
  { field: "nickname", label: "昵称" },
  { field: "email", label: "邮箱" },
  {
    field: 'status',
    label: '状态',
    render: (value) => h(DictTag, { type: 'common_status', value }),
    field: "status",
    label: "状态",
    render: (value) => h(DictTag, { type: "common_status", value }),
  },
  { field: 'createTime', label: '创建时间' },
  { field: "createTime", label: "创建时间" },
];
</script>
@@ -608,16 +612,16 @@
#### 常用 Store
| Store | 用途 | 导入路径 |
|-------|------|----------|
| `useUserStore` | 用户信息 | `#/packages/stores/src` |
| Store            | 用途       | 导入路径                |
| ---------------- | ---------- | ----------------------- |
| `useUserStore`   | 用户信息   | `#/packages/stores/src` |
| `useAccessStore` | 权限/Token | `#/packages/stores/src` |
| `useDictStore` | 数据字典 | `#/packages/stores/src` |
| `useDictStore`   | 数据字典   | `#/packages/stores/src` |
#### 示例
```typescript
import { useUserStore, useAccessStore } from '#/packages/stores/src';
import { useUserStore, useAccessStore } from "#/packages/stores/src";
const userStore = useUserStore();
const accessStore = useAccessStore();
@@ -627,7 +631,7 @@
// 检查权限
const { hasAccessByCodes } = useAccess();
const canEdit = hasAccessByCodes(['system:user:update']);
const canEdit = hasAccessByCodes(["system:user:update"]);
```
---
@@ -638,7 +642,7 @@
```vue
<script lang="ts" setup>
import { useAccess } from '#/packages/effects/access/src';
import { useAccess } from "#/packages/effects/access/src";
const { hasAccessByCodes } = useAccess();
</script>
@@ -686,12 +690,12 @@
```vue
<script lang="ts" setup>
import { $t } from '#/locales';
import { $t } from "#/locales";
</script>
<template>
  <Button>{{ $t('common.save') }}</Button>
  <span>{{ $t('ui.actionMessage.deleteSuccess', ['用户']) }}</span>
  <Button>{{ $t("common.save") }}</Button>
  <span>{{ $t("ui.actionMessage.deleteSuccess", ["用户"]) }}</span>
</template>
```
@@ -721,7 +725,7 @@
### 6.1 usePagination - 分页
```typescript
import { usePagination } from '#/packages/effects/hooks/src';
import { usePagination } from "#/packages/effects/hooks/src";
const { page, pageSize, total, setPage, setTotal } = usePagination();
```
@@ -729,7 +733,7 @@
### 6.2 useTabs - 标签页操作
```typescript
import { useTabs } from '#/packages/effects/hooks/src';
import { useTabs } from "#/packages/effects/hooks/src";
const { closeOtherTabs, closeTab, refreshTab } = useTabs();
@@ -743,13 +747,13 @@
### 6.3 useWatermark - 水印
```typescript
import { useWatermark } from '#/packages/effects/hooks/src';
import { useWatermark } from "#/packages/effects/hooks/src";
const { updateWatermark, destroyWatermark } = useWatermark();
// 设置水印
await updateWatermark({
  content: '用户名',
  content: "用户名",
});
// 销毁水印
@@ -764,20 +768,20 @@
```typescript
import {
  formatDateTime,      // 日期格式化
  formatDate,          // 日期格式化(不含时间)
  formatDateTime, // 日期格式化
  formatDate, // 日期格式化(不含时间)
  downloadFileFromBlobPart, // 文件下载
  isEmpty,             // 判空
  isFunction,          // 判断函数
  isString,            // 判断字符串
} from '#/packages/utils/src';
  isEmpty, // 判空
  isFunction, // 判断函数
  isString, // 判断字符串
} from "#/packages/utils/src";
// 日期格式化
formatDateTime('2024-01-01T12:00:00'); // "2024-01-01 12:00:00"
formatDateTime("2024-01-01T12:00:00"); // "2024-01-01 12:00:00"
// 文件下载
downloadFileFromBlobPart({
  fileName: '用户列表.xlsx',
  fileName: "用户列表.xlsx",
  source: blobData,
});
@@ -791,10 +795,10 @@
```typescript
import {
  erpNumberFormatter,    // 数字格式化
  fenToYuan,             // 分转元
  formatFileSize,        // 文件大小格式化
} from '#/packages/utils/src';
  erpNumberFormatter, // 数字格式化
  fenToYuan, // 分转元
  formatFileSize, // 文件大小格式化
} from "#/packages/utils/src";
// 保留两位小数
erpNumberFormatter(1234.567, 2); // "1,234.57"
@@ -812,28 +816,28 @@
```vue
<script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { SystemUserApi } from '#/api/system/user';
import type { VxeTableGridOptions } from "#/adapter/vxe-table";
import type { SystemUserApi } from "#/api/system/user";
import { ref } from 'vue';
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 { 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 { message } from "ant-design-vue";
import { useVbenVxeGrid, TableAction, ACTION_ICON } from '#/adapter/vxe-table';
import { useVbenVxeGrid, TableAction, ACTION_ICON } from "#/adapter/vxe-table";
import {
  getUserPage,
  deleteUser,
  createUser,
  updateUser,
  exportUser,
} from '#/api/system/user';
import { $t } from '#/locales';
} from "#/api/system/user";
import { $t } from "#/locales";
import Form from './modules/form.vue';
import Form from "./modules/form.vue";
// ========== 弹窗定义 ==========
const [FormModal, formModalApi] = useVbenModal({
@@ -848,37 +852,42 @@
  formOptions: {
    schema: [
      {
        fieldName: 'username',
        label: '用户名',
        component: 'Input',
        componentProps: { placeholder: '请输入用户名' },
        fieldName: "username",
        label: "用户名",
        component: "Input",
        componentProps: { placeholder: "请输入用户名" },
      },
      {
        fieldName: 'status',
        label: '状态',
        component: 'Select',
        fieldName: "status",
        label: "状态",
        component: "Select",
        componentProps: {
          options: getDictOptions('common_status', 'number'),
          placeholder: '请选择状态',
          options: getDictOptions("common_status", "number"),
          placeholder: "请选择状态",
        },
      },
    ],
  },
  gridOptions: {
    columns: [
      { type: 'checkbox', width: 50 },
      { field: 'username', title: '用户名', width: 120 },
      { field: 'nickname', title: '昵称', width: 120 },
      { type: "checkbox", width: 50 },
      { field: "username", title: "用户名", width: 120 },
      { field: "nickname", title: "昵称", width: 120 },
      {
        field: 'status',
        title: '状态',
        field: "status",
        title: "状态",
        width: 100,
        cellRender: { name: 'CellDict', props: { type: 'common_status' } },
        cellRender: { name: "CellDict", props: { type: "common_status" } },
      },
      { field: 'createTime', title: '创建时间', width: 180 },
      { field: 'action', title: '操作', width: 180, slots: { default: 'actions' } },
      { field: "createTime", title: "创建时间", width: 180 },
      {
        field: "action",
        title: "操作",
        width: 180,
        slots: { default: "actions" },
      },
    ],
    height: 'auto',
    height: "auto",
    keepSource: true,
    proxyConfig: {
      ajax: {
@@ -891,7 +900,7 @@
        },
      },
    },
    rowConfig: { keyField: 'id', isHover: true },
    rowConfig: { keyField: "id", isHover: true },
    toolbarConfig: { refresh: true, search: true },
  } as VxeTableGridOptions<SystemUserApi.User>,
  gridEvents: {
@@ -911,7 +920,7 @@
async function handleExport() {
  const data = await exportUser(await gridApi.formApi.getValues());
  downloadFileFromBlobPart({ fileName: '用户.xls', source: data });
  downloadFileFromBlobPart({ fileName: "用户.xls", source: data });
}
function handleCreate() {
@@ -924,12 +933,12 @@
async function handleDelete(row: SystemUserApi.User) {
  await deleteUser(row.id!);
  message.success($t('ui.actionMessage.deleteSuccess', [row.username]));
  message.success($t("ui.actionMessage.deleteSuccess", [row.username]));
  handleRefresh();
}
async function handleDeleteBatch() {
  await confirm($t('ui.actionMessage.deleteBatchConfirm'));
  await confirm($t("ui.actionMessage.deleteBatchConfirm"));
  // ... 批量删除逻辑
}
</script>