# Vue3 Admin Template

基于 **Vue3 + Vite + TypeScript + Element Plus** 的后台管理模板。

本模板经过重构，已将通用的请求、工具、Hooks、UI 组件抽离到 **`@sud-web/*`** 公共库统一维护，业务工程只保留页面与接口定义，开箱即用。

---

## 一、技术栈

| 类型     | 选型                                                          |
| -------- | ------------------------------------------------------------- |
| 框架     | Vue 3.5 + `<script setup>`                                    |
| 构建     | Vite 7 + TypeScript 5                                         |
| UI       | Element Plus（按需）                                          |
| 状态     | Pinia                                                         |
| 路由     | Vue Router 4（常驻路由 + 动态路由）                           |
| 国际化   | vue-i18n                                                      |
| 原子样式 | UnoCSS                                                        |
| 公共能力 | `@sud-web/http`、`@sud-web/utils`、`@sud-web/vue-hooks`、`@sud-web/vue-ui` |

### `@sud-web/*` 公共库一览

| 包                    | 作用                                                                 |
| --------------------- | -------------------------------------------------------------------- |
| `@sud-web/http`       | Axios 封装：CSRF/Token 注入、401 自动刷新、统一错误处理（`createAxios` / `RefreshManager`） |
| `@sud-web/utils`      | 通用工具（如 SSO 登录跳转 `redirectToLogin` 等）                     |
| `@sud-web/vue-hooks`  | 业务 Hooks，如 `useTablePage` / `useModalForm` / `useDialogOut`      |
| `@sud-web/vue-ui`     | 公共组件 `PcSearch` / `PcTable` / `Pagination` 及对应 TS 类型        |

> 各公共库的详细 API 见 sud-web 仓库 `docs/` 目录（vue-ui / vue-hooks / http / utils）。

---

## 二、快速开始

```bash
# 安装依赖
npm install

# 启动开发服务（默认 vite dev）
npm run start

# 代码检查并自动修复
npm run lint

# 单元测试
npm run test
```

### 打包

| 环境     | 命令               |
| -------- | ------------------ |
| 测试环境 | `npm run build:FAT` |
| 生产环境 | `npm run build`     |

> 打包均会先执行 `vue-tsc` 做类型检查再 `vite build`。

---

## 三、环境变量

环境变量按 `.env.development` / `.env.fat` / `.env.production` 区分，自定义变量必须以 `VITE_` 开头。

| 变量                                | 说明                                          |
| ----------------------------------- | --------------------------------------------- |
| `VITE_APP_BASE_URL`                 | 后端接口地址（请求经 vite 代理 `/api` 转发）  |
| `VITE_PUBLIC_PATH`                  | 部署路径前缀，一般为 `/`                       |
| `VITE_AUTH_BASE_URL`                | SSO 统一登录地址                              |
| `VITE_PUBLIC_AUTH_REDIRECT_ACTION`  | SSO 自动登录 redirectAction 地址             |
| `VITE_AUTH_SERVERBASE_URL`          | 退出登录后端地址                             |

> 接口统一走 vite 代理的 `/api`，请求实例 **不设 baseURL**，由代理转发到 `VITE_APP_BASE_URL`。

### SSO 登录模板本地联调

> 仅 SSO（统一登录）模板适用；企业微信模板的本地登录方式见仓库内说明。

- **dev 端口固定 `3800`**：SSO 鉴权按 channel 校验回跳地址，回跳地址 = `当前 origin + VITE_PUBLIC_PATH`（即 `http://localhost:3800/`），默认 `pink_web` channel 已在 SSO 系统注册该地址。**改 `devPort`（vite 端口）会让回跳地址不在白名单 → 登录失败**。确需换端口时，先到 SSO 系统给对应 channel 注册新的 `http://localhost:<新端口>/` 回跳地址，或改用你自己的 `ssoChannel`。
- **切换业务后端重启测试**：把 `.env.development` 的 `VITE_APP_BASE_URL` 改成你业务项目的服务端地址，重启 `npm run start` 即可联调——业务请求经 vite `/api` 代理转发到该地址；SSO 登录仍走 `VITE_AUTH_BASE_URL`，与业务后端互不影响。

---

## 四、目录结构

```text
src/
├── api/            # 接口定义，按业务模块分目录
├── assets/         # 静态资源
├── components/     # 业务组件（公共组件已移至 @sud-web/vue-ui）
├── config/         # 全局配置（白名单等）
├── constants/      # 常量（缓存 key 等）
├── directives/     # 自定义指令
├── hooks/          # 工程内 Hooks（通用 Hooks 已移至 @sud-web/vue-hooks）
├── lang/           # 国际化
├── layout/         # 布局（侧边栏 / 顶栏 / TagsView 等）
├── plugins/        # 插件加载
├── router/         # 路由与权限拦截
├── store/          # Pinia 状态
├── styles/         # 全局样式
├── utils/          # 工程工具与请求实例（service.ts）
└── views/          # 页面
```

---

## 五、核心约定

### 1. 请求实例（`src/utils/service.ts`）

请求基于 `@sud-web/http` 的 `createAxios` 创建，CSRF/Token 注入与回写走库内默认方案，无需自定义。401 通过 `RefreshManager` 自动刷新，刷新失败回调 `redirectToLogin()` 跳转 SSO。

业务侧只需从 `@/utils/service` 引入 `request`：

```ts
// src/api/recharge.ts
import { request } from "@/utils/service"

/** 交易流水列表 */
export function getPayList(params: IApiTableRequestData) {
  return request<IRequestResult<IResultPayList>>({
    method: "get",
    url: "/api/fund/tx/list",
    params
  })
}
```

接口响应统一类型（见 `types/api.d.ts`）：

- `IApiResponseData<T>` —— `{ code, data, message }` 标准格式
- `IRequestResult<T>` —— `{ code, data }` 精简格式
- `IApiTableRequestData<T>` —— 分页请求参数 `{ pageNo, pageSize } & T`

### 2. 公共组件注册（`src/main.ts`）

`PcTable` / `PcSearch` 在入口注册一次即可全局使用，并引入样式：

```ts
import { PcSearch, PcTable } from "@sud-web/vue-ui"
import "@sud-web/vue-ui/style.css"

app.use(PcTable)
app.use(PcSearch)
```

### 3. 路由

`src/router/index.ts` 区分 **常驻路由** `constantRoutes` 与 **动态路由**（带权限），动态路由必须带 `name`。`src/router/permission.ts` 负责登录态/权限拦截。

路由 `meta` 常用字段：`title`、`svgIcon`、`affix`、`keepAlive`、`hidden`、`Roles`。

---

## 六、写一个列表页（PcSearch + PcTable + useTablePage）

这是本模板最核心的页面范式，参考 `src/views/demo/index.vue`。

`useTablePage` 统一持有 `searchRef` / `tableRef` 并提供 `requestTable` 刷新方法；`PcTable` 负责分页与请求，`PcSearch` 负责搜索表单。

```vue
<script setup lang="ts">
import type { FormItemProp, IColumList } from "@sud-web/vue-ui"
import useTablePage from "@sud-web/vue-hooks/useTablePage"
import { reactive } from "vue"
import { getPayList } from "@/api/recharge"

defineOptions({ name: "DemoList" })

// 统一管理 search / table 引用，requestTable 触发刷新
const { tableRef, searchRef, requestTable } = useTablePage()

// 搜索表单数据
const searchFormData = reactive({ txType: "", status: "" })

// 搜索表单项配置
const searchFormList: FormItemProp[] = [
  { prop: "txType", label: "交易类型", type: "input", width: 200 },
  {
    prop: "status",
    label: "状态",
    type: "select",
    width: 180,
    sources: [
      { label: "成功", value: "SUCCESS" },
      { label: "处理中", value: "READY" },
      { label: "已关闭", value: "CLOSED" }
    ]
  }
]

// 表格列配置
const columList: IColumList = [
  { label: "交易号", prop: "txId", minWidth: 160, align: "left" },
  { label: "时间", prop: "time", minWidth: 160, align: "left" },
  { label: "金额", prop: "amount", minWidth: 140, align: "left" },
  { label: "状态", prop: "status", minWidth: 120, align: "left" }
]

// 分页参数由 PcTable 注入，搜索条件并入
function requestApi(params: any) {
  return getPayList({ ...params, ...searchFormData })
}
</script>

<template>
  <el-card shadow="never">
    <PcSearch
      ref="searchRef"
      v-model="searchFormData"
      :form-list="searchFormList"
      @request="requestTable"
    />
    <PcTable
      ref="tableRef"
      :request-api="requestApi"
      :colum-list="columList"
      :show-action-colu="false"
      type-index
    >
      <!-- 具名插槽自定义列内容 -->
      <template #amount="{ row }">
        <span>¥ {{ row.amount }}</span>
      </template>
    </PcTable>
  </el-card>
</template>
```

**要点：**

- `requestApi(params)` 中 `params` 的分页字段（`pageNo` / `pageSize`）由 `PcTable` 自动注入，业务只负责合并搜索条件。
- 搜索点击查询 → `PcSearch` 触发 `@request` → 调用 `requestTable` → `PcTable` 重新请求。
- 列内容自定义用 **具名插槽**（`#列prop="{ row }"`）。
- `show-action-colu`、`type-index` 等属性详见 `@sud-web/vue-ui` 表格文档。

---

## 七、新增 / 编辑 / 查看弹窗（useDialogOut + useModalForm）

CRUD 弹窗采用一对配套 Hook：

- **列表页**用 `useDialogOut` 管理弹窗显示、当前模式（`add` / `edit` / `check`）、当前行数据与查看态禁用。
- **弹窗子组件**用 `useModalForm` 处理表单初始化、回显、校验、提交 loading 与关闭。

参考 `src/views/demo/index.vue` 与 `src/views/demo/components/DemoModal.vue`。

### 1. 列表页：用 useDialogOut 打开弹窗

```vue
<script setup lang="ts">
import type { IPayItem } from "@/api/recharge/types/recharge"
import useDialogOut from "@sud-web/vue-hooks/useDialogOut"
import { reactive } from "vue"
import DemoModal from "./components/DemoModal.vue"

// reactive 包裹后字段无需 .value（不包裹则返回 Ref）
const dialog = reactive(useDialogOut<IPayItem>())
</script>

<template>
  <!-- 工具栏：新增 -->
  <el-button type="primary" @click="dialog.add()">新增</el-button>

  <PcTable ...>
    <!-- 操作列：编辑 / 查看，传入行拷贝避免污染表格数据 -->
    <template #action="{ row }">
      <el-button link type="primary" @click="dialog.edit({ ...row })">编辑</el-button>
      <el-button link type="info" @click="dialog.check({ ...row })">查看</el-button>
    </template>
  </PcTable>

  <!-- 提交成功后刷新列表 -->
  <DemoModal
    v-model:visible="dialog.visible"
    :mode="dialog.mode"
    :data="dialog.row"
    :disabled="dialog.disabled"
    @submit="requestTable()"
  />
</template>
```

| 方法           | mode    | row      | disabled（查看态） |
| -------------- | ------- | -------- | ------------------ |
| `dialog.add()` | `add`   | 重置 `{}` | `false`            |
| `dialog.edit(row)` | `edit`  | 设为 row | `false`            |
| `dialog.check(row)` | `check` | 设为 row | `true`             |

> `edit` / `check` 保存的是传入对象的引用，传 `{ ...row }` 拷贝可避免污染表格行数据。

### 2. 弹窗子组件：用 useModalForm 处理表单

子组件用 `defineProps<IProp>()` 接收父级透传的 `visible` / `mode` / `data` / `disabled`，`useModalForm` 负责：

- `add` 模式调用 `resetForm` 初始化；`edit` / `check` 模式调用 `setFormData` 回显。
- `title` 随 `mode` 自动切换（新增 / 编辑 / 查看）。
- `submitForm` 先做表单校验，通过后执行 `handleSubmit`；查看态由 `disabled` 控制只读。

```vue
<script setup lang="ts">
import type { IProp } from "@sud-web/vue-hooks/useModalForm"
import type { IPayItem } from "@/api/recharge/types/recharge"
import useModalForm, { emits, propsData } from "@sud-web/vue-hooks/useModalForm"
import { ElMessage } from "element-plus"
import { reactive } from "vue"
import { addPay, editPay } from "@/api/recharge"

const props = withDefaults(defineProps<IProp<Partial<IPayItem>>>(), propsData)
const emit = defineEmits(emits)

const formData = reactive({ txId: "", txType: "", amount: "", status: "READY" })
const rules = {
  txType: [{ required: true, message: "请输入交易类型", trigger: "blur" }],
  amount: [{ required: true, message: "请输入金额", trigger: "blur" }]
}

const { formRef, title, loading, submitForm, handleResSuccess, close } = useModalForm({
  props,
  emit,
  // 新增初始化：先清空校验再设默认值（传了 resetForm，hook 不再自动 resetFields）
  resetForm() {
    formRef.value?.resetFields()
    Object.assign(formData, { txId: "", txType: "", amount: "", status: "READY" })
  },
  // 编辑 / 查看回显
  setFormData() {
    Object.assign(formData, props.data)
  },
  // 校验通过后的提交逻辑
  async handleSubmit() {
    const api = props.mode === "add" ? addPay : editPay
    await api({ ...formData })
    handleResSuccess() // 成功提示 + 关闭弹窗 + emit('submit')
  },
  successTips() {
    ElMessage.success(props.mode === "add" ? "新增成功" : "编辑成功")
  }
})
</script>

<template>
  <el-dialog
    :model-value="visible"
    :title="title"
    width="520px"
    @update:model-value="(val: boolean) => !val && close()"
  >
    <el-form ref="formRef" :model="formData" :rules="rules" :disabled="disabled" label-width="90px">
      <el-form-item v-if="mode !== 'add'" label="交易号" prop="txId">
        <el-input v-model="formData.txId" disabled />
      </el-form-item>
      <el-form-item label="交易类型" prop="txType">
        <el-input v-model="formData.txType" placeholder="请输入交易类型" />
      </el-form-item>
      <el-form-item label="金额" prop="amount">
        <el-input v-model="formData.amount" placeholder="请输入金额" />
      </el-form-item>
    </el-form>

    <!-- 查看模式隐藏底部按钮 -->
    <template v-if="mode !== 'check'" #footer>
      <el-button @click="close">取消</el-button>
      <el-button type="primary" :loading="loading" @click="submitForm">确定</el-button>
    </template>
  </el-dialog>
</template>
```

**要点：**

- `propsData`（默认值）与 `emits`（`['update:visible', 'submit']`）由 `@sud-web/vue-hooks/useModalForm` 一并导出，直接复用。
- 传了 `resetForm` 后 hook 不再自动 `resetFields`，需在其中自行清空校验并设默认值。
- 提交成功务必调用 `handleResSuccess()` 来「提示 + 关闭 + 通知父级刷新」。

> 接口约定见第五节，新增 / 编辑接口（`addPay` / `editPay`）定义在 `src/api/recharge/index.ts`，类型在 `src/api/recharge/types/recharge.ts`。

---

## 八、注意事项

- **禁用 husky**：不同 husky 版本可能导致本地代码被立即删除且无法在回收站找回。
- Element Plus 部分组件需手动引入（如 `import { ElSlider } from "element-plus"`），因旧版 sass 处理对新版兼容不足。
- 升级公共能力时，统一在 sud-web 仓库改 `@sud-web/*` 后发布版本，再于本工程更新依赖版本，避免在业务工程内重复实现。
