vue-vben-admin-doc
如何本地开发
# 克隆本仓库
+$ git clone git@github.com:vbenjs/vue-vben-admin-doc.git
+
+# 或者使用 yarn
+$ yarn install
+
+# 启动开发服务器
+$ yarn dev
+
diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..1ad2fd0e --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +doc.vvbin.cn diff --git a/README.html b/README.html new file mode 100644 index 00000000..1aa07c2d --- /dev/null +++ b/README.html @@ -0,0 +1,39 @@ + + +
+ + +# 克隆本仓库
+$ git clone git@github.com:vbenjs/vue-vben-admin-doc.git
+
+# 或者使用 yarn
+$ yarn install
+
+# 启动开发服务器
+$ yarn dev
+
# 克隆本仓库\n$ git clone git@github.com:vbenjs/vue-vben-admin-doc.git\n\n# 或者使用 yarn\n$ yarn install\n\n# 启动开发服务器\n$ yarn dev\n
用于项目权限的组件,一般用于按钮级等细粒度权限管理
<template>\n <div>\n <Authority :value="RoleEnum.ADMIN">\n <a-button type="primary" block> 只有admin角色可见 </a-button>\n </Authority>\n </div>\n</template>\n<script>\n import { Authority } from '/@/components/Authority';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { Authority },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
value | RoleEnum,RoleEnum[],string,string[] | - | 角色信息或者权限编码。会自动区分权限模式 |
一些比较基础的通用组件使用方式
用于显示标题,可以显示帮助按钮及文本
<template>\n <div>\n <BasicTitle helpMessage="提示1">标题</BasicTitle>\n <BasicTitle :helpMessage="['提示1', '提示2']">标题</BasicTitle>\n </div>\n</template>\n<script>\n import { BasicTitle } from '/@/components/Basic/index';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { BasicTitle },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
helpMessage | string|string[] | - | 标题右侧帮助按钮信息 |
span | boolean | false | 是否显示标题左侧蓝色色块 |
normal | boolean | false | 将文字默认化,不加粗 |
名称 | 说明 |
---|---|
default | 标题文本 |
带动画的箭头组件
<template>\n <div>\n <BasicArrow :expand="false" />\n </div>\n</template>\n<script>\n import { BasicArrow } from '/@/components/Basic/index';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { BasicArrow },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
expand | boolean | false | 箭头展开状态 |
top | boolean | false | 箭头默认向上 |
bottom | boolean | false | 箭头默认向下 |
inset | boolean | false | 取消 padding/margin,用于内嵌 |
帮助按钮组件
<template>\n <div>\n <BasicHelp :text="['提示1', '提示2']" />\n <BasicHelp text="提示" />\n </div>\n</template>\n<script>\n import { BasicHelp } from '/@/components/Basic/index';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { BasicHelp },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
fontSize | string | 14px | - | 字体大小 |
color | string | #fff | - | 颜色 |
text | string|string[] | - | - | 文本列表 |
showIndex | boolean | true | - | 是否显示序号,在 text 为 string[]情况下生效 |
maxWidth | string | 600px | - | 最大宽度 |
placement | string | right | - | 显示方向,参考 Tooltip 组件 |
名称 | 说明 |
---|---|
default | 默认图标 |
用于监听包裹的元素点击外部触发事件
<template>\n <div>\n <ClickOutSide @clickOutside="() => (showRef = false)">\n <div @click="() => (showRef = true)">\n {{ showRef ? '鼠标点击那部(点击边框外可以恢复)' : '点击该区域状态(初始状态)' }}\n </div>\n </ClickOutSide>\n </div>\n</template>\n<script>\n import { defineComponent, ref } from 'vue';\n import { ClickOutSide } from '/@/components/ClickOutSide/';\n export default defineComponent({\n components: { ClickOutSide },\n setup() {\n const showRef = ref(false);\n return {\n showRef,\n };\n },\n });\n</script>\n
事件 | 回调参数 | 说明 |
---|---|---|
clickOutside | ()=>void | 点击包裹元素外部区域触发 |
名称 | 说明 |
---|---|
default | 被包裹的元素 |
代码编辑器
<template>\n <CodeEditor v-model:value="value" :mode="modeValue" />\n</template>\n<script>\n import { defineComponent, ref } from 'vue';\n export default defineComponent({\n components: { CodeEditor },\n setup() {\n const modeValue = ref('application/json');\n return { value, modeValue };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value(v-model:value) | any | - | - | 绑定值 |
mode | string | application/json | 'application/json' ,'htmlmixed' ,'javascript' | 代码类型 |
readonly | boolean | - | - | 是否只读 |
区域折叠卡片容器
<template>\n <div>\n <CollapseContainer> content </CollapseContainer>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { CollapseContainer } from '/@/components/Container/index';\n\n export default defineComponent({\n components: {\n CollapseContainer,\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
title | string | - | - | 标题 |
canExpan | boolean | true | - | 是否可以展开,为true 显示折叠按钮 |
helpMessage | string[],string | - | - | 标题右侧温馨提醒 |
triggerWindowResize | boolean | false | - | 展开收缩的时候是否触发 window.resize |
loading | boolean | false | - | 显示加载骨架屏 |
lazyTime | number | 0 | - | 延迟加载时间 |
名称 | 说明 |
---|---|
title | 自定义标题 |
action | 自定义右侧操作按钮 |
default | 默认区域 |
footer | 自定义底部区域 |
倒计时组件
倒计时按钮组件
<template>\n <CountButton />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { CountButton } from '/@/components/CountDown';\n\n export default defineComponent({\n components: { CountButton },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | any | - | - | 绑定值 |
count | number | 60 | - | 倒计时时间 |
beforeStartFunc | ()=>promise | - | - | 倒计时之前执行的函数,返回 true 才会开始执行 |
倒计时输入框按钮组件
<template>\n <CountdownInput />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { CountdownInput } from '/@/components/CountDown';\n\n export default defineComponent({\n components: { CountdownInput },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | any | - | - | 绑定值 |
size | string | 'default', 'large', 'small' | - | 输入框即按钮大小 |
count | number | 60 | - | 倒计时时间 |
sendCodeApi | ()=>promise | - | - | 倒计时之前执行的函数,返回 true 才会开始执行 |
数字动画组件
该组件对 vue-countTo 进行了重构,改造成适配 vue3 语法的组件。
<template>\n <CountTo prefix="$" :color="'#409EFF'" :startVal="1" :endVal="200000" :duration="8000" />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { CountTo } from '/@/components/CountTo/index';\n\n export default defineComponent({\n components: {\n CountTo,\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
startVal | number | 0 | 起始值 |
endVal | number | 2021 | 结束值 |
duration | number | 1500 | 动画持续时间 |
autoplay | boolean | true | 自动执行 |
prefix | string | - | 前缀 |
suffix | string | - | 后缀 |
separator | string | , | 分隔符 |
color | string | - | 字体颜色 |
useEasing | boolean | true | 是否开启动画 |
transition | string | linear | 动画效果 |
decimals | number | 0 | 保留小数点位数 |
名称 | 回调参数 | 说明 |
---|---|---|
start | ()=>void | 开始执行动画 |
reset | ()=>void | 重置 |
图片裁剪组件
图片裁剪组件
<template>\n <CropperImage ref="refCropper" :src="img" @cropend="handleCropend" style="width: 40vw" />\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { CropperImage } from '/@/components/Cropper';\n import img from '/@/assets/images/header.jpg';\n\n export default defineComponent({\n components: {\n CropperImage,\n },\n setup() {\n const info = ref('');\n const cropperImg = ref('');\n\n function handleCropend({ imgBase64, imgInfo }) {\n info.value = imgInfo;\n cropperImg.value = imgBase64;\n }\n\n return {\n img,\n info,\n cropperImg,\n handleCropend,\n };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
src | string | - | 图片源 |
alt | string | - | 图片 alt |
circled | boolean | false | 圆形裁剪框 |
realTimePreview | boolean | true | 实时触发预览 |
height | string | 360px | 高度 |
crossorigin | string | - | crossorigin |
imageStyle | object | `` | 图片样式 |
options | object | DefaultOptions | corpperjs 配置项 |
{\n aspectRatio: 1,\n zoomable: true,\n zoomOnTouch: true,\n zoomOnWheel: true,\n cropBoxMovable: true,\n cropBoxResizable: true,\n toggleDragModeOnDblclick: true,\n autoCrop: true,\n background: true,\n highlight: true,\n center: true,\n responsive: true,\n restore: true,\n checkCrossOrigin: true,\n checkOrientation: true,\n scalable: true,\n modal: true,\n guides: true,\n movable: true,\n rotatable: true,\n}\n
头像裁剪组件
<template>\n <CropperAvatar :uploadApi="uploadApi" />\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { CropperAvatar } from '/@/components/Cropper';\n import { uploadApi } from '/@/api/sys/upload';\n\n export default defineComponent({\n components: {\n CropperAvatar,\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 | 版本 |
---|---|---|---|---|
width | string,number | 200px | 图片源 | |
uploadApi | ({ file: Blob, name: string }) => Promise<void> | - | 图片上传接口 | |
value | String | - | 当前头像地址(v-model) | 2.5.3 |
showBtn | Boolean | true | 是否显示按钮 | 2.5.3 |
btnText | String | - | 按钮文案 | 2.5.3 |
btnProps | ButtonProps | - | 按钮的其它属性 | 2.5.3 |
名称 | 参数 | 说明 | 版本 |
---|---|---|---|
change | value: String | 当头像上传完成时触发 | 2.5.3 |
名称 | 定义 | 说明 | 版本 |
---|---|---|---|
openModal | ()=>void | 打开上传Modal | 2.5.3 |
closeModal | ()=>void | 关闭上传Modal | 2.5.3 |
对 antv
的 Descriptions 组件进行封装
<template>\n <div class="p-4">\n <Description\n title="基础示例"\n :collapseOptions="{ canExpand: true, helpMessage: 'help me' }"\n :column="3"\n :data="mockData"\n :schema="schema"\n />\n <Description @register="register" class="mt-4" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { Alert } from 'ant-design-vue';\n import { Description, DescItem, useDescription } from '/@/components/Description/index';\n const mockData: any = {\n username: 'test',\n nickName: 'VB',\n age: 123,\n phone: '15695909xxx',\n email: '190848757@qq.com',\n addr: '厦门市思明区',\n sex: '男',\n certy: '3504256199xxxxxxxxx',\n tag: 'orange',\n };\n const schema: DescItem[] = [\n {\n field: 'username',\n label: '用户名',\n },\n {\n field: 'nickName',\n label: '昵称',\n render: (curVal, data) => {\n return `${data.username}-${curVal}`;\n },\n },\n {\n field: 'phone',\n label: '联系电话',\n },\n {\n field: 'email',\n label: '邮箱',\n },\n {\n field: 'addr',\n label: '地址',\n },\n ];\n export default defineComponent({\n components: { Description, Alert },\n setup() {\n const [register] = useDescription({\n title: 'useDescription',\n data: mockData,\n schema: schema,\n });\n return { mockData, schema, register };\n },\n });\n</script>\n
参考以上示例
const [register] = useDescription(Props);\n
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv Description
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
title | string | - | - | 标题 |
size | string | small | - | 大小 |
bordered | boolean | true | - | 是否展示边框 |
column | Number, Object | { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 } | - | 一行的 DescriptionItems 数量 |
useCollapse | boolean | - | - | 是否包裹 CollapseContainer 组件 |
collapseOptions | Object | - | - | CollapseContainer 组件属性 |
schema | DescItem[] | - | - | 详情项配置,见下方 DescItem 配置 |
data | object | - | - | 数据源 |
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
field | string | - | - | 字段名 |
label | string | - | - | 标签名 |
labelMinWidth | number | - | - | label 最小宽度 |
contentMinWidth | number | - | - | content 最小宽度 |
labelStyle | any | - | - | label 样式 |
span | number | - | - | 和并列数量 |
show | (data)=>boolean | - | - | 动态判断当前组件是否显示 |
render | (val: string, data: any)=>VNode,undefined,Element,string,number | - | - | 自定义渲染 content |
对 antv
的 drawer 组件进行封装,扩展拖拽,全屏,自适应高度等功能。
由于 drawer 内部代码一般独立成单独文件,推荐独立成组件来进行开发,所以示例都是以独立的文件来进行说明
独立组件代码,用于写组件内部的内容
<template>\n <BasicDrawer v-bind="$attrs" title="Drawer Title" width="50%"> Drawer Info. </BasicDrawer>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicDrawer } from '/@/components/Drawer';\n export default defineComponent({\n components: { BasicDrawer },\n });\n</script>\n
页面引用弹窗
<template>\n <div>\n <Drawer @register="register" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { Alert } from 'ant-design-vue';\n import { useDrawer } from '/@/components/Drawer';\n import Drawer from './Drawer.vue';\n\n export default defineComponent({\n components: { Drawer },\n setup() {\n const [register, { openDrawer }] = useDrawer();\n return {\n register,\n openDrawer,\n };\n },\n });\n</script>\n
useDrawer 用于操作组件
const [register, { openDrawer, setDrawerProps }] = useDrawer();\n
register
register 用于注册 useDrawer
,如果需要使用 useDrawer
提供的 api,必须将 register
传入组件的 onRegister
。
原理其实很简单,就是 vue 的组件子传父通信,内部通过 emit("register",instance)
实现。
同时,独立出去的组件需要将 attrs
绑定到 Drawer 的上面。
<BasicDrawer v-bind="$attrs"> Drawer Info. </BasicDrawer>\n
openDrawer
用于打开/关闭弹窗
// true/false: 打开关闭弹窗\n// data: 传递到子组件的数据\nopenDrawer(true, data);\n
closeDrawer
用于关闭弹窗
closeDrawer();\n
setDrawerProps
用于更改 drawer 的 props 参数因为 drawer 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供 setDrawerProps 方便更改内部 drawer 的 props
Props 内容可以见下方
setDrawerProps(props);\n
用于独立的 Drawer 内部调用
<template>\n <BasicDrawer v-bind="$attrs" @register="register" title="Drawer Title" width="50%">\n Drawer Info.\n <a-button type="primary" @click="closeDrawer">内部关闭drawer</a-button>\n </BasicDrawer>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';\n export default defineComponent({\n components: { BasicDrawer },\n setup() {\n const [register, { closeDrawer }] = useDrawerInner();\n return { register, closeDrawer };\n },\n });\n</script>\n
useModalInner用于操作独立组件
const [register, { closeModal, setModalProps }] = useModalInner(callback);\n
callback
type: (data:any)=>void
回调函数用于接收 openDrawer 第二个参数传递的值
openDrawer((data: any) => {\n console.log(data);\n});\n
closeDrawer
用于关闭抽屉
closeDrawer();\n
changeOkLoading
用于修改确认按钮的 loading 状态
// true or false\nchangeOkLoading(true);\n
changeLoading
用于修改 modal 的 loading 状态
// true or false\nchangeLoading(true);\n
setDrawerProps
用于更改 drawer 的 props 参数因为 modal 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供setDrawerProps 方便更改内部 drawer 的 props
Props 内容可以见下方
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv drawer
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
isDetail | boolean | false | - | 是否为详情模式 |
loading | boolean | false | - | loading 状态 |
loadingText | string | `` | - | loading 文本 s |
showDetailBack | boolean | true | - | isDetail=true 状态下是否显示返回按钮 |
closeFunc | () => Promise<boolean> | - | - | 自定义关闭函数,返回true 关闭,否则不关闭 |
showFooter | boolean | - | - | 是否显示底部 |
footerHeight | number | 60 | - | 底部区域高度 |
事件 | 回调参数 | 说明 |
---|---|---|
close | (e)=>void | 点击关闭回调 |
visible-change | (visible:boolean)=>void | 弹窗打开关闭时触发 |
ok | (e)=>void | 点击确定回调 |
excel 导入导出操作
项目中使用到的是 XLSX,具体文档可以参考XLSX 文档
<template>\n <ImpExcel @success="loadDataSuccess">\n <a-button class="m-3">导入Excel</a-button>\n </ImpExcel>\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { ImpExcel, ExcelData } from '/@/components/Excel';\n export default defineComponent({\n components: { ImpExcel },\n setup() {\n function loadDataSuccess(excelDataList: ExcelData[]) {\n tableListRef.value = [];\n console.log(excelDataList);\n for (const excelData of excelDataList) {\n const {\n header,\n results,\n meta: { sheetName },\n } = excelData;\n const columns: BasicColumn[] = [];\n for (const title of header) {\n columns.push({ title, dataIndex: title });\n }\n tableListRef.value.push({ title: sheetName, dataSource: results, columns });\n }\n }\n return {\n loadDataSuccess,\n };\n },\n });\n</script>\n
事件 | 回调参数 | 说明 |
---|---|---|
success | (res:ExcelData)=>void | 导入成功回调 |
error | ()=>void | 导出错误 |
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
header: | string[]; | table 表头 | |
results: | T[]; | table 数据 | |
meta: | { sheetName: string }; | table title |
具体详情可以参考完整版示例
import { jsonToSheetXlsx, aoaToSheetXlsx } from '/@/components/Excel';\n
import { aoaToSheetXlsx } from '/@/components/Excel';\n// 保证data顺序与header一致\naoaToSheetXlsx({\n data: [],\n header: [],\n filename: '二维数组方式导出excel.xlsx',\n});\n
import { jsonToSheetXlsx } from '/@/components/Excel';\n\njsonToSheetXlsx({\n data,\n filename,\n write2excelOpts: {\n // 可以是 xlsx/html/csv/txt\n bookType,\n },\n});\n
import { jsonToSheetXlsx } from '/@/components/Excel';\n\njsonToSheetXlsx({\n data,\n filename: '使用key作为默认头部.xlsx',\n});\n\njsonToSheetXlsx({\n data,\n header: {\n id: 'ID',\n name: '姓名',\n age: '年龄',\n no: '编号',\n address: '地址',\n beginTime: '开始时间',\n endTime: '结束时间',\n },\n filename: '自定义头部.xlsx',\n json2sheetOpts: {\n // 指定顺序\n header: ['name', 'id'],\n },\n});\n
方法 | 回调参数 | 返回值 | 说明 |
---|---|---|---|
jsonToSheetXlsx | Function(JsonToSheet) | json 格式数据,导出到 excel | |
aoaToSheetXlsx | Function(AoAToSheet) | 数组格式,导出到 excel |
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
data | T[] | JSON 对象数组 | |
header?: | T ; | 表头未设置则取 JSON 对象的 key 作为 header | |
filename?: | string | excel-list.xlsx | 导出的文件名 |
json2sheetOpts?: | JSON2SheetOpts | 调用 XLSX.utils.json_to_sheet 的可选参数 | |
write2excelOpts?: | WritingOptions | { bookType: 'xlsx' } | 调用 XLSX.writeFile 的可选参数,具体参 XLSX 文档 |
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
data | T[][]; | 二维数组 | |
header?: | T; | 表头 ;未设置则没有表头 | |
filename?: | string; | excel-list.xlsx | 导出的文件名 |
write2excelOpts?: | WritingOptions; | { bookType: 'xlsx' } | 调用 XLSX.writeFile 的可选参数 |
流程图组件,基于 didi/LogicFlow
的简单封装。详细配置请参考文档 FlowChart
<template>\n <FlowChart :data="demoData" />\n</template>\n\n<script lang="ts">\n import { FlowChart } from '/@/components/FlowChart';\n import { PageWrapper } from '/@/components/Page';\n\n import demoData from './dataTurbo.json';\n export default {\n components: { FlowChart, PageWrapper },\n setup() {\n return { demoData };\n },\n };\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
flowOptions | object | - | - | FlowCharts 配置项 |
data | object | - | - | 流程数据 |
toolbar | boolean | true | - | 是否显示工具栏 |
patternItems | [] | - | - | 左侧拖拽列表数据 |
对 antv
的 form 组件进行封装,扩展一些常用的功能
如果文档内没有,可以尝试在在线示例内寻找
下面是一个使用简单表单的示例,只有一个输入框
<template>\n <div class="m-4">\n <BasicForm\n :labelWidth="100"\n :schemas="schemas"\n :actionColOptions="{ span: 24 }"\n @submit="handleSubmit"\n />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicForm, FormSchema } from '/@/components/Form';\n import { CollapseContainer } from '/@/components/Container';\n import { useMessage } from '/@/hooks/web/useMessage';\n const schemas: FormSchema[] = [\n {\n field: 'field',\n component: 'Input',\n label: '字段1',\n colProps: {\n span: 8,\n },\n defaultValue: '1',\n componentProps: {\n placeholder: '自定义placeholder',\n onChange: (e) => {\n console.log(e);\n },\n },\n },\n ];\n\n export default defineComponent({\n components: { BasicForm, CollapseContainer },\n setup() {\n const { createMessage } = useMessage();\n return {\n schemas,\n handleSubmit: (values: any) => {\n createMessage.success('click search,values:' + JSON.stringify(values));\n },\n };\n },\n });\n</script>\n
所有可调用函数见下方 Methods
说明
<template>\n <div class="m-4">\n <BasicForm\n :schemas="schemas"\n ref="formElRef"\n :labelWidth="100"\n @submit="handleSubmit"\n :actionColOptions="{ span: 24 }"\n />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { BasicForm, FormSchema, FormActionType, FormProps } from '/@/components/Form';\n import { CollapseContainer } from '/@/components/Container';\n const schemas: FormSchema[] = [];\n\n export default defineComponent({\n components: { BasicForm, CollapseContainer },\n setup() {\n const formElRef = ref<Nullable<FormActionType>>(null);\n return {\n formElRef,\n schemas,\n setProps(props: FormProps) {\n const formEl = formElRef.value;\n if (!formEl) return;\n formEl.setProps(props);\n },\n };\n },\n });\n</script>\n
form 组件还提供了 useForm
,方便调用函数内部方法
<template>\n <BasicForm @register="register" @submit="handleSubmit" />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';\n import { CollapseContainer } from '/@/components/Container/index';\n import { useMessage } from '/@/hooks/web/useMessage';\n const schemas: FormSchema[] = [\n {\n field: 'field1',\n component: 'Input',\n label: '字段1',\n colProps: {\n span: 8,\n },\n componentProps: {\n placeholder: '自定义placeholder',\n onChange: (e: any) => {\n console.log(e);\n },\n },\n },\n ];\n\n export default defineComponent({\n components: { BasicForm, CollapseContainer },\n setup() {\n const { createMessage } = useMessage();\n const [register, { setProps }] = useForm({\n labelWidth: 120,\n schemas,\n actionColOptions: {\n span: 24,\n },\n });\n return {\n register,\n schemas,\n handleSubmit: (values: any) => {\n createMessage.success('click search,values:' + JSON.stringify(values));\n },\n setProps,\n };\n },\n });\n</script>\n
const [register, methods] = useForm(props);\n
参数 props 内的值可以是 computed 或者 ref 类型
register
register 用于注册 useForm
,如果需要使用 useForm
提供的 api,必须将 register 传入组件的 onRegister
<template>\n <BasicForm @register="register" @submit="handleSubmit" />\n</template>\n<script>\n export default defineComponent({\n components: { BasicForm },\n setup() {\n const [register] = useForm();\n return {\n register,\n };\n },\n });\n</script>\n
Methods
见下方说明
getFieldsValue
类型: () => Recordable;
说明: 获取表单值
setFieldsValue
类型: <T>(values: T) => Promise<void>
说明: 设置表单字段值
resetFields
类型: ()=> Promise<void>
说明: 重置表单值
validateFields
类型: (nameList?: NamePath[]) => Promise<any>
说明: 校验指定表单项
validate
类型: (nameList?: NamePath[]) => Promise<any>
说明: 校验整个表单
submit
类型: () => Promise<void>
说明: 提交表单
scrollToField
类型: (name: NamePath, options?: ScrollOptions) => Promise<void>
说明: 滚动到对应字段位置
clearValidate
类型: (name?: string | string[]) => Promise<void>
说明: 清空校验
setProps
TIP
设置表单的 props 可以直接在标签上传递,也可以使用 setProps,或者初始化直接写 useForm(props)
类型: (formProps: Partial<FormProps>) => Promise<void>
说明: 设置表单 Props
removeSchemaByField
类型: (field: string | string[]) => Promise<void>
说明: 根据 field 删除 Schema
appendSchemaByField
类型: ( schema: FormSchema, prefixField: string | undefined, first?: boolean | undefined ) => Promise<void>
说明: 插入到指定 filed 后面,如果没传指定 field,则插入到最后,当 first = true 时插入到第一个位置
updateSchema
类型: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
说明: 更新表单的 schema, 只更新函数所传的参数
e.g
updateSchema({ field: 'filed', componentProps: { disabled: true } });\nupdateSchema([\n { field: 'filed', componentProps: { disabled: true } },\n { field: 'filed1', componentProps: { disabled: false } },\n]);\n
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv form
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
schemas | Schema[] | - | - | 表单配置,见下方 FormSchema 配置 | |
submitOnReset | boolean | false | - | 重置时是否提交表单 | |
labelCol | Partial<ColEx> | - | - | 整个表单通用 LabelCol 配置 | |
wrapperCol | Partial<ColEx> | - | - | 整个表单通用 wrapperCol 配置 | |
baseColProps | Partial<ColEx> | - | - | 配置所有选子项的 ColProps,不需要逐个配置,子项也可单独配置优先与全局 | |
baseRowStyle | object | - | - | 配置所有 Row 的 style 样式 | |
labelWidth | number , string | - | - | 扩展 form 组件,增加 label 宽度,表单内所有组件适用,可以单独在某个项覆盖或者禁用 | |
labelAlign | string | - | left ,right | label 布局 | |
mergeDynamicData | object | - | - | 额外传递到子组件的参数 values | |
autoFocusFirstItem | boolean | false | - | 是否聚焦第一个输入框,只在第一个表单项为 input 的时候作用 | |
compact | boolean | false | true/false | 紧凑类型表单,减少 margin-bottom | |
size | string | default | 'default' , 'small' , 'large' | 向表单内所有组件传递 size 参数,自定义组件需自行实现 size 接收 | |
disabled | boolean | false | true/false | 向表单内所有组件传递 disabled 属性,自定义组件需自行实现 disabled 接收 | |
autoSetPlaceHolder | boolean | true | true/false | 自动设置表单内组件的 placeholder,自定义组件需自行实现 | |
autoSubmitOnEnter | boolean | false | true/false | 在 input 中输入时按回车自动提交 | 2.4.0 |
rulesMessageJoinLabel | boolean | false | true/false | 如果表单项有校验,会自动生成校验信息,该参数控制是否将字段中文名字拼接到自动生成的信息后方 | |
showAdvancedButton | boolean | false | true/false | 是否显示收起展开按钮 | |
emptySpan | number , Partial<ColEx> | 0 | - | 空白行格,可以是数值或者 col 对象 数 | |
autoAdvancedLine | number | 3 | - | 如果 showAdvancedButton 为 true,超过指定行数行默认折叠 | |
alwaysShowLines | number | 1 | - | 折叠时始终保持显示的行数 | 2.7.1 |
showActionButtonGroup | boolean | true | true/false | 是否显示操作按钮(重置/提交) | |
actionColOptions | Partial<ColEx> | - | - | 操作按钮外层 Col 组件配置,如果开启 showAdvancedButton,则不用设置,具体见下方 actionColOptions | |
showResetButton | boolean | true | - | 是否显示重置按钮 | |
resetButtonOptions | object | - | 重置按钮配置见下方 ActionButtonOption | ||
showSubmitButton | boolean | true | - | 是否显示提交按钮 | |
submitButtonOptions | object | - | 确认按钮配置见下方 ActionButtonOption | ||
resetFunc | () => Promise<void> | - | 重置表单行为前执行自定义重置按钮逻辑() => Promise<void>; | ||
submitFunc | () => Promise<void> | - | 自定义提交按钮逻辑() => Promise<void>; | ||
fieldMapToTime | [string, [string, string], string?][] | 'timestamp' ,'timestampStartDay' ,momentjs 时间格式 | 用于将表单内时间区域的应设成 2 个字段,见下方说明 |
见src/components/Form/src/types/index.ts
export interface ButtonProps extends BasicButtonProps {\n text?: string;\n}\n
将表单内时间区域的值映射成 2 个字段
如果表单内有时间区间组件,获取到的值是一个数组,但是往往我们传递到后台需要是 2 个字段
useForm({\n fieldMapToTime: [\n // data为时间组件在表单内的字段,startTime,endTime为转化后的开始时间与结束时间\n // 'YYYY-MM-DD'为时间格式,参考moment\n ['datetime', ['startTime', 'endTime'], 'YYYY-MM-DD'],\n // 支持多个字段\n ['datetime1', ['startTime1', 'endTime1'], 'YYYY-MM-DD HH:mm:ss'],\n ],\n});\n\n// fieldMapToTime没写的时候表单获取到的值\n{\n datetime: [Date(),Date()]\n}\n// ['datetime', ['startTime', 'endTime'], 'YYYY-MM-DD'],等同于 dayjs(Date()).format('YYYY-MM-DD'). 之后\n{\n startTime: '2020-08-12',\n endTime: '2020-08-15',\n}\n\n// ['datetime', ['startTime', 'endTime'], 'timestamp'],等同于 dayjs(Date()).unix(). 之后\n{\n startTime: 1597190400,\n endTime: 1597449600,\n}\n\n// ['datetime', ['startTime', 'endTime'], 'timestampStartDay'],等同于 dayjs(Date()).startOf('day').unix(). 之后\n{\n startTime: 1597190400,\n endTime: 1597449600,\n}\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
field | string | - | - | 字段名 |
label | string | - | - | 标签名 |
subLabel | string | - | - | 二级标签名灰色 |
suffix | string , number , ((values: RenderCallbackParams) => string / number); | - | - | 组件后面的内容 |
changeEvent | string | - | - | 表单更新事件名称 |
helpMessage | string , string[] | - | - | 标签名右侧温馨提示 |
helpComponentProps | HelpComponentProps | - | - | 标签名右侧温馨提示组件 props,见下方 HelpComponentProps |
labelWidth | string , number | - | - | 覆盖统一设置的 labelWidth |
disabledLabelWidth | boolean | false | true/false | 禁用 form 全局设置的 labelWidth,自己手动设置 labelCol 和 wrapperCol |
component | string | - | - | 组件类型,见下方 ComponentType |
componentProps | any,()=>{} | - | - | 所渲染的组件的 props |
rules | ValidationRule[] | - | - | 校验规则,见下方 ValidationRule |
required | boolean | - | - | 简化 rules 配置,为 true 则转化成 [{required:true}]。2.4.0 之前的版本只支持 string 类型的值 |
rulesMessageJoinLabel | boolean | false | - | 校验信息是否加入 label |
itemProps | any | - | - | 参考下方 FormItem |
colProps | ColEx | - | - | 参考上方 actionColOptions |
defaultValue | object | - | - | 所渲渲染组件的初始值 |
render | (renderCallbackParams: RenderCallbackParams) => VNode / VNode[] / string | - | - | 自定义渲染组件 |
renderColContent | (renderCallbackParams: RenderCallbackParams) => VNode / VNode[] / string | - | - | 自定义渲染组件(需要自行包含 formItem) |
renderComponentContent | (renderCallbackParams: RenderCallbackParams) => any / string | - | - | 自定义渲染组内部的 slot |
slot | string | - | - | 自定义 slot,渲染组件 |
colSlot | string | - | - | 自定义 slot,渲染组件 (需要自行包含 formItem) |
show | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判断当前组件是否显示,css 控制,不会删除 dom |
ifShow | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判断当前组件是否显示,js 控制,会删除 dom |
dynamicDisabled | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判断当前组件是否禁用 |
dynamicRules | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判返当前组件你校验规则 |
RenderCallbackParams
export interface RenderCallbackParams {\n schema: FormSchema;\n values: any;\n model: any;\n field: string;\n}\n
componentProps
当值为对象类型时,该对象将作为component
所对应组件的的 props 传入组件
当值为一个函数时候
参数有 4 个
schema
: 表单的整个 schemas
formActionType
: 操作表单的函数。与 useForm 返回的操作函数一致
formModel
: 表单的双向绑定对象,这个值是响应式的。所以可以方便处理很多操作
tableAction
: 操作表格的函数,与 useTable 返回的操作函数一致。注意该参数只在表格内开启搜索表单的时候有值,其余情况为null
,
{\n // 简单例子,值改变的时候操作表格或者修改表单内其他元素的值\n component:'Input',\n componentProps: ({ schema, tableAction, formActionType, formModel }) => {\n return {\n // xxxx props\n onChange:e=>{\n const {reload}=tableAction\n reload()\n // or\n formModel.xxx='123'\n }\n };\n };\n}\n
HelpComponentProps
export interface HelpComponentProps {\n maxWidth: string;\n // 是否显示序号\n showIndex: boolean;\n // 文本列表\n text: any;\n // 颜色\n color: string;\n // 字体大小\n fontSize: string;\n icon: string;\n absolute: boolean;\n // 定位\n position: any;\n}\n
ComponentType
schema 内组件的可选类型
export type ComponentType =\n | 'Input'\n | 'InputGroup'\n | 'InputPassword'\n | 'InputSearch'\n | 'InputTextArea'\n | 'InputNumber'\n | 'InputCountDown'\n | 'Select'\n | 'ApiSelect'\n | 'TreeSelect'\n | 'RadioButtonGroup'\n | 'RadioGroup'\n | 'Checkbox'\n | 'CheckboxGroup'\n | 'AutoComplete'\n | 'Cascader'\n | 'DatePicker'\n | 'MonthPicker'\n | 'RangePicker'\n | 'WeekPicker'\n | 'TimePicker'\n | 'Switch'\n | 'StrengthMeter'\n | 'Upload'\n | 'IconPicker'\n | 'Render'\n | 'Slider'\n | 'Rate'\n | 'Divider'; // v2.7.2新增\n
Divider
类型用于在schemas
中占位,将会渲染成一个分割线(始终占一整行的版面),可以用于较长表单的版面分隔。请只将 Divider 类型的 schema 当作一个分割线,而不是一个常规的表单字段。
Divider
仅在showAdvancedButton
为 false 时才会显示(也就是说如果启用了表单收起和展开功能,Divider
将不会显示)Divider
使用schema
中的label
以及helpMessage
来渲染分割线中的提示内容Divider
可以使用componentProps
来设置除type
之外的 propsDivider
不会渲染AFormItem
,因此schema
中除label
、componentProps
、helpMessage
、helpComponentProps
以外的属性不会被用到在 src/components/Form/src/componentMap.ts
内,添加需要的组件,并在上方 ComponentType 添加相应的类型 key
这种写法适用与适用频率较高的组件
componentMap.set('componentName', 组件);\n\n// ComponentType\nexport type ComponentType = xxxx | 'componentName';\n
使用 useComponentRegister 进行注册
这种写法只能在当前页使用,页面销毁之后会从 componentMap 删除相应的组件
import { useComponentRegister } from '@/components/form/index';\n\nimport { StrengthMeter } from '@/components/strength-meter/index';\n\nuseComponentRegister('StrengthMeter', StrengthMeter);\n
提示
方式 2 出现的原因是为了减少打包体积,如果某个组件体积很大,用方式 1 的话可能会使首屏体积增加
自定义渲染内容
<template>\n <div class="m-4">\n <BasicForm @register="register" @submit="handleSubmit" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, h } from 'vue';\n import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';\n import { useMessage } from '/@/hooks/web/useMessage';\n import { Input } from 'ant-design-vue';\n const schemas: FormSchema[] = [\n {\n field: 'field1',\n component: 'Input',\n label: '字段1',\n colProps: {\n span: 8,\n },\n rules: [{ required: true }],\n render: ({ model, field }) => {\n return h(Input, {\n placeholder: '请输入',\n value: model[field],\n onChange: (e: ChangeEvent) => {\n model[field] = e.target.value;\n },\n });\n },\n },\n {\n field: 'field2',\n component: 'Input',\n label: '字段2',\n colProps: {\n span: 8,\n },\n rules: [{ required: true }],\n renderComponentContent: () => {\n return {\n suffix: () => 'suffix',\n };\n },\n },\n ];\n export default defineComponent({\n components: { BasicForm },\n setup() {\n const { createMessage } = useMessage();\n const [register, { setProps }] = useForm({\n labelWidth: 120,\n schemas,\n actionColOptions: {\n span: 24,\n },\n });\n return {\n register,\n schemas,\n handleSubmit: (values: any) => {\n createMessage.success('click search,values:' + JSON.stringify(values));\n },\n setProps,\n };\n },\n });\n</script>\n
自定义渲染内容
提示
使用插槽自定义表单域时,请注意 antdv 有关 FormItem 的相关说明。
<template>\n <div class="m-4">\n <BasicForm @register="register">\n <template #customSlot="{ model, field }">\n <a-input v-model:value="model[field]" />\n </template>\n </BasicForm>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'compatible-vue';\n import { BasicForm, useForm } from '@/components/Form/index';\n import { BasicModal } from '@/components/modal/index';\n export default defineComponent({\n name: 'FormDemo',\n setup(props) {\n const [register] = useForm({\n labelWidth: 100,\n actionColOptions: {\n span: 24,\n },\n schemas: [\n {\n field: 'field1',\n label: '字段1',\n slot: 'customSlot',\n },\n ],\n });\n return {\n register,\n };\n },\n });\n</script>\n
自定义显示/禁用
<template>\n <div class="m-4">\n <BasicForm @register="register" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';\n const schemas: FormSchema[] = [\n {\n field: 'field1',\n component: 'Input',\n label: '字段1',\n colProps: {\n span: 8,\n },\n show: ({ values }) => {\n return !!values.field5;\n },\n },\n {\n field: 'field2',\n component: 'Input',\n label: '字段2',\n colProps: {\n span: 8,\n },\n ifShow: ({ values }) => {\n return !!values.field6;\n },\n },\n {\n field: 'field3',\n component: 'DatePicker',\n label: '字段3',\n colProps: {\n span: 8,\n },\n dynamicDisabled: ({ values }) => {\n return !!values.field7;\n },\n },\n ];\n\n export default defineComponent({\n components: { BasicForm },\n setup() {\n const [register, { setProps }] = useForm({\n labelWidth: 120,\n schemas,\n actionColOptions: {\n span: 24,\n },\n });\n return {\n register,\n schemas,\n setProps,\n };\n },\n });\n</script>\n
名称 | 说明 |
---|---|
formFooter | 表单底部区域 |
formHeader | 表单顶部区域 |
resetBefore | 重置按钮前 |
submitBefore | 提交按钮前 |
advanceBefore | 展开按钮前 |
advanceAfter | 展开按钮后 |
远程下拉加载组件,该组件可以用于学习参考如何自定义组件集成到 Form 组件内,将自定义组件交由 Form 去管理
const schemas: FormSchema[] = [\n {\n field: 'field',\n component: 'ApiSelect',\n label: '字段',\n },\n];\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
numberToString | boolean | false | 是否将number 值转化为string |
api | ()=>Promise<{ label: string; value: string; disabled?: boolean }[]> | - | 数据接口,接受一个 Promise 对象 |
params | object | - | 接口参数。此属性改变时会自动重新加载接口数据 |
resultField | string | - | 接口返回的字段,如果接口返回数组,可以不填。支持x.x.x 格式 |
labelField | string | label | 下拉数组项内label 显示文本的字段,支持x.x.x 格式 |
valueField | string | value | 下拉数组项内value 实际值的字段,支持x.x.x 格式 |
immediate | boolean | true | 是否立即请求接口,否则将在第一次点击时候触发请求 |
远程下拉树加载组件,和ApiSelect
类似,2.6.1 以上版本
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
api | ()=>Promise<{ label: string; value: string; children?: any[] }[]> | - | 数据接口,接受一个 Promise 对象 |
params | object | - | 接口参数。此属性改变时会自动重新加载接口数据 |
resultField | string | - | 接口返回的字段,如果接口返回数组,可以不填。支持x.x.x 格式 |
immediate | boolean | true | 是否立即请求接口。 |
Radio Button 风格的选择按钮
const schemas: FormSchema[] = [\n {\n field: 'field',\n component: 'RadioButtonGroup',\n label: '字段',\n },\n];\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
options | { label: string; value: string; disabled?: boolean }[] | - | 数据字段 |
函数式创建右键菜单组件, 只要能拿到 dom 的 event
对象就能为其创建右键菜单。
<template>\n <div>\n <a-button type="primary" @contextmenu="handleContext">Right Click on me</a-button>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { useContextMenu } from '/@/hooks/web/useContextMenu';\n import { CollapseContainer } from '/@/components/Container';\n import { useMessage } from '/@/hooks/web/useMessage';\n export default defineComponent({\n components: { CollapseContainer },\n setup() {\n const [createContextMenu] = useContextMenu();\n const { createMessage } = useMessage();\n function handleContext(e: MouseEvent) {\n createContextMenu({\n event: e,\n items: [\n {\n label: 'New',\n icon: 'ant-design:plus-outlined',\n handler: () => {\n createMessage.success('click new');\n },\n },\n {\n label: 'Open',\n icon: 'ant-design:folder-open-filled',\n handler: () => {\n createMessage.success('click open');\n },\n },\n ],\n });\n }\n return { handleContext };\n },\n });\n</script>\n
Options
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
event | Event | - | - | 需要创建的 dom 的 Event 对象 |
items | ContextMenuItem[] | - | - | 右键菜单列表,ContextMenuItem 见下方说明 |
ContextMenuItem
属性 | 类型 | 说明 |
---|---|---|
label | string | 文本 |
icon | string | 图标,参考图标组件 |
disabled | boolean | 是否禁用 |
handler | ()=>void | 点击触发函数 |
<template>\n <div class="p-5" ref="wrapEl" v-loading="loadingRef" loading-tip="加载中...">\n <a-alert message="函数方式" />\n\n <a-button class="my-4 mr-4" type="primary" @click="openFnFullLoading">全屏 Loading</a-button>\n <a-button class="my-4" type="primary" @click="openFnWrapLoading">容器内 Loading</a-button>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, reactive, toRefs, ref } from 'vue';\n import { Loading, useLoading } from '/@/components/Loading';\n export default defineComponent({\n components: { Loading },\n setup() {\n const [openFullLoading, closeFullLoading] = useLoading({\n tip: '加载中...',\n });\n\n const [openWrapLoading, closeWrapLoading] = useLoading({\n target: wrapEl,\n props: {\n tip: '加载中...',\n absolute: true,\n },\n });\n\n function openFnFullLoading() {\n openFullLoading();\n\n setTimeout(() => {\n closeFullLoading();\n }, 2000);\n }\n\n function openFnWrapLoading() {\n openWrapLoading();\n\n setTimeout(() => {\n closeWrapLoading();\n }, 2000);\n }\n\n return {\n openFnFullLoading,\n openFnWrapLoading,\n ...toRefs(compState),\n };\n },\n });\n</script>\n
使用
import { useLoading } from '/@/components/Loading';\n\nconst [open, close, setTip] = useLoading(opt: Partial<LoadingProps> | Partial<UseLoadingOptions>);\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
target | HTMLElement or Ref<HTMLElement> | - | - | 挂载的 dom 节点 |
props | LoadingProps | - | - | loading 组件参数 |
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
tip | string | - | - | 加载文本 |
size | default, small , large | default | - | 大小 |
absolute | boolean | false | - | 绝对定位,为 false 时可以全屏 |
loading | boolean | - | - | 当前加载状态 |
background | string | - | - | 背景色, |
theme | 'dark' or 'light' | light | - | 背景色主题 ,当背景色不为空时使用背景色 |
open
打开 loading
close
关闭 loading
setTip
设置加在提示文案(v2.6.2以上版本)
',17);p.render=function(s,t,p,e,c,u){return n(),a("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/components_functional_loading.md.8cd1f253.lean.js b/assets/components_functional_loading.md.8cd1f253.lean.js new file mode 100644 index 00000000..e3ec1af6 --- /dev/null +++ b/assets/components_functional_loading.md.8cd1f253.lean.js @@ -0,0 +1 @@ +import{o as n,c as a,a as s}from"./app.8cddb23b.js";const t='{"title":"Loading","description":"","frontmatter":{},"headers":[{"level":2,"title":"Usage","slug":"usage"},{"level":2,"title":"useLoading","slug":"useloading"},{"level":3,"title":"UseLoadingOptions","slug":"useloadingoptions"},{"level":3,"title":"LoadingProps","slug":"loadingprops"},{"level":3,"title":"返回值","slug":"返回值"}],"relativePath":"components/functional/loading.md","lastUpdated":1697592578589}',p={},o=s('',17);p.render=function(s,t,p,e,c,u){return n(),a("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/components_functional_preview.md.6a63f787.js b/assets/components_functional_preview.md.6a63f787.js new file mode 100644 index 00000000..2cce634d --- /dev/null +++ b/assets/components_functional_preview.md.6a63f787.js @@ -0,0 +1 @@ +import{o as n,c as a,a as s}from"./app.8cddb23b.js";const t='{"title":"Preview","description":"","frontmatter":{},"headers":[{"level":2,"title":"Usage","slug":"usage"},{"level":2,"title":"createImgPreview","slug":"createimgpreview"},{"level":3,"title":"参数/Options","slug":"参数-options"},{"level":3,"title":"返回值/PreviewActions","slug":"返回值-previewactions"}],"relativePath":"components/functional/preview.md","lastUpdated":1697592578589}',p={},o=s('将图片预览组件组件函数化。通过函数方便创建组件
<template>\n <div class="p-4">\n <Alert message="有预览图" type="info" />\n <div class="flex justify-center mt-4">\n <img :src="img" v-for="img in imgList" :key="img" class="mr-2" @click="handleClick(img)" />\n </div>\n <Alert message="无预览图" type="info" />\n <a-button @click="handlePreview" type="primary" class="mt-4">预览图片</a-button>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { Alert } from 'ant-design-vue';\n import { createImgPreview } from '/@/components/Preview/index';\n const imgList: string[] = [\n 'https://picsum.photos/id/66/346/216',\n 'https://picsum.photos/id/67/346/216',\n 'https://picsum.photos/id/68/346/216',\n ];\n export default defineComponent({\n components: { Alert },\n setup() {\n function handleClick(img: string) {\n createImgPreview({ imageList: [img] });\n }\n\n function handlePreview() {\n createImgPreview({ imageList: imgList });\n }\n return { imgList, handleClick, handlePreview };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
imgList | string[] | - | - | 图片列表 |
index | number | 0 | - | 初始预览时的图片索引 |
scaleStep | number | - | - | 缩放步进值(每次缩放的幅度)。默认为自动(当前缩放值的10%) |
defaultWidth | number | - | - | 默认宽度(单位px)。当提供此值时,所有图片初始时都会被缩放至此宽度 |
maskClosable | boolean | false | true/false | 点击遮罩时是否自动关闭预览 |
rememberState | boolean | false | true/false | 是否记住每张图片各自的缩放状态 |
onImgLoad | ({ index: number, url: string, dom: HTMLImageElement }) => void | - | - | 图片加载成功时的回调函数 |
onImgError | ({ index: number, url: string, dom: HTMLImageElement }) => void | - | - | 图片加载失败时的回调函数 |
可用于控制当前预览状态
interface PreviewActions {\n // 重置状态\n resume: () => void;\n // 关闭预览\n close: () => void;\n // 显示前一张\n prev: () => void;\n // 显示后一张\n next: () => void;\n // 设置缩放比例(针对当前图片)\n setScale: (scale: number) => void;\n // 设置旋转角度(针对当前图片)\n setRotate: (rotate: number) => void;\n}\n
二次封装按钮组件,且使用相同的组件名替换全局的 a-button
组件
TIP
a-button
标签即可<template>\n <a-button color="success">成功按钮</a-button>\n <a-button color="error">错误按钮</a-button>\n <a-button color="warning">警告按钮</a-button>\n</template>\n
提示
保持 ant design button 组件 原有功能的情况下扩展以下属性
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
color | 'error','warning', 'success' | - | 按钮的颜色场景状态颜色, |
preIcon | string | - | 按钮文本前图标,参考 Icon 组件 |
postIcon | string | - | 按钮文本后图标,参考 Icon 组件 |
iconSize | number | 14 | 按钮图标大小 |
用于项目内组件的展示,基本支持所有图标库(支持按需加载,只打包所用到的图标)
icon 组件位于 src/components/Icon 内
<template>\n <Icon icon="gg:loadbar-doc"></Icon>\n</template>\n\n<script>\n import { defineComponent } from 'vue';\n import { Icon } from '/@/components/Icon';\n export default defineComponent({\n components: { Icon },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
icon | string | - | 图标名 |
color | string | - | 图标颜色 |
size | number | 16 | 图标大小 |
prefix | string | - | 图标前缀 |
提示
如果 icon
值以 |svg
结尾,则会渲染成 SvgIcon 组件
用于使用项目 svg 雪碧图
<template>\n <div>\n <SvgIcon name="test"> </SvgIcon>\n </div>\n</template>\n<script>\n import { SvgIcon } from '/@/components/Icon';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { SvgIcon },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
name | string | - | svg 图标名 |
size | number | 16 | 图标大小 |
本组件详细说明请参阅图标选择器
<template>\n <div>\n <IconPicker />\n </div>\n</template>\n<script>\n import { IconPicker } from '/@/components/Icon';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { IconPicker },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
width | string | 100% | 宽度 |
pageSize | number | 140 | 每页显示的图标数 |
copy | boolean | false | 是否可以复制 |
mode | string | iconify | 备选图标池,为 svg 时,会读取所有 svg sprite 图标。详见下方说明 |
mode 说明
mode
为iconify
时,会使用预生成的图标集数据作为备选图标池mode
为svg
时,会使用 /src/assets/icons
下的所有svg图标(可包含一级子目录)作为备选图标池,详见vite-plugin-svg-icons。注意事项
组件的 defaultXXX
属性不要使用,ant-design-vue 2.2
版本之后将会逐步移除。二次封装的组件也不兼容 defaultXXX
属性。
该项目的组件大部分没有进行全局注册。采用了按需引入注册方式,如下
<template>\n <ConfigProvider>\n <router-view />\n </ConfigProvider>\n</template>\n\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { ConfigProvider } from 'ant-design-vue';\n export default defineComponent({\n name: 'App',\n components: { ConfigProvider },\n });\n</script>\n
json 数据预览组件
<template>\n <JsonPreview :data="data" />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { JsonPreview } from '/@/components/CodeEditor';\n\n export default defineComponent({\n components: { JsonPreview },\n setup() {\n return {\n data: {},\n };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
data | object | - | - | 需要预览的 Json 数据 |
延时加载/懒加载组件, 只在组件可见或者延迟一段时间才进行加载
<template>\n <div class="p-4 lazy-base-demo">\n <div class="lazy-base-demo-wrap">\n <h1>向下滚动</h1>\n <LazyContainer @init="() => {}">\n <TargetContent />\n <template #skeleton>\n <Skeleton :rows="10" />\n </template>\n </LazyContainer>\n </div>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { Skeleton } from 'ant-design-vue';\n import TargetContent from './TargetContent.vue';\n import { LazyContainer } from '/@/components/Container/index';\n export default defineComponent({\n components: { LazyContainer, TargetContent, Skeleton },\n });\n</script>\n<style lang="less" scoped>\n .lazy-base-demo {\n &-wrap {\n display: flex;\n width: 50%;\n height: 2000px;\n margin: 20px auto;\n text-align: center;\n background: #fff;\n justify-content: center;\n flex-direction: column;\n align-items: center;\n }\n\n h1 {\n height: 1300px;\n margin: 20px 0;\n }\n }\n</style>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
timeout | number | - | - | 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 |
viewport | HTMLElement | - | - | 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 |
threshold | string | 0px | - | 预加载阈值, css 单位 |
direction | 'vertical', 'horizontal' , vertical | - | 视口的滚动方向, vertical 代表垂直方向,horizontal 代表水平方向 | |
tag | string' | div | - | 包裹组件的外层容器的标签名 |
transitionName | string' | lazy-container | - | transition 动画 name |
maxWaitingTime | number' | 80 | - | 最大等待时间 |
事件 | 回调参数 | 说明 |
---|---|---|
init | ()=>void | 初始化之后 |
名称 | 说明 |
---|---|
default | 默认区域 |
skeleton | 懒加载骨架屏 |
<template>\n <div class="p-5" ref="wrapEl" v-loading="loadingRef" loading-tip="加载中...">\n <a-button class="my-4 mr-4" type="primary" @click="openCompFullLoading">全屏 Loading</a-button>\n <a-button class="my-4" type="primary" @click="openCompAbsolute">容器内 Loading</a-button>\n <Loading :loading="loading" :absolute="absolute" :tip="tip" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, reactive, toRefs, ref } from 'vue';\n import { Loading } from '/@/components/Loading';\n export default defineComponent({\n components: { Loading },\n setup() {\n const compState = reactive({\n absolute: false,\n loading: false,\n tip: '加载中...',\n });\n\n function openLoading(absolute: boolean) {\n compState.absolute = absolute;\n compState.loading = true;\n setTimeout(() => {\n compState.loading = false;\n }, 2000);\n }\n\n function openCompFullLoading() {\n openLoading(false);\n }\n\n function openCompAbsolute() {\n openLoading(true);\n }\n\n return {\n openCompFullLoading,\n openCompAbsolute,\n ...toRefs(compState),\n };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
tip | string | - | - | 加载文本 |
size | default, small , large | default | - | 大小 |
absolute | boolean | false | - | 绝对定位,为 false 时可以全屏 |
loading | boolean | - | - | 当前加载状态 |
background | string | - | - | 背景色 |
theme | 'dark' or 'light' | light | - | 背景色主题,当背景色不为空时使用背景色 |
基于 Vditor 的 MarkDown 编辑器
<template>\n <div class="p-4">\n <a-button @click="toggleTheme" class="mb-2" type="primary">黑暗主题</a-button>\n <MarkDown v-model:value="value" ref="markDownRef" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, ref, unref } from 'vue';\n import { MarkDown, MarkDownActionType } from '/@/components/Markdown';\n export default defineComponent({\n components: { MarkDown },\n setup() {\n const markDownRef = ref<Nullable<MarkDownActionType>>(null);\n const valueRef = ref(`\n# title\n\n# content\n`);\n\n function toggleTheme() {\n const markDown = unref(markDownRef);\n if (!markDown) return;\n const vditor = markDown.getVditor();\n vditor.setTheme('dark');\n }\n return {\n value: valueRef,\n toggleTheme,\n markDownRef,\n };\n },\n });\n</script>\n
TIP
除以下两个外,props 还可以传入 vidtor 的所有属性。可用 v-bind 统一绑定
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
v-model | string | - | - | 双向绑定文本值 |
height | number | - | - | 高度 |
名称 | 回调参数 | 说明 |
---|---|---|
getVditor | Function | 获取 vditor 实例 |
对 antv 的 modal 组件进行封装,扩展拖拽,全屏,自适应高度等功能
代码路径 src/components/Modal
由于弹窗内代码一般作为单文件组件存在,也推荐这样做,所以示例都为单文件组件形式
TIP
注意 v-bind="$attrs"
记得写,用于将弹窗组件的 attribute
传入 BasicModal
组件
// Modal.vue\n<template>\n <BasicModal v-bind="$attrs" title="Modal Title" :helpMessage="['提示1', '提示2']">\n Modal Info.\n </BasicModal>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicModal } from '/@/components/Modal';\n export default defineComponent({\n components: { BasicModal },\n setup() {\n return {};\n },\n });\n</script>\n
页面引用弹窗
// Page.vue\n<template>\n <div class="px-10">\n <Modal @register="register" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { useModal } from '/@/components/Modal';\n import Modal from './Modal.vue';\n export default defineComponent({\n components: { Modal },\n setup() {\n const [register, { openModal }] = useModal();\n return {\n register,\n openModal,\n };\n },\n });\n</script>\n
用于外部组件调用
useModal 用于操作组件
const [register, { openModal, setModalProps }] = useModal();\n
register
register 用于注册 useModal
,如果需要使用 useModal
提供的 api,必须将 register
传入组件的 onRegister
。
原理其实很简单,就是 vue 的组件子传父通信,内部通过 emit("register",instance)
实现。
同时独立出去的组件需要将 attrs
绑定到 BasicModal
上面。
<template>\n <BasicModal v-bind="$attrs"></BasicModal>\n</template>\n
openModal
用于打开/关闭弹窗
// true/false: 打开关闭弹窗\n// data: 传递到子组件的数据\nopenModal(true, data);\n
closeModal
用于关闭弹窗
closeModal();\n
setModalProps
用于更改 modal 的 props 参数因为 modal 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供 setModalProps 方便更改内部 modal 的 props
Props 内容可以见下方
setModalProps(props);\n
用于独立的 Modal 内部调用
<template>\n <BasicModal\n v-bind="$attrs"\n @register="register"\n title="Modal Title"\n :helpMessage="['提示1', '提示2']"\n >\n <a-button type="primary" @click="closeModal" class="mr-2">从内部关闭弹窗</a-button>\n\n <a-button type="primary" @click="setModalProps">从内部修改title</a-button>\n </BasicModal>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicModal, useModalInner } from '/@/components/Modal';\n export default defineComponent({\n components: { BasicModal },\n setup() {\n const [register, { closeModal, setModalProps }] = useModalInner();\n return {\n register,\n closeModal,\n setModalProps: () => {\n setModalProps({ title: 'Modal New Title' });\n },\n };\n },\n });\n</script>\n
useModalInner用于操作独立组件
const [register, { closeModal, setModalProps }] = useModalInner(callback);\n
callback
type: (data:any)=>void
回调函数用于接收 openModal 第二个参数传递的值
useModal((data: any) => {\n console.log(data);\n});\n
closeModal
用于关闭弹窗
closeModal();\n
changeOkLoading
用于修改确认按钮的 loading 状态
changeOkLoading(true);\n
changeLoading
用于修改 modal 的 loading 状态
// true or false\nchangeLoading(true);\n
setModalProps
用于更改 modal 的 props 参数因为 modal 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供 setModalProps 方便更改内部 modal 的 props
Props 内容可以见下方
TIP
除以下参数外,组件库文档内的 props 也都支持,具体可以参考 antv modal
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
title | string | - | - | modal 标题 |
height | number | - | - | 固定 modal 的高度 |
minHeight | number | - | - | 设置 modal 的最小高度 |
draggable | boolean | true | true/false | 是否开启拖拽 |
useWrapper | boolean | true | true/false | 是否开启自适应高度,开启后会跟随屏幕变化自适应内容,并出现滚动条 |
wrapperFooterOffset | number | 0 | - | 开启是适应高度后,如果超过屏幕高度,底部和顶部会保持一样的间距,该参数可以用来缩小底部的间距 |
canFullscreen | boolean | true | true/false | 是否可以进行全屏 |
defaultFullscreen | boolean | false | true/false | 默认全屏 |
loading | boolean | false | true/false | loading 状态 |
loadingTip | string | - | - | loading 文本 |
showCancelBtn | boolean | true | true/false | 显示关闭按钮 |
showOkBtn | boolean | true | true/false | 显示确认按钮 |
helpMessage | string , string[] | - | - | 标题右侧提示文本 |
centered | boolean | false | true/false | 是否居中弹窗 |
cancelText | string | '关闭' | - | 关闭按钮文本 |
okText | string | '保存' | - | 确认按钮文本 |
closeFunc | () => Promise<boolean> | 关闭函数 | - | 关闭前执行,返回 true 则关闭,否则不关闭 |
事件 | 回调参数 | 说明 |
---|---|---|
ok | function(e) | 点击确定回调 |
cancel | function(e) | 点击取消回调 |
visible-change | (visible:boolean)=>{} | 打开或者关闭触发 |
名称 | 说明 |
---|---|
default | 默认区域 |
footer | 底部区域(会替换掉默认的按钮) |
insertFooter | 关闭按钮的左边(不使用footer插槽时有效) |
centerFooter | 关闭按钮和确认按钮的中间(不使用footer插槽时有效) |
appendFooter | 确认按钮的右边(不使用footer插槽时有效) |
页面相关组件
用于包裹页面组件
<template>\n <div>\n <PageWrapper>\n <template #left>left</template>\n <template #right>right</template>\n </PageWrapper>\n </div>\n</template>\n<script>\n import { PageWrapper } from '/@/components/Page';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { PageWrapper },\n setup() {\n return {};\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
title | string | - | pageHeader title |
dense | 是否缩小主体区域 | false | 为 true 将会取消 padding/margin |
content | string | - | pageHeader Content 内容 |
contentStyle | object | - | 主体区域样式 |
contentClass | string | - | 主体区域 class |
contentBackground | boolean | - | 主体区域背景 |
contentFullHeight | boolean | false | 主体区域是否占满整个屏幕高度 |
fixedHeight | boolean | false | 固定主体区域高度 |
pageHeader 的 slot 都支持
名称 | 说明 |
---|---|
leftFooter | PageFooter 左侧区域 |
rightFooter | PageFooter 右侧区域 |
headerContent | pageHeader 主体内容 |
default | 主体区域 |
用于页面底部工具栏
<template>\n <div>\n <PageFooter>\n <template #left>left</template>\n <template #right>right</template>\n </PageFooter>\n </div>\n</template>\n<script>\n import { PageFooter } from '/@/components/Page';\n import { defineComponent } from 'vue';\n export default defineComponent({\n components: { PageFooter },\n setup() {\n return {};\n },\n });\n</script>\n
名称 | 说明 |
---|---|
left | 左侧区域 |
right | 右侧区域 |
带有 PopConfirm 下拉菜单功能的按钮
<template>\n <PopConfirmButton>按钮文本</PopConfirmButton>\n</template>\n\n<script>\n import { defineComponent } from 'vue';\n import { PopConfirmButton } from '/@/components/Button';\n export default defineComponent({\n components: { PopConfirmButton },\n });\n</script>\n
提示
保持 anv design popconfirm 组件 原有功能的情况下扩展以下属性
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
enable | boolean | true | 是否启用下拉菜单,为 false 则显示默认按钮 |
用于生成二维码的组件
<template>\n <QrCode :value="qrCodeUrl" />\n</template>\n<script lang="ts">\n import { defineComponent, ref, unref } from 'vue';\n import { QrCode, QrCodeActionType } from '/@/components/Qrcode/index';\n import LogoImg from '/@/assets/images/logo.png';\n const qrCodeUrl = 'https://www.vvbin.cn';\n export default defineComponent({\n components: { QrCode },\n setup() {\n const qrRef = ref<Nullable<QrCodeActionType>>(null);\n function download() {\n const qrEl = unref(qrRef);\n if (!qrEl) return;\n qrEl.download('文件名');\n }\n return {\n qrCodeUrl,\n LogoImg,\n download,\n qrRef,\n };\n },\n });\n</script>\n<style scoped>\n .qrcode-demo-item {\n width: 30%;\n margin-right: 1%;\n }\n</style>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string | - | - | 二维码地址 |
options | QRCodeRenderersOptions | - | - | 二维码配置 ,见 QRCodeRenderersOptions |
width | number | 2 | - | 宽度 |
logo | string|LogoType | - | - | 中间 logo 配置,见 LogoType |
tag | 渲染标签 | canvas | canvas | img | img 不支持内嵌 logo |
QRCodeRenderersOptions
/**\n * 定义margin的宽度。.\n * Default: 4\n */\nmargin?: number;\n/**\n * 比例因子。值1表示每个模块1像素(黑点)。\n * Default: 4\n */\nscale?: number;\n/**\n * 为输出图像强制指定宽度。\n * 如果宽度太小而不能包含qr符号,则此选项将被忽略。\n * 优先于规模。\n */\nwidth?: number;\ncolor?: {\n /**\n * 暗模块的颜色。值必须为十六进制格式(RGBA).\n * 注意:深色应始终比color.light暗。.\n * Default: #000000ff\n */\n dark?: string;\n /**\n * 照明模块的颜色。值必须为十六进制格式(RGBA).\n * Default: #ffffffff\n */\n light?: string;\n};\n\n
LogoType
{\n // logo图片\n src: string;\n // logo大小\n logoSize: number;\n // 背景颜色\n bgColor: string;\n // logo圆角\n logoRadius: number;\n}\n
名称 | 回调参数 | 说明 |
---|---|---|
download | Function(fileName:string) | 下载 |
名称 | 回调参数 | 说明 |
---|---|---|
done | (data: QrcodeDoneEventParams)=>void | 绘制完成 |
error | (error)=>void | 生成二维码时发生错误 |
QrcodeDoneEventParams
{\n url: string; // 二维码DataURL数据\n ctx?: CanvasRenderingContext2D; // 该对象为画布的2D渲染上下文,仅在tag为canvas时有效,可用于自定义绘制\n}\n
done
事件回调中可以对二维码进行自定义的绘制,示例代码如下:
<QrCode\n :value="qrCodeUrl"\n :width="200"\n @done="onQrcodeDone"\n/>\n
function onQrcodeDone({ ctx }) {\n if (ctx instanceof CanvasRenderingContext2D) {\n // 额外绘制\n ctx.fillStyle = 'black';\n ctx.font = '16px "微软雅黑"';\n ctx.textBaseline = 'bottom';\n ctx.textAlign = 'center';\n ctx.fillText('你帅你先扫', 100, 195, 200);\n }\n}\n
有关 CanvasRenderingContext2D
的更多资料以及绘制方法,请参考MDN
参考 element-ui
的 el-scrollbar 组件实现
滚动容器组件
<template>\n <div class="p-4">\n <div class="my-4">\n <a-button @click="scrollTo(100)">滚动到100px位置</a-button>\n <a-button @click="scrollTo(800)">滚动到800px位置</a-button>\n <a-button @click="scrollTo(0)">滚动到顶部</a-button>\n <a-button @click="scrollBottom()">滚动到底部</a-button>\n </div>\n <div class="scroll-wrap">\n <ScrollContainer ref="scrollRef">\n <ul>\n <template v-for="index in 100" :key="index">\n <li>{{ index }}</li>\n </template>\n </ul>\n </ScrollContainer>\n </div>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, ref, unref } from 'vue';\n import { CollapseContainer } from '/@/components/Container/index';\n import { ScrollContainer, ScrollActionType } from '/@/components/Container/index';\n export default defineComponent({\n components: { CollapseContainer, ScrollContainer },\n setup() {\n const scrollRef = ref<Nullable<ScrollActionType>>(null);\n const getScroll = () => {\n const scroll = unref(scrollRef);\n if (!scroll) {\n throw new Error('scroll is Null');\n }\n return scroll;\n };\n\n function scrollTo(top: number) {\n getScroll()?.scrollTo(top);\n }\n\n function scrollBottom() {\n getScroll()?.scrollBottom();\n }\n\n return {\n scrollTo,\n scrollRef,\n scrollBottom,\n };\n },\n });\n</script>\n<style lang="less" scoped>\n .scroll-wrap {\n width: 50%;\n height: 300px;\n background: #fff;\n }\n</style>\n
名称 | 回调参数 | 说明 |
---|---|---|
getScrollWrap | ()=>HtmlElement | 获取滚动容器 el |
scrollBottom | Function | 滚动到底部 |
scrollTo | Function(to:number,duration = 500) | 滚动到指定位置 |
名称 | 说明 |
---|---|
default | 默认区域 |
用于校验密码强度
<template>\n <div class="p-4 flex justify-center">\n <div class="demo-wrap p-10">\n <StrengthMeter placeholder="默认" />\n <StrengthMeter placeholder="禁用" disabled />\n <br />\n <StrengthMeter placeholder="隐藏input" :show-input="false" value="!@#qwe12345" />\n </div>\n </div>\n</template>\n\n<script lang="ts">\n import { defineComponent } from 'vue';\n import StrengthMeter from '/@/components/StrengthMeter/index';\n export default defineComponent({\n components: {\n StrengthMeter,\n },\n });\n</script>\n<style lang="less" scoped>\n .demo-wrap {\n width: 50%;\n background: #fff;\n border-radius: 10px;\n }\n</style>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string | - | - | 校验的值 |
showInput | boolean | true | - | 是否显示 input |
disabled | boolean | false | - | 是否禁用 |
事件 | 回调参数 | 说明 |
---|---|---|
score-change | number | 强度值改变触发 |
change | string | input 值改变触发 |
对 antv
的 table 组件进行封装
如果文档内没有,可以尝试在在线示例内寻找
<template>\n <div class="p-4">\n <BasicTable\n title="基础示例"\n titleHelpMessage="温馨提醒"\n :columns="columns"\n :dataSource="data"\n :canResize="canResize"\n :loading="loading"\n :striped="striped"\n :bordered="border"\n :pagination="{ pageSize: 20 }"\n >\n <template #toolbar>\n <a-button type="primary"> 操作按钮 </a-button>\n </template>\n </BasicTable>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { BasicTable } from '/@/components/Table';\n import { getBasicColumns, getBasicData } from './tableData';\n\n export default defineComponent({\n components: { BasicTable },\n setup() {\n return {\n columns: getBasicColumns(),\n data: getBasicData(),\n };\n },\n });\n</script>\n
所有可调用函数见下方 Methods
说明
<template>\n <div class="p-4">\n <BasicTable\n :canResize="false"\n title="RefTable示例"\n titleHelpMessage="使用Ref调用表格内方法"\n ref="tableRef"\n :api="api"\n :columns="columns"\n rowKey="id"\n :rowSelection="{ type: 'checkbox' }"\n />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, ref, unref } from 'vue';\n import { BasicTable, TableActionType } from '/@/components/Table';\n import { getBasicColumns, getBasicShortColumns } from './tableData';\n import { demoListApi } from '/@/api/demo/table';\n export default defineComponent({\n components: { BasicTable },\n setup() {\n const tableRef = ref<Nullable<TableActionType>>(null);\n\n function getTableAction() {\n const tableAction = unref(tableRef);\n if (!tableAction) {\n throw new Error('tableAction is null');\n }\n return tableAction;\n }\n function changeLoading() {\n getTableAction().setLoading(true);\n setTimeout(() => {\n getTableAction().setLoading(false);\n }, 1000);\n }\n return {\n tableRef,\n api: demoListApi,\n columns: getBasicColumns(),\n changeLoading,\n };\n },\n });\n</script>\n
<template>\n <div class="p-4">\n <BasicTable @register="registerTable">\n <template #action="{ record }">\n <TableAction\n :actions="[\n {\n label: '编辑',\n onClick: handleEdit.bind(null, record),\n auth: 'other', // 根据权限控制是否显示: 无权限,不显示\n },\n {\n label: '删除',\n icon: 'ic:outline-delete-outline',\n onClick: handleDelete.bind(null, record),\n auth: 'super', // 根据权限控制是否显示: 有权限,会显示\n },\n ]"\n :dropDownActions="[\n {\n label: '启用',\n popConfirm: {\n title: '是否启用?',\n confirm: handleOpen.bind(null, record),\n },\n ifShow: (_action) => {\n return record.status !== 'enable'; // 根据业务控制是否显示: 非enable状态的不显示启用按钮\n },\n },\n {\n label: '禁用',\n popConfirm: {\n title: '是否禁用?',\n confirm: handleOpen.bind(null, record),\n },\n ifShow: () => {\n return record.status === 'enable'; // 根据业务控制是否显示: enable状态的显示禁用按钮\n },\n },\n {\n label: '同时控制',\n popConfirm: {\n title: '是否动态显示?',\n confirm: handleOpen.bind(null, record),\n },\n auth: 'super', // 同时根据权限和业务控制是否显示\n ifShow: () => {\n return true; // 根据业务控制是否显示\n },\n },\n ]"\n />\n </template>\n </BasicTable>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicTable, useTable, BasicColumn, TableAction } from '/@/components/Table';\n\n import { demoListApi } from '/@/api/demo/table';\n const columns: BasicColumn[] = [\n {\n title: '姓名',\n dataIndex: 'name',\n auth: 'test', // 根据权限控制是否显示: 无权限,不显示\n },\n {\n title: '地址',\n dataIndex: 'address',\n auth: 'super', // 同时根据权限控制是否显示\n ifShow: (_column) => {\n return true; // 根据业务控制是否显示\n },\n },\n ];\n export default defineComponent({\n components: { BasicTable, TableAction },\n setup() {\n const [registerTable] = useTable({\n title: 'TableAction组件及固定列示例',\n api: demoListApi,\n columns: columns,\n bordered: true,\n actionColumn: {\n width: 250,\n title: 'Action',\n dataIndex: 'action',\n slots: { customRender: 'action' },\n },\n });\n function handleEdit(record: Recordable) {\n console.log('点击了编辑', record);\n }\n function handleDelete(record: Recordable) {\n console.log('点击了删除', record);\n }\n function handleOpen(record: Recordable) {\n console.log('点击了启用', record);\n }\n return {\n registerTable,\n handleEdit,\n handleDelete,\n handleOpen,\n };\n },\n });\n</script>\n
使用组件自带的 useTable 可以方便使用表单
下面是一个使用简单表格的示例,
<template>\n <BasicTable @register="registerTable" />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicTable, useTable } from '/@/components/Table';\n import { getBasicColumns, getBasicShortColumns } from './tableData';\n import { demoListApi } from '/@/api/demo/table';\n export default defineComponent({\n components: { BasicTable },\n setup() {\n const [\n registerTable,\n {\n setLoading,\n },\n ] = useTable({\n api: demoListApi,\n columns: getBasicColumns(),\n });\n\n function changeLoading() {\n setLoading(true);\n setTimeout(() => {\n setLoading(false);\n }, 1000);\n }\n }\n return {\n registerTable,\n changeLoading,\n };\n },\n });\n</script>\n
用于调用 Table 内部方法及 table 参数配置
// 表格的props也可以直接注册到useTable内部\nconst [register, methods] = useTable(props);\n
register
register 用于注册 useTable,如果需要使用useTable
提供的 api,必须将 register 传入组件的 onRegister
<template>\n <BasicTable @register="register" />\n</template>\n<script>\n export default defineComponent({\n components: { BasicForm },\n setup() {\n const [register] = useTable();\n return { register };\n },\n });\n</script>\n
setProps
类型:(props: Partial<BasicTableProps>) => void
说明: 用于设置表格参数
reload
类型:(opt?: FetchParams) => Promise<void>
说明: 刷新表格
redoHeight
类型:() => void
说明: 重新计算表格高度
setLoading
类型:(loading: boolean) => void
说明: 设置表格 loading 状态
getDataSource
获取表格数据
类型:<T = Recordable>() => T[]
说明: 获取表格数据
getRawDataSource
获取后端接口原始数据
类型:<T = Recordable>() => T
说明: 获取后端接口原始数据
getColumns
类型:(opt?: GetColumnsParams) => BasicColumn[]
说明: 获取表格数据
setColumns
类型:(columns: BasicColumn[] | string[]) => void
说明: 设置表头数据
setTableData
类型:<T = Recordable>(values: T[]) => void
说明: 设置表格数据
setPagination
类型:(info: Partial<PaginationProps>) => void
说明: 设置分页信息
deleteSelectRowByKey
类型:(key: string) => void
说明: 根据 key 删除取消选中行
getSelectRowKeys
类型:() => string[]
说明: 获取选中行的 keys
getSelectRows
类型:<T = Recordable>() => T[]
说明: 获取选中行的 rows
clearSelectedRowKeys
类型:() => void
说明: 清空选中行
setSelectedRowKeys
类型:(rowKeys: string[] | number[]) => void
说明: 设置选中行
getPaginationRef
类型:() => PaginationProps | boolean
说明: 获取当前分页信息
getShowPagination
类型:() => boolean
说明: 获取当前是否显示分页
setShowPagination
类型:(show: boolean) => Promise<void>
说明: 设置当前是否显示分页
getRowSelection
类型:() => TableRowSelection<Recordable>
说明: 获取勾选框信息
updateTableData
类型:(index: number, key: string, value: any)=>void
说明: 更新表格数据
updateTableDataRecord
类型: (rowKey: string | number, record: Recordable) => Recordable | void
说明: 根据唯一的 rowKey
更新指定行的数据.可用于不刷新整个表格而局部更新数据
deleteTableDataRecord
类型: (rowKey: string | number | string[] | number[]) => void
说明: 根据唯一的rowKey
动态删除指定行的数据.可用于不刷新整个表格而局部更新数据
insertTableDataRecord
类型: (record: Recordable, index?: number) => Recordable | void
说明: 可根据传入的 index
值决定插入数据行的位置,不传则是顺序插入,可用于不刷新整个表格而局部更新数据
getForm
类型:() => FormActionType
说明: 如果开启了搜索区域。可以通过该函数获取表单对象函数进行操作
expandAll
类型:() => void
说明: 展开树形表格
collapseAll
类型:() => void
说明: 折叠树形表格
温馨提醒
defaultExpandAllRows
、defaultExpandedRowKeys
属性在basicTable中不受支持,并且在antv table
v2.2.0之后也被移除。属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
clickToRowSelect | boolean | true | - | 点击行是否选中 checkbox 或者 radio。需要开启 | |
sortFn | (sortInfo: SorterResult<any>) => any | - | - | 自定义排序方法。见下方全局配置说明 | |
filterFn | (sortInfo: Partial<Recordable<string[]>>) => any | - | - | 自定义过滤方法。见下方全局配置说明 | |
showTableSetting | boolean | false | - | 显示表格设置工具 | |
tableSetting | TableSetting | - | - | 表格设置工具配置,见下方 TableSetting | |
striped | boolean | true | - | 斑马纹 | |
inset | boolean | false | - | 取消表格的默认 padding | |
autoCreateKey | boolean | true | - | 是否自动生成 key | |
showSummary | boolean | false | - | 是否显示合计行 | |
summaryData | any[] | - | - | 自定义合计数据。如果有则显示该数据 | |
emptyDataIsShowTable | boolean | true | - | 在启用搜索表单的前提下,是否在表格没有数据的时候显示表格 | |
summaryFunc | (...arg) => any[] | - | - | 计算合计行的方法 | |
boolean | false | - | |||
boolean | false | - | |||
isTreeTable | boolean | false | - | 是否树表 | |
api | (...arg: any) => Promise<any> | - | - | 请求接口,可以直接将src/api内的函数直接传入 | |
beforeFetch | (T)=>T | - | - | 请求之前对参数进行处理 | |
afterFetch | (T)=>T | - | - | 请求之后对返回值进行处理 | |
handleSearchInfoFn | (T)=>T | - | - | 开启表单后,在请求之前处理搜索条件参数 | |
fetchSetting | FetchSetting | - | - | 接口请求配置,可以配置请求的字段和响应的字段名,见下方全局配置说明 | |
immediate | boolean | true | - | 组件加载后是否立即请求接口,在 api 有传的情况下,如果为 false,需要自行使用 reload 加载表格数据 | |
searchInfo | any | - | - | 额外的请求参数 | |
useSearchForm | boolean | false | - | 使用搜索表单 | |
formConfig | any | - | - | 表单配置,参考表单组件的 Props | |
columns | any | - | - | 表单列信息 BasicColumn[] | |
showIndexColumn | boolean | ture | - | 是否显示序号列 | |
indexColumnProps | any | - | - | 序号列配置 BasicColumn | |
actionColumn | any | - | - | 表格右侧操作列配置 BasicColumn | |
ellipsis | boolean | true | - | 文本超过宽度是否显示... | |
canResize | boolean | true | - | 是否可以自适应高度(如果置于PageWrapper组件内,请勿启用PageWrapper的fixedHeight属性,二者不可同时使用) | |
clearSelectOnPageChange | boolean | false | - | 切换页码是否重置勾选状态 | |
resizeHeightOffset | number | 0 | - | 表格自适应高度计算结果会减去这个值 | |
rowSelection | any | - | - | 选择列配置 | |
title | string | - | - | 表格标题 | |
titleHelpMessage | string | string[] | - | - | 表格标题右侧温馨提醒 | |
maxHeight | number | - | - | 表格最大高度,超出会显示滚动条 | |
dataSource | any[] | - | - | 表格数据,非 api 加载情况 | |
bordered | boolean | false | - | 是否显示表格边框 | |
pagination | any | - | - | 分页信息配置,为 false 不显示分页 | |
loading | boolean | false | - | 表格 loading 状态 | |
scroll | any | - | - | 参考官方文档 scroll | |
beforeEditSubmit | ({record: Recordable,index: number,key: string | number,value: any}) => Promise<any> | - | - | 单元格编辑状态提交回调,返回false将阻止单元格提交数据到table。该回调在行编辑模式下无效。 | 2.7.2 |
{\n // 是否显示刷新按钮\n redo?: boolean;\n // 是否显示尺寸调整按钮\n size?: boolean;\n // 是否显示字段调整按钮\n setting?: boolean;\n // 是否显示全屏按钮\n fullScreen?: boolean;\n}\n
除 参考官方 Column 配置外,扩展以下参数
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
defaultHidden | boolean | false | - | 默认隐藏,可在列配置显示 |
helpMessage | string|string[] | - | - | 列头右侧帮助文本 |
edit | boolean | - | - | 是否开启单元格编辑 |
editRow | boolean | - | - | 是否开启行编辑 |
editable | boolean | false | - | 是否处于编辑状态 |
editComponent | ComponentType | Input | - | 编辑组件 |
editComponentProps | any | - | - | 对应编辑组件的 props |
editRule | ((text: string, record: Recordable) => Promise<string>) | - | - | 对应编辑组件的表单校验 |
editValueMap | (value: any) => string | - | - | 对应单元格值枚举 |
onEditRow | ()=>void | - | - | 触发行编辑 |
format | CellFormat | - | - | 单元格格式化 |
auth | RoleEnum | RoleEnum[] | string | string[] | - | - | 根据权限编码来控制当前列是否显示 |
ifShow | boolean | ((action: ActionItem) => boolean) | - | - | 根据业务状态来控制当前列是否显示 |
export type ComponentType =\n | 'Input'\n | 'InputNumber'\n | 'Select'\n | 'ApiSelect'\n | 'Checkbox'\n | 'Switch'\n | 'DatePicker' // v2.5.0 以上\n | 'TimePicker'; // v2.5.0 以上\n
export type CellFormat =\n | string\n | ((text: string, record: Recordable, index: number) => string | number)\n | Map<string | number, any>;\n
温馨提醒
除以下事件外,官方文档内的 event 也都支持,具体可以参考 antv table
事件 | 回调参数 | 说明 |
---|---|---|
fetch-success | Function({items,total}) | 接口请求成功后触发 |
fetch-error | Function(error) | 错误信息 |
selection-change | Function({keys,rows}) | 勾选事件触发 |
row-click | Function(record, index, event) | 行点击触发 |
row-dbClick | Function(record, index, event) | 行双击触发 |
row-contextmenu | Function(record, index, event) | 行右键触发 |
row-mouseenter | Function(record, index, event) | 行移入触发 |
row-mouseleave | Function(record, index, event) | 行移出触发 |
edit-end | Function({record, index, key, value}) | 单元格编辑完成触发 |
edit-cancel | Function({record, index, key, value}) | 单元格取消编辑触发 |
edit-row-end | Function() | 行编辑结束触发 |
edit-change | Function({column,value,record}) | 单元格编辑组件的 value 发生变化时触发 |
edit-change 说明
从版本 2.4.2
起,对于 edit-change
事件,record
中的 editValueRefs
装载了当前行的所有编辑组件(如果有的话)的值的 ref
对象,可用于处理同一行中的编辑组件的联动。请看下面的例子
function onEditChange({ column, record }) {\n // 当同一行的单价或者数量发生变化时,更新合计金额(三个数据均为当前行编辑组件的值)\n if (column.dataIndex === 'qty' || column.dataIndex === 'price') {\n const { editValueRefs: { total, qty, price } } = record;\n total.value = unref(qty) * unref(price);\n }\n }\n
温馨提醒
除以下参数外,官方文档内的 slot 也都支持,具体可以参考 antv table
名称 | 说明 | 版本 |
---|---|---|
tableTitle | 表格顶部左侧区域 | |
toolbar | 表格顶部右侧区域 | |
expandedRowRender | 展开行区域 | |
headerTop | 表格顶部区域(标题上方) | 2.6.1 |
当开启 form 表单后。以form-xxxx
为前缀的 slot 会被视为 form 的 slot
xxxx 为 form 组件的 slot。具体参考form 组件文档
e.g
form-submitBefore\n
字段调整组件
提供了可视化操作表格每一列的是否展示、位置、固定;包括序号列、勾选列。会响应tableMethods
中setColumns
和setProps
方法的更改内容。
值得注意的是
序号列
和勾选列
是在table的props中定义的,对应的字段分别是showIndexColumn
、rowSelection
。因此在动态改变表格列配置的时候,建议使用setProps方法,并显式地设置这两个字段的值来保证达到预期效果
// ...\nconst [registerTable, { setProps }] = useTable({...})\n\nsetProps({\n columns: [], // 表格的列配置 BasicColumn[]\n showIndexColumn: false, // 是否展示序号列\n rowSelection: false // 勾选列配置\n})\n
用于表格右侧操作列渲染
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
actions | ActionItem[] | - | - | 右侧操作列按钮列表 | |
dropDownActions | ActionItem[] | - | - | 右侧操作列更多下拉按钮列表 | |
stopButtonPropagation | boolean | false | true/false | 是否阻止操作按钮的click事件冒泡 | 2.5.0 |
ActionItem
export interface ActionItem {\n // 按钮文本\n label: string;\n // 是否禁用\n disabled?: boolean;\n // 按钮颜色\n color?: 'success' | 'error' | 'warning';\n // 按钮类型\n type?: string;\n // button组件props\n props?: any;\n // 按钮图标\n icon?: string;\n // 气泡确认框\n popConfirm?: PopConfirm;\n // 是否显示分隔线,v2.0.0+\n divider?: boolean;\n // 根据权限编码来控制当前列是否显示,v2.4.0+\n auth?: RoleEnum | RoleEnum[] | string | string[];\n // 根据业务状态来控制当前列是否显示,v2.4.0+\n ifShow?: boolean | ((action: ActionItem) => boolean);\n // 点击回调\n onClick?: Fn;\n // Tooltip配置,2.5.3以上版本支持,可以配置为string,或者完整的tooltip属性\n tooltip?: string | TooltipProps\n}\n
有关TooltipProps的说明,请参考tooltip
PopConfirm
export interface PopConfirm {\n title: string;\n okText?: string;\n cancelText?: string;\n confirm: Fn;\n cancel?: Fn;\n icon?: string;\n}\n
用于渲染单元格图片,支持图片预览
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
imgList | string[] | - | - | 图片地址列表 | |
size | number | - | - | 图片大小 | |
simpleShow | boolean | false | true/false | 简单显示模式(只显示第一张图片) | 2.5.0 |
showBadge | boolean | true | true/false | 简单模式下是否显示计数Badge | 2.5.0 |
margin | number | 4 | - | 常规模式下的图片间距 | 2.5.0 |
srcPrefix | string | - | - | 在每一个图片src前插入的内容 | 2.5.0 |
在componentsSettings 可以配置全局参数。用于统一整个项目的风格。可以通过 props 传值覆盖
',148);p.render=function(s,t,p,e,c,l){return n(),a("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/components_table.md.3c62f0f9.lean.js b/assets/components_table.md.3c62f0f9.lean.js new file mode 100644 index 00000000..9d83331e --- /dev/null +++ b/assets/components_table.md.3c62f0f9.lean.js @@ -0,0 +1 @@ +import{o as n,c as a,a as s}from"./app.8cddb23b.js";const t='{"title":"Table 表格","description":"","frontmatter":{},"headers":[{"level":2,"title":"Usage","slug":"usage"},{"level":3,"title":"示例","slug":"示例"},{"level":3,"title":"template 示例","slug":"template-示例"},{"level":3,"title":"BasicColumn 和 tableAction 通过权限和业务控制显示隐藏的示例","slug":"basiccolumn-和-tableaction-通过权限和业务控制显示隐藏的示例"},{"level":2,"title":"useTable","slug":"usetable"},{"level":3,"title":"Usage","slug":"usage-1"},{"level":3,"title":"Methods","slug":"methods"},{"level":2,"title":"Props","slug":"props"},{"level":3,"title":"TableSetting","slug":"tablesetting"},{"level":2,"title":"BasicColumn","slug":"basiccolumn"},{"level":3,"title":"EditComponentType","slug":"editcomponenttype"},{"level":3,"title":"CellFormat","slug":"cellformat"},{"level":2,"title":"事件","slug":"事件"},{"level":2,"title":"Slots","slug":"slots"},{"level":2,"title":"Form-Slots","slug":"form-slots"},{"level":2,"title":"ColumnSetting组件","slug":"columnsetting组件"},{"level":2,"title":"内置组件(只能用于表格内部)","slug":"内置组件(只能用于表格内部)"},{"level":3,"title":"TableAction","slug":"tableaction"},{"level":3,"title":"TableImg","slug":"tableimg"},{"level":2,"title":"全局配置","slug":"全局配置"}],"relativePath":"components/table.md","lastUpdated":1697592578589}',p={},o=s('',148);p.render=function(s,t,p,e,c,l){return n(),a("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/components_time.md.059df4f7.js b/assets/components_time.md.059df4f7.js new file mode 100644 index 00000000..1f74d4b4 --- /dev/null +++ b/assets/components_time.md.059df4f7.js @@ -0,0 +1 @@ +import{o as n,c as a,a as s}from"./app.8cddb23b.js";const t='{"title":"Time","description":"","frontmatter":{},"headers":[{"level":2,"title":"Usage","slug":"usage"},{"level":2,"title":"Props","slug":"props"}],"relativePath":"components/time.md","lastUpdated":1697592578589}',p={},o=s('相对时间组件
<template>\n <Time :value="time" />\n</template>\n<script lang="ts">\n import { defineComponent, reactive, toRefs } from 'vue';\n import { Time } from '/@/components/Time';\n\n export default defineComponent({\n components: { Time },\n setup() {\n const now = new Date().getTime();\n const state = reactive({\n time: now - 60 * 3 * 1000,\n });\n return {\n ...toRefs(state),\n now,\n };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string,Date,number | - | - | 时间值 |
step | number | 60 | - | 刷新时间 |
mode | string | relative | - | 模式,date:日期,datetime:时间戳,relative:相对时间 |
富文本组件位于 src/components/TinyMce
富文本组件使用的是 CDN 方式引入
可在 /@/components/TinyMce/src/Editor.vue 更改下面 CDN 地址
const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1';\n
<template>\n <Tinymce v-model="value" @change="handleChange" width="100%" />\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { Tinymce } from '/@/components/Tinymce/index';\n\n export default defineComponent({\n components: { Tinymce },\n setup() {\n const value = ref('hello world!');\n function handleChange(value: string) {\n console.log(value);\n }\n return { handleChange, value };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
options | any | {} | tinymce 的配置项 |
value(v-model) | string | - | 双向绑定值 |
height | number , string | 400 | 高度 |
width | number , string | auto | 宽度 |
toolbar | string[] | - | 工具栏 |
plugins | string[] | - | 插件 |
showImageUpload | boolean | true | 是否显示上传按钮 |
事件 | 回调参数 | 返回值 | 说明 |
---|---|---|---|
change | (str:string)=>{} | 富文本内容改变触发事件 |
用于页面/组件切换动画
<template>\n <div class="p-4">\n <div class="flex">\n <Select\n :options="options"\n v-model:value="value"\n placeholder="选择动画"\n :style="{ width: '150px' }"\n />\n <a-button type="primary" class="ml-4" @click="start"> start </a-button>\n </div>\n <component :is="`${value}Transition`">\n <div class="box" v-show="show"></div>\n </component>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { Select } from 'ant-design-vue';\n import {\n FadeTransition,\n ScaleTransition,\n SlideYTransition,\n ScrollYTransition,\n SlideYReverseTransition,\n ScrollYReverseTransition,\n SlideXTransition,\n ScrollXTransition,\n SlideXReverseTransition,\n ScrollXReverseTransition,\n ScaleRotateTransition,\n ExpandXTransition,\n ExpandTransition,\n } from '/@/components/Transition/index';\n\n const transitionList = [\n 'Fade',\n 'Scale',\n 'SlideY',\n 'ScrollY',\n 'SlideYReverse',\n 'ScrollYReverse',\n 'SlideX',\n 'ScrollX',\n 'SlideXReverse',\n 'ScrollXReverse',\n 'ScaleRotate',\n 'ExpandX',\n 'Expand',\n ];\n const options = transitionList.map((item) => ({\n label: item,\n value: item,\n key: item,\n }));\n\n export default defineComponent({\n components: {\n Select,\n FadeTransition,\n ScaleTransition,\n SlideYTransition,\n ScrollYTransition,\n SlideYReverseTransition,\n ScrollYReverseTransition,\n SlideXTransition,\n ScrollXTransition,\n SlideXReverseTransition,\n ScrollXReverseTransition,\n ScaleRotateTransition,\n ExpandXTransition,\n ExpandTransition,\n },\n setup() {\n const value = ref('Fade');\n const show = ref(true);\n function start() {\n show.value = false;\n setTimeout(() => {\n show.value = true;\n }, 300);\n }\n return { options, value, start, show };\n },\n });\n</script>\n<style lang="less" scoped>\n .box {\n width: 150px;\n height: 150px;\n margin-top: 20px;\n background: pink;\n }\n</style>\n
对 antv
的 tree 组件进行封装
<template>\n <BasicTree :treeData="treeData" />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicTree } from '/@/components/Tree/index';\n import { treeData } from './data';\n import { CollapseContainer } from '/@/components/Container/index';\n import { TreeItem } from '/@/components/Tree/index';\n\n export const treeData: TreeItem[] = [\n {\n title: 'parent 1',\n key: '0-0',\n icon: 'home|svg',\n children: [\n { title: 'leaf', key: '0-0-0' },\n {\n title: 'leaf',\n key: '0-0-1',\n children: [\n { title: 'leaf', key: '0-0-0-0' },\n { title: 'leaf', key: '0-0-0-1' },\n ],\n },\n ],\n },\n {\n title: 'parent 2',\n key: '1-1',\n icon: 'home|svg',\n children: [\n { title: 'leaf', key: '1-1-0' },\n { title: 'leaf', key: '1-1-1' },\n ],\n },\n {\n title: 'parent 3',\n key: '2-2',\n icon: 'home|svg',\n children: [\n { title: 'leaf', key: '2-2-0' },\n { title: 'leaf', key: '2-2-1' },\n ],\n },\n ];\n export default defineComponent({\n components: { BasicTree, CollapseContainer },\n setup() {\n return { treeData };\n },\n });\n</script>\n
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv tree
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
treeData | TreeItem[] | - | - | 树组件数据 | |
rightMenuList | ContextMenuItem[] | - | - | 右键菜单列表 | |
checkedKeys | string[] | - | - | 勾选的节点 | |
selectedKeys | string[] | - | - | 选中的节点 | |
expandedKeys | string[] | - | - | 展开的节点 | |
actionList | ActionItem[] | - | - | 鼠标移动上去右边操作按钮列表 | |
title | string | - | - | 定制标题字符串 | |
toolbar | boolean | - | - | 是否显示工具栏 | |
search | boolean | - | - | 显示搜索框 | |
clickRowToExpand | boolean | - | - | 是否在点击行时自动展开 | |
beforeRightClick | (node, event)=>ContextMenuItem[] | - | - | 右键点击回调,可返回右键菜单列表数据来生成右键菜单 | |
rightMenuList | ContextMenuItem[] | - | - | 右键菜单列表数据 | |
defaultExpandLevel | string | number | - | - | 初次渲染后默认展开的层级 | 2.4.1 |
defaultExpandAll | boolean | false | true/false | 初次渲染后默认全部 | 2.4.1 |
searchValue(v-model) | string | - | - | 当前搜索词 | 2.7.1 |
注意
defaultExpandLevel
、defaultExpandAll
仅在初次渲染时生效。如果basicTree
是在创建完毕之后才设置的treeData
(如异步数据),需要在更新后自己调用basicTree
提供的expandAll
、filterByLevel
来执行展开
ActionItem
{\n // 渲染的图标\n render: (record: any) => any;\n // 是否显示\n show?: boolean | ((record: Recordable) => boolean);\n}\n
ContextMenuItem
{\n // 文本\n label: string;\n // 图标\n icon?: string;\n // 是否禁用\n disabled?: boolean;\n // 事件\n handler?: (...arg) => any;\n // 是否显示分隔线\n divider?: boolean;\n // 子级菜单数据\n children?: ContextMenuItem[];\n}\n
温馨提醒
官方文档内的 slot 都支持,具体可以参考 antv tree
名称 | 回调参数 | 说明 |
---|---|---|
checkAll | (checkAll: boolean) => void | 选择所有 |
expandAll | (expandAll: boolean) => void | 展开所有 |
setExpandedKeys | (keys: Keys) => void | 设置展开节点 |
getExpandedKeys | () => Keys | 获取展开节点 |
setSelectedKeys | (keys: Keys) => void | 设置选中节点 |
getSelectedKeys | () => Keys | 获取选中节点 |
setCheckedKeys | (keys: CheckKeys) => void | 设置勾选节点 |
getCheckedKeys | () => CheckKeys | 获取勾选节点 |
filterByLevel | (level: number) => void | 显示指定等级 |
insertNodeByKey | (opt: InsertNodeParams) => void | 插入子节点到指定节点内 |
deleteNodeByKey | (key: string) => void | 根据 key 删除节点 |
updateNodeByKey | (key: string, node: Omit<TreeItem, 'key'>) => void | 根据 key 更新节点 |
setSearchValue | (value: string) => void | 设置当前搜索词(v2.7.1) |
getSearchValue | () => string | 获取当前搜索词(v2.7.1) |
文件上传组件
<template>\n <BasicUpload :maxSize="20" :maxNumber="10" @change="handleChange" :api="uploadApi" />\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { BasicUpload } from '/@/components/Upload';\n import { uploadApi } from '/@/api/sys/upload';\n\n export default defineComponent({\n components: { BasicUpload },\n setup() {\n return {\n uploadApi,\n handleChange: (list: string[]) => {\n createMessage.info(`已上传文件${JSON.stringify(list)}`);\n },\n };\n },\n });\n</script>\n
.env.development
和 .env.production
配置开发和生产的文件上传地址
# .env.development\n\nVITE_PROXY=[["/upload","http://localhost:3001/upload"]]\n\n::: tip\nv3.0.0开始,作者重构了vite.config.ts,新版本不再支持VITE_PROXY环境变量。\n:::\n\n# 如果没有跨域问题,则直接使用真实上传地址\nVITE_GLOB_UPLOAD_URL=/upload\n\n# .env.production\nVITE_GLOB_UPLOAD_URL=/upload\n\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string[] | - | - | 已上传的文件列表,支持v-model |
showPreviewNumber | boolean | true | - | 是否显示预览数量 |
emptyHidePreview | boolean | false | - | 没有上传文件时是否隐藏预览 |
helpText | string | - | - | 帮助文本 |
maxSize | number | 2 | - | 单个文件最大体积,单位 M |
maxNumber | number | Infinity | - | 最大上传数量,Infinity 则不限制 |
accept | string[] | - | - | 限制上传格式,可使用文件后缀名(点号可选)或MIME字符串。例如 ['.doc,','docx','application/msword','image/*'] |
multiple | boolean | - | - | 开启多文件上传 |
uploadParams | any | - | - | 上传携带的参数 |
api | Fn | - | - | 上传接口,为上面配置的接口 |
事件 | 回调参数 | 返回值 | 说明 | 版本 |
---|---|---|---|---|
change | (fileList)=>void | 文件列表内容改变触发事件 | ||
delete | (record)=>void | 在上传列表中删除文件的事件 | ||
preview-delete | (url:string)=>void | 在预览列表中删除文件的事件 | 2.5.3 |
拖动校验组件
<template>\n <div class="p-10">\n <BasicDragVerify @success="handleSuccess" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent, ref } from 'vue';\n import { BasicDragVerify, DragVerifyActionType, PassingData } from '/@/components/Verify/index';\n export default defineComponent({\n components: { BasicDragVerify },\n setup() {\n function handleSuccess(data: PassingData) {\n const { time } = data;\n createMessage.success(`校验成功,耗时${time}秒`);\n }\n return {\n handleSuccess,\n handleBtnClick,\n };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
value | boolean | - | 是否通过 |
text | string | 请按住滑块拖动 | 未拖动时候显示文字 |
successText | string | 验证通过 | 验证成功后显示文本 |
height | string|string | 40 | 高度 |
width | string|string | 260 | 宽度 |
circle | boolean | false | 是否圆角 |
wrapStyle | any | - | 外层容器样式 |
contentStyle | any | - | 主体内容样式 |
barStyle | any | - | bar 样式 |
actionStyle | any | - | 拖拽按钮样式 |
名称 | 回调参数 | 说明 |
---|---|---|
resume | ()=>{} | 还原初始值 |
图片还原正方向校验组件
<template>\n <div class="p-10">\n <RotateDragVerify :src="img" ref="el" @success="handleSuccess" />\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { RotateDragVerify } from '/@/components/Verify/index';\n\n import img from '/@/assets/images/header.jpg';\n export default defineComponent({\n components: { RotateDragVerify },\n setup() {\n const handleSuccess = () => {\n console.log('success!');\n };\n return {\n handleSuccess,\n img,\n };\n },\n });\n</script>\n
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
src | string | - | 图片地址 |
imgWidth | number | - | 图片宽度 |
imgWrapStyle | any | - | 图片外层容器样式 |
minDegree | number | - | 最小旋转角度 |
maxDegree | number | - | 最大旋转角度 |
diffDegree | number | - | 误差角度 |
value | boolean | - | 是否通过 |
text | string | 请按住滑块拖动 | 未拖动时候显示文字 |
successText | string | 验证通过 | 验证成功后显示文本 |
height | string|string | 40 | 高度 |
width | string|string | 260 | 宽度 |
circle | boolean | false | 是否圆角 |
wrapStyle | any | - | 外层容器样式 |
contentStyle | any | - | 主体内容样式 |
barStyle | any | - | bar 样式 |
actionStyle | any | - | 拖拽按钮样式 |
名称 | 回调参数 | 说明 |
---|---|---|
resume | Function | 还原初始值 |
虚拟滚动组件(用于大量数据纯展示时使用)
<template>\n <div class="p-4 virtual-scroll-demo">\n <Divider>基础滚动示例</Divider>\n <div class="virtual-scroll-demo-wrap">\n <VirtualScroll :itemHeight="41" :items="data" :height="300" :width="300">\n <template v-slot="{ item }">\n <div class="virtual-scroll-demo__item">{{ item.title }}</div>\n </template>\n </VirtualScroll>\n </div>\n\n <Divider>即使不可见,也预先加载50条数据,防止空白</Divider>\n <div class="virtual-scroll-demo-wrap">\n <VirtualScroll :itemHeight="41" :items="data" :height="300" :width="300" :bench="50">\n <template v-slot="{ item }">\n <div class="virtual-scroll-demo__item">{{ item.title }}</div>\n </template>\n </VirtualScroll>\n </div>\n </div>\n</template>\n<script lang="ts">\n import { defineComponent } from 'vue';\n import { VirtualScroll } from '/@/components/VirtualScroll/index';\n\n import { Divider } from 'ant-design-vue';\n const data: any[] = (() => {\n const arr: any[] = [];\n for (let index = 1; index < 20000; index++) {\n arr.push({\n title: '列表项' + index,\n });\n }\n return arr;\n })();\n export default defineComponent({\n components: { VirtualScroll, Divider },\n setup() {\n return { data: data };\n },\n });\n</script>\n<style lang="less" scoped>\n .virtual-scroll-demo {\n &-wrap {\n display: flex;\n margin: 0 30%;\n background: #fff;\n justify-content: center;\n }\n\n /deep/ &__item {\n height: 40px;\n padding: 0 20px;\n line-height: 40px;\n border-bottom: 1px solid #ddd;\n }\n }\n</style>\n
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
height | string|number | - | - | 高度 |
width | string|number | - | - | 宽度 |
maxHeight | string|number | - | - | 最大高度 |
maxWidth | string|number | - | - | 最大宽度 |
minHeight | string|number | - | - | 最小高度 |
minWidth | string|number | - | - | 最小宽度 |
itemHeight | string|number | - | - | 每个选项高度,必传 |
items | any[] | - | - | 选项列表 |
名称 | 说明 |
---|---|
default | 默认 |
跨域产生的原因是由于前端地址与后台接口不是同源,从而导致 ajax 不能发送
非同源产生的问题
同源条件
协议,端口,主机 三者相同即为同源
反之,其中只要 某一个 不一样则为不同源
本地开发跨域
本地开发一般使用下面 3 种方式进行处理
项目内部自带第一种方式,具体可以参考服务端交互-本地开发环境接口地址修改
生产环境跨域
生产环境一般使用下面 2 种方式进行处理
后台开启 cors 不需要前端做任何改动
nginx 配置文件可以查看nginx 配置
',15);r.render=function(i,o,r,s,a,n){return l(),t("div",null,[e])};export default r;export{o as __pageData}; diff --git a/assets/dep_cors.md.37a05889.lean.js b/assets/dep_cors.md.37a05889.lean.js new file mode 100644 index 00000000..ce0310c7 --- /dev/null +++ b/assets/dep_cors.md.37a05889.lean.js @@ -0,0 +1 @@ +import{o as l,c as t,a as i}from"./app.8cddb23b.js";const o='{"title":"跨域处理","description":"","frontmatter":{},"headers":[{"level":2,"title":"产生原因","slug":"产生原因"},{"level":2,"title":"解决方式","slug":"解决方式"}],"relativePath":"dep/cors.md","lastUpdated":1697592578589}',r={},e=i('',15);r.render=function(i,o,r,s,a,n){return l(),t("div",null,[e])};export default r;export{o as __pageData}; diff --git a/assets/dep_dark.md.37147652.js b/assets/dep_dark.md.37147652.js new file mode 100644 index 00000000..99ed73a9 --- /dev/null +++ b/assets/dep_dark.md.37147652.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.8cddb23b.js";const t='{"title":"黑暗主题","description":"","frontmatter":{},"headers":[{"level":2,"title":"介绍","slug":"介绍"},{"level":2,"title":"原理","slug":"原理"},{"level":2,"title":"配置","slug":"配置"},{"level":2,"title":"切换","slug":"切换"}],"relativePath":"dep/dark.md","lastUpdated":1697592578593}',p={},e=a('项目已经内置了黑暗主题切换,只需配置自己需要的颜色变量,即可在项目中使用
通过 vite-plugin-theme 插件,将所有的颜色变量抽取到独立的 css 文件,并且全部在 html 上面加上 css 选择器。通过改变 html 标签的 data-theme
属性来进行黑暗主题切换
黑暗主题颜色配置通过 vite-plugin-theme 实现,具体代码在 build/vite/plugin/theme
antdDarkThemePlugin({\n darkModifyVars: {\n ...generateModifyVars(true),\n 'text-color': '#c9d1d9',\n 'text-color-base': '#c9d1d9',\n 'component-background': '#151515',\n 'text-color-secondary': '#8b949e',\n 'border-color-base': '#303030',\n 'item-active-bg': '#111b26',\n 'app-content-background': 'rgb(255 255 255 / 4%)',\n },\n});\n
只需要使用 vite-plugin-theme 提供的函数来进行切换即可
import { darkCssIsReady, loadDarkThemeCss } from 'vite-plugin-theme/es/client';\n\nexport async function updateDarkTheme(mode: string | null = 'light') {\n const htmlRoot = document.getElementById('htmlRoot');\n if (mode === 'dark') {\n if (import.meta.env.PROD && !darkCssIsReady) {\n await loadDarkThemeCss();\n }\n htmlRoot?.setAttribute('data-theme', 'dark');\n } else {\n htmlRoot?.setAttribute('data-theme', 'light');\n }\n}\n
使用 lint 的好处
具备基本工程素养的同学都会注重编码规范,而代码风格检查(Code Linting,简称 Lint)是保障代码规范一致性的重要手段。
遵循相应的代码规范有以下好处
项目内集成了以下几种代码校验方式
WARNING
lint 不是必须的,但是很有必要,一个项目做大了以后或者参与人员过多后,就会出现各种风格迥异的代码,对后续的维护造成了一定的麻烦
ESLint 是一个代码规范和错误检查工具,有以下几个特性
# 执行下面代码.能修复的会自动修复,不能修复的需要手动修改\nyarn run lint:eslint\n
项目的 eslint 配置位于根目录下 .eslintrc.js 内,可以根据团队自行修改代码规范
推荐使用 vscode 进行开发,vscode 自带 eslint 插件,可以自动修改一些错误。
同时项目内也自带了 vscode eslint 配置,具体在 .vscode/setting.json
文件夹内部。只要使用 vscode 开发不用任何设置即可使用
在一个团队中,每个人的 git 的 commit 信息都不一样,五花八门,没有一个机制很难保证规范化,如何才能规范化呢?可能你想到的是 git 的 hook 机制,去写 shell 脚本去实现。这当然可以,其实 JavaScript 有一个很好的工具可以实现这个模板,它就是 commitlint(用于校验 git 提交信息规范)。
commit-lint 的配置位于项目根目录下 commitlint.config.js
feat
增加新功能fix
修复问题/BUGstyle
代码风格相关无影响运行结果的perf
优化/性能提升refactor
重构revert
撤销修改test
测试相关docs
文档/注释chore
依赖更新/脚手架配置修改等workflow
工作流改进ci
持续集成mod
不确定分类的修改wip
开发中types
类型修改在 .husky/commit-msg
内注释以下代码即可
# npx --no-install commitlint --edit "$1"\n
\ngit commit -m 'feat(home): add home page'\n\n
stylelint 用于校验项目内部 css 的风格,加上编辑器的自动修复,可以很好的统一项目内部 css 风格
stylelint 配置位于根目录下 stylelint.config.js
如果您使用的是 vscode 编辑器的话,只需要安装下面插件,即可在保存的时候自动格式化文件内部 css 样式
插件
prettier 可以用于统一项目代码风格,统一的缩进,单双引号,尾逗号等等风格
prettier 配置文件位于项目根目录下 prettier.config.js
如果您使用的是 vscode 编辑器的话,只需要安装下面插件,即可在保存的时候自动格式化文件内部 js 格式
插件
git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风格校验,如果校验没通过,则不会进行提交。需要开发者自行修改后再次进行提交
有一个问题就是校验会校验全部代码,但是我们只想校验我们自己提交的代码,这个时候就可以使用 husky。
最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 husky 或者 pre-commit 在本地提交之前先做一次 Lint 校验。
项目在 .husky
内部定义了相应的 hooks
# 删除husky依赖即可\nyarn remove huksy\n\n
# 加上 --no-verify即可跳过git hook校验(--no-verify 简写为 -n)\ngit commit -m "xxx" --no-verify\n
用于自动修复提交文件风格问题
lint-staged 配置位于项目 .husky
目录下 lintstagedrc.js
module.exports = {\n // 对指定格式文件 在提交的时候执行相应的修复命令\n '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],\n '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],\n 'package.json': ['prettier --write'],\n '*.vue': ['eslint --fix', 'stylelint --fix', 'prettier --write', 'git add .'],\n '*.{scss,less,styl,css,html}': ['stylelint --fix', 'prettier --write', 'git add .'],\n '*.md': ['prettier --write'],\n};\n
项目中集成了三种权限处理方式:
实现原理: 在前端固定写死路由的权限,指定路由有哪些权限可以查看。只初始化通用的路由,需要权限才能访问的路由没有被加入路由表内。在登陆后或者其他方式获取用户角色后,通过角色去遍历路由表,获取该角色可以访问的路由表,生成路由表,再通过 router.addRoutes
添加到路由实例,实现权限的过滤。
缺点: 权限相对不自由,如果后台改动角色,前台也需要跟着改动。适合角色较固定的系统
ROLE
模式// ! 改动后需要清空浏览器缓存\nconst setting: ProjectConfig = {\n // 权限模式\n permissionMode: PermissionModeEnum.ROLE,\n};\n
import type { AppRouteModule } from '/@/router/types';\n\nimport { getParentLayout, LAYOUT } from '/@/router/constant';\nimport { RoleEnum } from '/@/enums/roleEnum';\nimport { t } from '/@/hooks/web/useI18n';\n\nconst permission: AppRouteModule = {\n path: '/permission',\n name: 'Permission',\n component: LAYOUT,\n redirect: '/permission/front/page',\n meta: {\n icon: 'ion:key-outline',\n title: t('routes.demo.permission.permission'),\n },\n\n children: [\n {\n path: 'front',\n name: 'PermissionFrontDemo',\n component: getParentLayout('PermissionFrontDemo'),\n meta: {\n title: t('routes.demo.permission.front'),\n },\n children: [\n {\n path: 'auth-pageA',\n name: 'FrontAuthPageA',\n component: () => import('/@/views/demo/permission/front/AuthPageA.vue'),\n meta: {\n title: t('routes.demo.permission.frontTestA'),\n roles: [RoleEnum.SUPER],\n },\n },\n {\n path: 'auth-pageB',\n name: 'FrontAuthPageB',\n component: () => import('/@/views/demo/permission/front/AuthPageB.vue'),\n meta: {\n title: t('routes.demo.permission.frontTestB'),\n roles: [RoleEnum.TEST],\n },\n },\n ],\n },\n ],\n};\n\nexport default permission;\n
详细代码见 src/router/guard/permissionGuard.ts
// 这里只列举了主要代码\nconst routes = await permissionStore.buildRoutesAction();\n\nroutes.forEach((route) => {\n router.addRoute(route as unknown as RouteRecordRaw);\n});\n\nconst redirectPath = (from.query.redirect || to.path) as string;\nconst redirect = decodeURIComponent(redirectPath);\nconst nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };\npermissionStore.setDynamicAddedRoute(true);\nnext(nextData);\n
permissionStore.buildRoutesAction 用于过滤动态路由,详细代码见 src/store/modules/permission.ts
// 主要代码\nif (permissionMode === PermissionModeEnum.ROLE) {\n const routeFilter = (route: AppRouteRecordRaw) => {\n const { meta } = route;\n const { roles } = meta || {};\n if (!roles) return true;\n return roleList.some((role) => roles.includes(role));\n };\n routes = filter(asyncRoutes, routeFilter);\n routes = routes.filter(routeFilter);\n // Convert multi-level routing to level 2 routing\n routes = flatMultiLevelRoutes(routes);\n}\n
系统提供 usePermission 方便角色相关操作
import { usePermission } from '/@/hooks/web/usePermission';\nimport { RoleEnum } from '/@/enums/roleEnum';\n\nexport default defineComponent({\n setup() {\n const { changeRole } = usePermission();\n // 更换为test角色\n // 动态更改角色,传入角色名称,可以是数组\n changeRole(RoleEnum.TEST);\n return {};\n },\n});\n
函数方式
usePermission 还提供了按钮级别的权限控制。
<template>\n <a-button v-if="hasPermission([RoleEnum.TEST, RoleEnum.SUPER])" color="error" class="mx-4">\n 拥有[test,super]角色权限可见\n </a-button>\n</template>\n<script lang="ts">\n import { usePermission } from '/@/hooks/web/usePermission';\n import { RoleEnum } from '/@/enums/roleEnum';\n\n export default defineComponent({\n setup() {\n const { hasPermission } = usePermission();\n\n return { hasPermission };\n },\n });\n</script>\n
组件方式
具体查看权限组件使用
指令方式
TIP
指令方式不能动态更改权限
<a-button v-auth="RoleEnum.SUPER" type="primary" class="mx-4"> 拥有super角色权限可见</a-button>\n
实现原理: 是通过接口动态生成路由表,且遵循一定的数据结构返回。前端根据需要处理该数据为可识别的结构,再通过 router.addRoutes
添加到路由实例,实现权限的动态生成。
BACK
模式// ! 改动后需要清空浏览器缓存\nconst setting: ProjectConfig = {\n // 权限模式\n permissionMode: PermissionModeEnum.BACK,\n};\n
permissionStore.buildRoutesAction 用于过滤动态路由,详细代码见 /@/store/modules/permission.ts
// 主要代码\nif (permissionMode === PermissionModeEnum.BACK) {\n const { createMessage } = useMessage();\n\n createMessage.loading({\n content: t('sys.app.menuLoading'),\n duration: 1,\n });\n\n // !Simulate to obtain permission codes from the background,\n // this function may only need to be executed once, and the actual project can be put at the right time by itself\n let routeList: AppRouteRecordRaw[] = [];\n try {\n this.changePermissionCode();\n routeList = (await getMenuList()) as AppRouteRecordRaw[];\n } catch (error) {\n console.error(error);\n }\n\n // Dynamically introduce components\n routeList = transformObjToRoute(routeList);\n\n // Background routing to menu structure\n const backMenuList = transformRouteToMenu(routeList);\n this.setBackMenuList(backMenuList);\n\n routeList = flatMultiLevelRoutes(routeList);\n routes = [PAGE_NOT_FOUND_ROUTE, ...routeList];\n}\n
getMenuList 返回值格式
返回值由多个路由模块组成
注意
后端接口返回的数据中必须包含PageEnum.BASE_HOME
指定的路由(path定义于src/enums/pageEnum.ts
)
[\n {\n path: '/dashboard',\n name: 'Dashboard',\n component: '/dashboard/welcome/index',\n meta: {\n title: 'routes.dashboard.welcome',\n affix: true,\n icon: 'ant-design:home-outlined',\n },\n },\n {\n path: '/permission',\n name: 'Permission',\n component: 'LAYOUT',\n redirect: '/permission/front/page',\n meta: {\n icon: 'carbon:user-role',\n title: 'routes.demo.permission.permission',\n },\n children: [\n {\n path: 'back',\n name: 'PermissionBackDemo',\n meta: {\n title: 'routes.demo.permission.back',\n },\n\n children: [\n {\n path: 'page',\n name: 'BackAuthPage',\n component: '/demo/permission/back/index',\n meta: {\n title: 'routes.demo.permission.backPage',\n },\n },\n {\n path: 'btn',\n name: 'BackAuthBtn',\n component: '/demo/permission/back/Btn',\n meta: {\n title: 'routes.demo.permission.backBtn',\n },\n },\n ],\n },\n ],\n },\n];\n
系统提供 usePermission 方便角色相关操作
import { usePermission } from '/@/hooks/web/usePermission';\nimport { RoleEnum } from '/@/enums/roleEnum';\n\nexport default defineComponent({\n setup() {\n const { changeMenu } = usePermission();\n\n // 更改菜单的实现需要自行去修改\n changeMenu();\n return {};\n },\n});\n
函数方式
usePermission 还提供了按钮级别的权限控制。
<template>\n <a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4">\n 拥有[20000,2000010]code可见\n </a-button>\n</template>\n<script lang="ts">\n import { usePermission } from '/@/hooks/web/usePermission';\n import { RoleEnum } from '/@/enums/roleEnum';\n\n export default defineComponent({\n setup() {\n const { hasPermission } = usePermission();\n return { hasPermission };\n },\n });\n</script>\n
组件方式
具体查看权限组件使用
指令方式
TIP
指令方式不能动态更改权限
<a-button v-auth="'1000'" type="primary" class="mx-4"> 拥有code ['1000']权限可见 </a-button>\n
通常,如需做按钮级别权限,后台会提供相应的 code,或者类型的判断标识。这些编码只需要在登录后获取一次即可。
import { getPermCodeByUserId } from '/@/api/sys/user';\nimport { permissionStore } from '/@/store/modules/permission';\nasync function changePermissionCode(userId: string) {\n // 从后台获取当前用户拥有的编码\n const codeList = await getPermCodeByUserId({ userId });\n permissionStore.commitPermCodeListState(codeList);\n}\n
项目目前的组件注册机制是按需注册,是在需要用到的页面才引入。
<template>\n <Menu>\n <SubMenu></SubMenu>\n <Menu>\n\n <menu>\n <sub-menu></sub-menu>\n <menu>\n</template>\n<script>\nimport { Menu } from 'ant-design-vue';\nexport default defineComponent({\n components: {\n Menu: Menu,\n SubMenu: Menu.SubMenu\n },\n})\n</script>\n
tsx 文件内不能使用全局注册组件
import { Menu } from 'ant-design-vue';\n\nexport default defineComponent({\n setup() {\n return () => (\n <Menu>\n <Menu.SubMenu></Menu.SubMenu>\n </Menu>\n );\n },\n});\n
如果不习惯按需引入方式,可以进行全局注册。全局注册也分两种方式
只注册需要的组件
代码地址:src/components/registerGlobComp.ts
import {\n // Need\n Button as AntButton,\n Optional,\n Select,\n Alert,\n Checkbox,\n DatePicker,\n Radio,\n Switch,\n Card,\n List,\n Tabs,\n Descriptions,\n Tree,\n Table,\n Divider,\n Modal,\n Drawer,\n Dropdown,\n Tag,\n Tooltip,\n Badge,\n Popover,\n Upload,\n Transfer,\n Steps,\n PageHeader,\n Result,\n Empty,\n Avatar,\n Menu,\n Breadcrumb,\n Form,\n Input,\n Row,\n Col,\n Spin,\n} from 'ant-design-vue';\n\nexport function registerGlobComp(app: App) {\n app\n .use(Select)\n .use(Alert)\n .use(Breadcrumb)\n .use(Checkbox)\n .use(DatePicker)\n .use(Radio)\n .use(Switch)\n .use(Card)\n .use(List)\n .use(Descriptions)\n .use(Tree)\n .use(Table)\n .use(Divider)\n .use(Modal)\n .use(Drawer)\n .use(Dropdown)\n .use(Tag)\n .use(Tooltip)\n .use(Badge)\n .use(Popover)\n .use(Upload)\n .use(Transfer)\n .use(Steps)\n .use(PageHeader)\n .use(Result)\n .use(Empty)\n .use(Avatar)\n .use(Menu)\n .use(Tabs)\n .use(Form)\n .use(Input)\n .use(Row)\n .use(Col)\n .use(Spin);\n}\n
main.ts
内import { createApp } from 'vue';\nimport Antd from 'ant-design-vue';\nimport 'ant-design-vue/dist/antd.less';\nconst app = createApp(App);\napp.use(Antd);\n
if (import.meta.env.DEV) {\n import('ant-design-vue/dist/antd.less');\n}\n
主要介绍如何在项目中使用和规划样式文件。
默认使用 less 作为预处理语言,建议在使用前或者遇到疑问时学习一下 Less 的相关特性(如果想获取基础的 CSS 知识或查阅属性,请参考 MDN 文档)。
项目中使用的通用样式,都存放于 src/design/ 下面。
.\n├── ant # ant design 一些样式覆盖\n├── color.less # 颜色\n├── index.less # 入口\n├── public.less # 公共类\n├── theme.less # 主题相关\n├── config.less # 每个组件都会自动引入样式\n├── transition # 动画相关\n└── var # 变量\n\n
全局注入
config.less 这个文件会被全局注入到所有文件,所以在页面内可以直接使用变量而不需要手动引入
<style lang="less" scoped>\n // 这里已经隐式注入了 config.less\n</style>\n
项目中引用到了 tailwindcss,具体可以见文件使用说明。
语法如下:
<div class="relative w-full h-full px-4"></div>\n
项目中使用了 windicss,具体参见文件使用说明。
语法如下:
<div class="relative w-full h-full px-4"></div>\n
注意事项
windcss 目前会造成本地开发内存溢出,所以后续可能会考虑切换到 TailwindCss,两者基本相同。
所以尽量少用 Windicss 新增的特性,防止后续切换成本高。
主要是因为 Ant Design 默认使用 less 作为样式语言,使用 Less 可以跟其保持一致。
没有加 scoped
属性,默认会编译成全局样式,可能会造成全局污染
<style></style>\n\n<style scoped></style>\n
温馨提醒
使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。
有时我们可能想明确地制定一个针对子组件的规则。
如果你希望 scoped
样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>>
操作符。有些像 Sass 之类的预处理器无法正确解析 >>>
。这种情况下你可以使用 /deep/
或 ::v-deep
操作符取而代之——两者都是 >>>
的别名,同样可以正常工作。
详情可以查看 RFC0023-scoped-styles-changes。
使用 scoped 后,父组件的样式将不会渗透到子组件中,所以可以使用以下方式解决:
<style scoped>\n /* deep selectors */\n ::v-deep(.foo) {\n }\n /* shorthand */\n :deep(.foo) {\n }\n\n /* targeting slot content */\n ::v-slotted(.foo) {\n }\n /* shorthand */\n :slotted(.foo) {\n }\n\n /* one-off global rule */\n ::v-global(.foo) {\n }\n /* shorthand */\n :global(.foo) {\n }\n</style>\n
针对样式覆盖问题,还有一种方案是使用 CSS Modules 模块化方案。使用方式如下。
<template>\n <span :class="$style.span1">hello</span>\n</template>\n\n<script>\n import { useCSSModule } from 'vue';\n\n export default {\n setup(props, context) {\n const $style = useCSSModule();\n const moduleAStyle = useCSSModule('moduleA');\n return {\n $style,\n moduleAStyle,\n };\n },\n };\n</script>\n\n<style lang="less" module>\n .span1 {\n color: green;\n font-size: 30px;\n }\n</style>\n\n<style lang="less" module="moduleA">\n .span1 {\n color: green;\n font-size: 30px;\n }\n</style>\n
加上 reference 可以解决页面内重复引用导致实际生成的 style 样式表重复的问题。
这步已经全局引入了。所以可以不写,直接使用变量
<style lang="less" scoped>\n /* 该行代码已全局引用。可以不用单独引入 */\n @import (reference) '../../design/config.less';\n<style>\n
这种模式会先启动 vite 服务,Electron 使用 Url 地址来进行渲染
Electron 代码在 electron-main 分支
# clone electron-main分支代码\ngit clone -b electron-main https://github.com/vbenjs/vue-vben-admin vben-admin-electron\n
yarn\n
提示
首次下载 Electron 依赖会比较慢,可以在项目根目录下新建.npmrc
文件,填入下方内容即可
ELETRON_MIRROR=https://npm.taobao.org/mirrors/electron/\n
yarn dev:app\n
yarn build:app\n
TODO: 待适配
',16);s.render=function(n,r,s,l,i,c){return e(),a("div",null,[t])};export default s;export{r as __pageData}; diff --git a/assets/guide_electron.md.1fc6b47b.lean.js b/assets/guide_electron.md.1fc6b47b.lean.js new file mode 100644 index 00000000..6dd8d68a --- /dev/null +++ b/assets/guide_electron.md.1fc6b47b.lean.js @@ -0,0 +1 @@ +import{o as e,c as a,a as n}from"./app.8cddb23b.js";const r='{"title":"Electron","description":"","frontmatter":{},"headers":[{"level":2,"title":"URL 模式","slug":"url-模式"},{"level":3,"title":"使用","slug":"使用"},{"level":3,"title":"从 GitHub 获取代码","slug":"从-github-获取代码"},{"level":3,"title":"安装依赖","slug":"安装依赖"},{"level":3,"title":"运行","slug":"运行"},{"level":3,"title":"打包","slug":"打包"},{"level":2,"title":"标准模式","slug":"标准模式"}],"relativePath":"guide/electron.md","lastUpdated":1697592578593}',s={},t=n('',16);s.render=function(n,r,s,l,i,c){return e(),a("div",null,[t])};export default s;export{r as __pageData}; diff --git a/assets/guide_index.md.bb37fa1c.js b/assets/guide_index.md.bb37fa1c.js new file mode 100644 index 00000000..2db7159e --- /dev/null +++ b/assets/guide_index.md.bb37fa1c.js @@ -0,0 +1 @@ +import{o as n,c as s,a as e}from"./app.8cddb23b.js";const a='{"title":"开始","description":"","frontmatter":{},"headers":[{"level":2,"title":"前言","slug":"前言"},{"level":2,"title":"环境准备","slug":"环境准备"},{"level":2,"title":"工具配置","slug":"工具配置"},{"level":2,"title":"代码获取","slug":"代码获取"},{"level":3,"title":"从 GitHub 获取代码","slug":"从-github-获取代码"},{"level":3,"title":"从 Gitee 获取代码","slug":"从-gitee-获取代码"},{"level":2,"title":"安装","slug":"安装"},{"level":3,"title":"安装 Node.js","slug":"安装-node-js"},{"level":3,"title":"安装依赖","slug":"安装依赖"},{"level":2,"title":"npm script","slug":"npm-script"},{"level":3,"title":"生成图标集","slug":"生成图标集"},{"level":3,"title":"重新安装依赖","slug":"重新安装依赖"},{"level":2,"title":"目录说明","slug":"目录说明"}],"relativePath":"guide/index.md","lastUpdated":1697592578593}',t={},o=e('本文会帮助你从头启动项目
关于组件
项目虽然二次封装了一些组件,但是可能不能满足大部分的要求。所以,如果组件不满足你的要求,完全可以不用甚至删除代码自己写,不必坚持使用项目自带的组件。
如果您使用的 IDE 是vscode(推荐)的话,可以安装以下工具来提高开发效率及代码格式化
注意
注意存放代码的目录及所有父级目录不能存在中文、韩文、日文以及空格,否则安装依赖后启动会出错。
# clone 代码\ngit clone https://github.com/vbenjs/vue-vben-admin.git\n\n
如果从 github clone 代码较慢的话,可以尝试用 Gitee 同步代码到自己的仓库,再 clone 下来即可。
也可以通过下方地址进行 clone
git clone https://gitee.com/annsion/vue-vben-admin.git\n
注意
Gitee的代码可能不是最新的
如果您电脑未安装Node.js,请安装它。
验证
# 出现相应npm版本即可\nnpm -v\n# 出现相应node版本即可\nnode -v\n\n
如果你需要同时存在多个 node 版本,可以使用 Nvm 或者其他工具进行 Node.js 进行版本管理。
必须使用 pnpm进行依赖安装(若其他包管理器安装不了需要自行处理)。
如果未安装pnpm
,可以用下面命令来进行全局安装
# 全局安装pnpm\nnpm install -g pnpm\n# 验证\npnpm -v # 出现对应版本号即代表安装成功\n
在项目根目录下,打开命令窗口执行,耐心等待安装完成即可
# 安装依赖\npnpm i\n
安装依赖时 husky 安装失败
请查看你的源码是否从 github 直接下载的,直接下载是没有 .git
文件夹的,而 husky
需要依赖 git
才能安装。此时需使用 git init
初始化项目,再尝试重新安装即可。
"scripts": {\n # 安装依赖\n "bootstrap": "pnpm install",\n # 构建项目\n "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build",\n # 生成打包分析,在电脑上执行完成后会自动打开界面\n "build:analyze": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode analyze",\n # 构建成docker镜像\n "build:docker": "vite build --mode docker",\n # 清空缓存后构建项目\n "build:no-cache": "pnpm clean:cache && npm run build",\n "build:test": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode test",\n # 用于生成标准化的git commit message\n "commit": "czg",\n # 运行项目\n "dev": "pnpm vite",\n "preinstall": "npx only-allow pnpm",\n "postinstall": "turbo run stub",\n "lint": "turbo run lint",\n # 执行 eslint 校验,并修复部分问题\n "lint:eslint": "eslint --cache --max-warnings 0 \\"{src,mock}/**/*.{vue,ts,tsx}\\" --fix",\n # 执行 prettier 格式化(该命令会对项目所有代码进行 prettier 格式化,请谨慎执行)\n "lint:prettier": "prettier --write .",\n # 执行 stylelint 格式化\n "lint:stylelint": "stylelint \\"**/*.{vue,css,less,scss}\\" --fix --cache --cache-location node_modules/.cache/stylelint/",\n # 安装git hooks\n "prepare": "husky install",\n # 预览打包后的内容(先打包在进行预览)\n "preview": "npm run build && vite preview",\n # 重新安装依赖\n "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",\n # 运行项目\n "serve": "npm run dev",\n # 对打包结果进行 gzip 测试\n "test:gzip": "npx http-server dist --cors --gzip -c-1",\n # 类型检查\n "type:check": "vue-tsc --noEmit --skipLibCheck"\n},\n
该命令会生成所选择的图标集,提供给图标选择器使用。具体使用方式请查看 图标集生成
该命令会先删除 node_modules
、yarn.lock
、package.lock.json
后再进行依赖重新安装(安装速度会明显变慢)。
接下来你可以修改代码进行业务开发了。我们内建了模拟数据、HMR 实时预览、状态管理、国际化、全局路由等各种实用的功能辅助开发,请阅读其他章节了解更多。
\n.\n├── build # 打包脚本相关\n│ ├── config # 配置文件\n│ ├── generate # 生成器\n│ ├── script # 脚本\n│ └── vite # vite配置\n├── mock # mock文件夹\n├── public # 公共静态资源目录\n├── src # 主目录\n│ ├── api # 接口文件\n│ ├── assets # 资源文件\n│ │ ├── icons # icon sprite 图标文件夹\n│ │ ├── images # 项目存放图片的文件夹\n│ │ └── svg # 项目存放svg图片的文件夹\n│ ├── components # 公共组件\n│ ├── design # 样式文件\n│ ├── directives # 指令\n│ ├── enums # 枚举/常量\n│ ├── hooks # hook\n│ │ ├── component # 组件相关hook\n│ │ ├── core # 基础hook\n│ │ ├── event # 事件相关hook\n│ │ ├── setting # 配置相关hook\n│ │ └── web # web相关hook\n│ ├── layouts # 布局文件\n│ │ ├── default # 默认布局\n│ │ ├── iframe # iframe布局\n│ │ └── page # 页面布局\n│ ├── locales # 多语言\n│ ├── logics # 逻辑\n│ ├── main.ts # 主入口\n│ ├── router # 路由配置\n│ ├── settings # 项目配置\n│ │ ├── componentSetting.ts # 组件配置\n│ │ ├── designSetting.ts # 样式配置\n│ │ ├── encryptionSetting.ts # 加密配置\n│ │ ├── localeSetting.ts # 多语言配置\n│ │ ├── projectSetting.ts # 项目配置\n│ │ └── siteSetting.ts # 站点配置\n│ ├── store # 数据仓库\n│ ├── utils # 工具类\n│ └── views # 页面\n├── types # 类型文件\n└── vite.config.ts # vite配置文件\n\n
Vue-Vben-Admin 是一个基于 Vue3.0、Vite、 Ant-Design-Vue、TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3
、vite
、ts
等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
如需本地运行文档,请拉取代码到本地。
# 拉取代码\ngit clone https://github.com/vbenjs/vue-vben-admin-doc\n\n# 安装依赖\nyarn\n\n# 运行项目\nyarn dev\n
本项目需要一定前端基础知识,请确保掌握 Vue 的基础知识,以便能处理一些常见的问题。建议在开发前先学一下以下内容,提前了解和学习这些知识,会对项目理解非常有帮助:
该版本主要是提供一些 Demo
示例及插件的使用集成方式,主要用于参考。如果对项目不是很熟悉,不建议在此基础上进行开发,请使用下方提供的精简版本。
vue-vben-admin
精简版本。删除了相关示例、无用文件及功能、依赖。可以根据自身需求安装对应的依赖。因为使用的是 vite
,依赖删除不会导致相关组件或者 hook
发出警告。只在需要的时候安装对应的库即可。
如果这些插件对你有帮助,可以给一个 star 支持下
mock
html
模版转换,可以在html
文件内进行书写模版语法.gz
|.br
文件svg sprite
本地开发推荐使用Chrome 最新版
浏览器,不支持Chrome 80
以下版本。
生产环境支持现代浏览器,不支持 IE。
IE | Edge | Firefox | Chrome | Safari |
---|---|---|---|---|
not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
除了自带组件以外,有时我们还需要引入其他外部模块。我们以 ant-design-vue
为例:
安装 ant-design-vue
# 在终端输入下面的命令完成安装\nyarn add ant-design-vue\n
import { createApp } from 'vue';\nimport App from './App.vue';\nimport Antd from 'ant-design-vue';\nconst app = createApp(App);\napp.use(Antd);\napp.mount('#app');\n
<template>\n <Button>text</Button>\n</template>\n\n<script>\n import { defineComponent } from 'vue';\n import { Button } from 'ant-design-vue';\n export default defineComponent({\n components: {\n Button,\n },\n });\n</script>\n
项目菜单配置存放于 src/router/menus 下面
提示
菜单必须和路由匹配才能显示
export interface Menu {\n // 菜单名\n name: string;\n // 菜单图标,如果没有,则会尝试使用route.meta.icon\n icon?: string;\n // 菜单图片,如果同时传递了icon和img,则只会显示img\n img?: string;\n // 菜单路径\n path: string;\n // 是否禁用\n disabled?: boolean;\n // 子菜单\n children?: Menu[];\n // 菜单标签设置\n tag: {\n // 为true则显示小圆点\n dot: boolean;\n // 内容\n content: string';\n // 类型\n type: 'error' | 'primary' | 'warn' | 'success';\n };\n // 是否隐藏菜单\n hideMenu?: boolean;\n}\n
一个菜单文件会被当作一个模块
提示
children 的 path 字段不需要以/
开头
import type { MenuModule } from '/@/router/types';\nimport { t } from '/@/hooks/web/useI18n';\nconst menu: MenuModule = {\n orderNo: 10,\n menu: {\n name: t('routes.dashboard.dashboard'),\n path: '/dashboard',\n\n children: [\n {\n path: 'analysis',\n name: t('routes.dashboard.analysis'),\n },\n {\n path: 'workbench',\n name: t('routes.dashboard.workbench'),\n },\n ],\n },\n};\nexport default menu;\n
以上模块会转化成以下结构
[\n path: '/dashboard',\n name: t('routes.dashboard.dashboard'),\n children: [\n {\n path: 'dashboard/analysis',\n name: t('routes.dashboard.analysis'),\n },\n {\n path: 'dashboard/workbench',\n name: t('routes.dashboard.workbench'),\n },\n ],\n]\n
直接在 src/router/routes/modules 内新增一个模块文件即可。
不需要手动引入,放在src/router/routes/modules 内的文件会自动被加载。
在菜单模块内,设置 orderNo
变量,数值越大,排序越靠后
如果前端应用和后端接口服务器没有运行在同一个主机上,你需要在开发环境下将接口请求代理到接口服务器。
如果是同一个主机,可以直接请求具体的接口地址。
开发环境时候,接口地址在项目根目录下
.env.development 文件配置
# vite 本地跨域代理\nVITE_PROXY=[["/basic-api","http://localhost:3000"]]\n# 接口地址\nVITE_GLOB_API_URL=/api\n
TIP
TIP
v3.0.0开始,作者重构了vite.config.ts,新版本不再支持VITE_PROXY环境变量。
如果你在 src/api/
下面的接口为下方代码,且 .env.development 文件配置如下注释,则在控制台看到的地址为 http://localhost:3100/basic-api/login
。
由于 /basic-api
匹配到了设置的 VITE_PROXY
,所以上方实际是请求 http://localhost:3000/login,这样同时也解决了跨域问题。(3100为项目端口号,http://localhost:3000为PROXY代理的目标地址)
// .env.development\n// VITE_PROXY=[["/basic-api","http://localhost:3000"]]\n// VITE_GLOB_API_URL=/basic-api\n\nenum Api {\n Login = '/login',\n}\n\n/**\n * @description: 用户登陆\n */\nexport function loginApi(params: LoginParams) {\n return http.request<LoginResultModel>({\n url: Api.Login,\n method: 'POST',\n params,\n });\n}\n
如果没有跨域问题,可以直接忽略 VITE_PROXY 配置,直接将接口地址设置在 VITE_GLOB_API_URL
# 例如接口地址为 http://localhost:3000 则\nVITE_GLOB_API_URL=http://localhost:3000\n
如果有跨域问题,将 VITE_GLOB_API_URL 设置为跟 VITE_PROXY 内其中一个数组的第一个项一致的值即可。
下方的接口地址设置为 /basic-api
,当请求发出的时候会经过 Vite 的 proxy 代理,匹配到了我们设置的 VITE_PROXY 规则,将 /basic-api
转化为 http://localhost:3000
进行请求
# 例如接口地址为 http://localhost:3000 则\nVITE_PROXY=[["/basic-api","http://localhost:3000"]]\n# 接口地址\nVITE_GLOB_API_URL=/basic-api\n
在 vite.config.ts
配置文件中,提供了 server 的 proxy 功能,用于代理 API 请求。
server: {\n proxy: {\n "/basic-api":{\n target: 'http://localhost:3000',\n changeOrigin: true,\n ws: true,\n rewrite: (path) => path.replace(new RegExp(`^/basic-api`), ''),\n }\n },\n},\n
注意
从浏览器控制台的 Network 看,请求是 http://localhost:3000/basic-api/xxx
,这是因为 proxy 配置不会改变本地请求的 url。
生产环境接口地址在项目根目录下 .env.production 文件配置。
生产环境接口地址值需要修改 VITE_GLOB_API_URL,如果出现跨域问题,可以使用 nginx 或者后台开启 cors 进行处理
打包后如何进行地址修改?
VITE_GLOB_* 开头的变量会在打包的时候注入 _app.config.js 文件内。
在 dist/_app.config.js 修改相应的接口地址后刷新页面即可,不需要在根据不同环境打包多次,一次打包可以用于多个不同接口环境的部署。
在 vue-vben-admin 中:
接口统一存放于 src/api/ 下面管理
以登陆接口为例:
在 src/api/ 内新建模块文件,其中参数与返回值最好定义一下类型,方便校验。虽然麻烦,但是后续维护字段很方便。
TIP
类型定义文件可以抽取出去统一管理,具体参考项目
import { defHttp } from '/@/utils/http/axios';\nimport { LoginParams, LoginResultModel } from './model/userModel';\n\nenum Api {\n Login = '/login',\n}\n\nexport function loginApi(params: LoginParams) {\n return defHttp.request<LoginResultModel>({\n url: Api.Login,\n method: 'POST',\n params,\n });\n}\n
axios 请求封装存放于 src/utils/http/axios 文件夹内部
除 index.ts
文件内容需要根据项目自行修改外,其余文件无需修改
\n├── Axios.ts // axios实例\n├── axiosCancel.ts // axiosCancel实例,取消重复请求\n├── axiosTransform.ts // 数据转换类\n├── checkStatus.ts // 返回状态值校验\n├── index.ts // 接口返回统一处理\n\n
const axios = new VAxios({\n // 认证方案,例如: Bearer\n // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes\n authenticationScheme: '',\n // 接口超时时间 单位毫秒\n timeout: 10 * 1000,\n // 接口可能会有通用的地址部分,可以统一抽取出来\n prefixUrl: prefix,\n headers: { 'Content-Type': ContentTypeEnum.JSON },\n // 数据处理方式,见下方说明\n transform,\n // 配置项,下面的选项都可以在独立的接口请求中覆盖\n requestOptions: {\n // 默认将prefix 添加到url\n joinPrefix: true,\n // 是否返回原生响应头 比如:需要获取响应头时使用该属性\n isReturnNativeResponse: false,\n // 需要对返回数据进行处理\n isTransformRequestResult: true,\n // post请求的时候添加参数到url\n joinParamsToUrl: false,\n // 格式化提交参数时间\n formatDate: true,\n // 消息提示类型\n errorMessageMode: 'message',\n // 接口地址\n apiUrl: globSetting.apiUrl,\n // 是否加入时间戳\n joinTime: true,\n // 忽略重复请求\n ignoreCancelToken: true,\n },\n});\n
transform 数据处理说明
类型定义,见 axiosTransform.ts 文件
export abstract class AxiosTransform {\n /**\n * @description: 请求之前处理配置\n */\n beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;\n\n /**\n * @description: 请求成功处理\n */\n transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;\n\n /**\n * @description: 请求失败处理\n */\n requestCatch?: (e: Error) => Promise<any>;\n\n /**\n * @description: 请求之前的拦截器\n */\n requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;\n\n /**\n * @description: 请求之后的拦截器\n */\n responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;\n\n /**\n * @description: 请求之前的拦截器错误处理\n */\n requestInterceptorsCatch?: (error: Error) => void;\n\n /**\n * @description: 请求之后的拦截器错误处理\n */\n responseInterceptorsCatch?: (error: Error) => void;\n}\n\n\n
项目默认 transform 处理逻辑,可以根据各自项目进行处理。一般需要更改的部分为下方代码,见代码注释说明
/**\n * @description: 数据处理,方便区分多种处理方式\n */\nconst transform: AxiosTransform = {\n /**\n * @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误\n */\n transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {\n const { t } = useI18n();\n const { isTransformResponse, isReturnNativeResponse } = options;\n // 是否返回原生响应头 比如:需要获取响应头时使用该属性\n if (isReturnNativeResponse) {\n return res;\n }\n // 不进行任何处理,直接返回\n // 用于页面代码可能需要直接获取code,data,message这些信息时开启\n if (!isTransformResponse) {\n return res.data;\n }\n // 错误的时候返回\n\n const { data } = res;\n if (!data) {\n // return '[HTTP] Request has no return value';\n throw new Error(t('sys.api.apiRequestFailed'));\n }\n // 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式\n const { code, result, message } = data;\n\n // 这里逻辑可以根据项目进行修改\n const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;\n if (hasSuccess) {\n return result;\n }\n\n // 在此处根据自己项目的实际情况对不同的code执行不同的操作\n // 如果不希望中断当前请求,请return数据,否则直接抛出异常即可\n let timeoutMsg = '';\n switch (code) {\n case ResultEnum.TIMEOUT:\n timeoutMsg = t('sys.api.timeoutMessage');\n default:\n if (message) {\n timeoutMsg = message;\n }\n }\n\n // errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误\n // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示\n if (options.errorMessageMode === 'modal') {\n createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg });\n } else if (options.errorMessageMode === 'message') {\n createMessage.error(timeoutMsg);\n }\n\n throw new Error(timeoutMsg || t('sys.api.apiRequestFailed'));\n },\n\n // 请求之前处理config\n beforeRequestHook: (config, options) => {\n const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;\n\n if (joinPrefix) {\n config.url = `${urlPrefix}${config.url}`;\n }\n\n if (apiUrl && isString(apiUrl)) {\n config.url = `${apiUrl}${config.url}`;\n }\n const params = config.params || {};\n if (config.method?.toUpperCase() === RequestEnum.GET) {\n if (!isString(params)) {\n // 给 get 请求加上时间戳参数,避免从缓存中拿数据。\n config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));\n } else {\n // 兼容restful风格\n config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;\n config.params = undefined;\n }\n } else {\n if (!isString(params)) {\n formatDate && formatRequestDate(params);\n config.data = params;\n config.params = undefined;\n if (joinParamsToUrl) {\n config.url = setObjToUrlParams(config.url as string, config.data);\n }\n } else {\n // 兼容restful风格\n config.url = config.url + params;\n config.params = undefined;\n }\n }\n return config;\n },\n\n /**\n * @description: 请求拦截器处理\n */\n requestInterceptors: (config, options) => {\n // 请求之前处理config\n const token = getToken();\n if (token) {\n // jwt token\n config.headers.Authorization = options.authenticationScheme\n ? `${options.authenticationScheme} ${token}`\n : token;\n }\n return config;\n },\n\n /**\n * @description: 响应拦截器处理\n */\n responseInterceptors: (res: AxiosResponse<any>) => {\n return res;\n },\n\n /**\n * @description: 响应错误处理\n */\n responseInterceptorsCatch: (error: any) => {\n const { t } = useI18n();\n const errorLogStore = useErrorLogStoreWithOut();\n errorLogStore.addAjaxErrorInfo(error);\n const { response, code, message, config } = error || {};\n const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';\n const msg: string = response?.data?.error?.message ?? '';\n const err: string = error?.toString?.() ?? '';\n let errMessage = '';\n\n try {\n if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {\n errMessage = t('sys.api.apiTimeoutMessage');\n }\n if (err?.includes('Network Error')) {\n errMessage = t('sys.api.networkExceptionMsg');\n }\n\n if (errMessage) {\n if (errorMessageMode === 'modal') {\n createErrorModal({ title: t('sys.api.errorTip'), content: errMessage });\n } else if (errorMessageMode === 'message') {\n createMessage.error(errMessage);\n }\n return Promise.reject(error);\n }\n } catch (error) {\n throw new Error(error);\n }\n\n checkStatus(error?.response?.status, msg, errorMessageMode);\n return Promise.reject(error);\n },\n};\n
项目接口默认为 Json 参数格式,即 headers: { 'Content-Type': ContentTypeEnum.JSON }
,
如果需要更改为 form-data
格式,更改 headers 的 'Content-Type
为 ContentTypeEnum.FORM_URLENCODED
即可
当项目中需要用到多个接口地址时, 可以在 src/utils/http/axios/index.ts 导出多个 axios 实例
// 目前只导出一个默认实例,接口地址对应的是环境变量中的 VITE_GLOB_API_URL 接口地址\nexport const defHttp = createAxios();\n\n// 需要有其他接口地址的可以在后面添加\n\n// other api url\nexport const otherHttp = createAxios({\n requestOptions: {\n apiUrl: 'xxx',\n },\n});\n
如果不需要 url 上面默认携带的时间戳参数 ?_t=xxx
const axios = new VAxios({\n requestOptions: {\n // 是否加入时间戳\n joinTime: false,\n },\n});\n
Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发进程所阻塞。
本项目使用 vite-plugin-mock 来进行 mock 数据处理。项目内 mock 服务分本地和线上。
本地 mock 采用 Node.js 中间件进行参数拦截(不采用 mock.js 的原因是本地开发看不到请求参数和响应结果)。
如果你想添加 mock 数据,只要在根目录下找到 mock 文件,添加对应的接口,对其进行拦截和模拟数据。
在 mock 文件夹内新建文件
TIP
文件新增后会自动更新,不需要手动重启,可以在代码控制台查看日志信息 mock 文件夹内会自动注册,排除以_开头的文件夹及文件
例:
import { MockMethod } from 'vite-plugin-mock';\nimport { resultPageSuccess } from '../_util';\n\nconst demoList = (() => {\n const result: any[] = [];\n for (let index = 0; index < 60; index++) {\n result.push({\n id: `${index}`,\n beginTime: '@datetime',\n endTime: '@datetime',\n address: '@city()',\n name: '@cname()',\n 'no|100000-10000000': 100000,\n 'status|1': ['正常', '启用', '停用'],\n });\n }\n return result;\n})();\n\nexport default [\n {\n url: '/api/table/getDemoList',\n timeout: 1000,\n method: 'get',\n response: ({ query }) => {\n const { page = 1, pageSize = 20 } = query;\n return resultPageSuccess(page, pageSize, demoList);\n },\n },\n] as MockMethod[];\n
TIP
mock 的值可以直接使用 mockjs 的语法。
{\n url: string; // mock 接口地址\n method?: MethodType; // 请求方式\n timeout?: number; // 延时时间\n statusCode: number; // 响应状态码\n response: ((opt: { // 响应结果\n body: any;\n query: any;\n }) => any) | object;\n}\n
GET 接口: ({ query }) => { }
POST 接口: ({ body }) => { }
可在 代码 中查看
TIP
util 只作为服务处理结果数据使用。可以不用,如需使用可自行封装,需要将对应的字段改为接口的返回结构
在 src/api
下面,如果接口匹配到 mock,则会优先使用 mock 进行响应
import { defHttp } from '/@/utils/http/axios';\nimport { LoginParams, LoginResultModel } from './model/userModel';\n\nenum Api {\n Login = '/login',\n}\n\n/**\n * @description: user login api\n */\nexport function loginApi(params: LoginParams) {\n return defHttp.request<LoginResultModel>(\n {\n url: Api.Login,\n method: 'POST',\n params,\n },\n {\n errorMessageMode: 'modal',\n }\n );\n}\n// 会匹配到上方的\nexport default [\n {\n url: '/api/login',\n timeout: 1000,\n method: 'POST',\n response: ({ body }) => {\n return resultPageSuccess({});\n },\n },\n] as MockMethod[];\n
当后台接口已经开发完成,只需要将相应的 mock 函数去掉即可。
以上方接口为例,假如后台接口 login 已经开发完成,则只需要删除/注释掉下方代码即可
export default [\n {\n url: '/api/login',\n timeout: 1000,\n method: 'POST',\n response: ({ body }) => {\n return resultPageSuccess({});\n },\n },\n] as MockMethod[];\n
由于该项目是一个展示类项目,线上也是用 mock 数据,所以在打包后同时也集成了 mock。通常项目线上一般为正式接口。
项目线上 mock 采用的是 mockjs 进行 mock 数据模拟。
注意
线上开启 mock 只适用于一些简单的示例网站及预览网站。一定不要在正式的生产环境开启!!!
VITE_USE_MOCK
的值为 trueVITE_USE_MOCK = true;\n
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';\n\nconst modules = import.meta.globEager('./**/*.ts');\n\nconst mockModules: any[] = [];\nObject.keys(modules).forEach((key) => {\n if (key.includes('/_')) {\n return;\n }\n mockModules.push(...modules[key].default);\n});\n\nexport function setupProdMockServer() {\n createProdMockServer(mockModules);\n}\n
import { viteMockServe } from 'vite-plugin-mock';\n\nexport function configMockPlugin(isBuild: boolean) {\n return viteMockServe({\n injectCode: `\n import { setupProdMockServer } from '../mock/_createProductionServer';\n\n setupProdMockServer();\n `,\n });\n}\n
为什么通过插件注入代码而不是直接在 main.ts 内插入
在插件内通过 injectCode
插入代码,方便控制 mockjs 是否被打包到最终代码内。如果在 main.ts 内判断,如果关闭了 mock 功能,mockjs 也会打包到构建文件内,这样会增加打包体积。
到这里线上 mock 就配置完成了。线上与本地差异不大,比较大的区别是线上在控制台内看不到接口请求日志。
',96);p.render=function(a,t,p,e,c,l){return n(),s("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/guide_mock.md.4205a506.lean.js b/assets/guide_mock.md.4205a506.lean.js new file mode 100644 index 00000000..b6bea3a0 --- /dev/null +++ b/assets/guide_mock.md.4205a506.lean.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.8cddb23b.js";const t='{"title":"数据 mock&联调","description":"","frontmatter":{},"headers":[{"level":2,"title":"开发环境","slug":"开发环境"},{"level":3,"title":"配置","slug":"配置"},{"level":3,"title":"跨域处理","slug":"跨域处理"},{"level":3,"title":"没有跨域时的配置","slug":"没有跨域时的配置"},{"level":3,"title":"跨域原理解析","slug":"跨域原理解析"},{"level":2,"title":"生产环境","slug":"生产环境"},{"level":2,"title":"接口请求","slug":"接口请求"},{"level":2,"title":"axios 配置","slug":"axios-配置"},{"level":3,"title":"index.ts 配置说明","slug":"index-ts-配置说明"},{"level":3,"title":"更改参数格式","slug":"更改参数格式"},{"level":3,"title":"多个接口地址","slug":"多个接口地址"},{"level":3,"title":"删除请求 URL 携带的时间戳参数","slug":"删除请求-url-携带的时间戳参数"},{"level":2,"title":"Mock 服务","slug":"mock-服务"},{"level":3,"title":"本地 Mock","slug":"本地-mock"},{"level":3,"title":"线上 mock","slug":"线上-mock"}],"relativePath":"guide/mock.md","lastUpdated":1697592578593}',p={},o=a('',96);p.render=function(a,t,p,e,c,l){return n(),s("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/guide_router.md.31ce5cd8.js b/assets/guide_router.md.31ce5cd8.js new file mode 100644 index 00000000..4e4c2d84 --- /dev/null +++ b/assets/guide_router.md.31ce5cd8.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.8cddb23b.js";const t='{"title":"路由","description":"","frontmatter":{},"headers":[{"level":2,"title":"配置","slug":"配置"},{"level":3,"title":"模块说明","slug":"模块说明"},{"level":3,"title":"多级路由","slug":"多级路由"},{"level":3,"title":"Meta 配置说明","slug":"meta-配置说明"},{"level":3,"title":"外部页面嵌套","slug":"外部页面嵌套"},{"level":3,"title":"外链","slug":"外链"},{"level":3,"title":"动态路由Tab自动关闭功能","slug":"动态路由tab自动关闭功能"},{"level":2,"title":"图标","slug":"图标"},{"level":2,"title":"新增路由","slug":"新增路由"},{"level":3,"title":"如何新增一个路由模块","slug":"如何新增一个路由模块"},{"level":3,"title":"验证","slug":"验证"},{"level":2,"title":"路由刷新","slug":"路由刷新"},{"level":3,"title":"实现","slug":"实现"},{"level":3,"title":"Redirect","slug":"redirect"},{"level":2,"title":"页面跳转","slug":"页面跳转"},{"level":3,"title":"方式","slug":"方式"},{"level":2,"title":"多标签页","slug":"多标签页"},{"level":3,"title":"如何开启页面缓存","slug":"如何开启页面缓存"},{"level":3,"title":"如何让某个页面不缓存","slug":"如何让某个页面不缓存"},{"level":2,"title":"如何更改首页路由","slug":"如何更改首页路由"}],"relativePath":"guide/router.md","lastUpdated":1697592578593}',p={},o=a('项目路由配置存放于 src/router/routes 下面。 src/router/routes/modules用于存放路由模块,在该目录下的文件会自动注册。
在 src/router/routes/modules 内的 .ts
文件会被视为一个路由模块。
一个路由模块包含以下结构
import type { AppRouteModule } from '/@/router/types';\n\nimport { LAYOUT } from '/@/router/constant';\nimport { t } from '/@/hooks/web/useI18n';\n\nconst dashboard: AppRouteModule = {\n path: '/dashboard',\n name: 'Dashboard',\n component: LAYOUT,\n redirect: '/dashboard/analysis',\n meta: {\n icon: 'ion:grid-outline',\n title: t('routes.dashboard.dashboard'),\n },\n children: [\n {\n path: 'analysis',\n name: 'Analysis',\n component: () => import('/@/views/dashboard/analysis/index.vue'),\n meta: {\n affix: true,\n title: t('routes.dashboard.analysis'),\n },\n },\n {\n path: 'workbench',\n name: 'Workbench',\n component: () => import('/@/views/dashboard/workbench/index.vue'),\n meta: {\n title: t('routes.dashboard.workbench'),\n },\n },\n ],\n};\nexport default dashboard;\n
注意事项
name
不能重复/
,其余子路由都不要以/
开头示例
import type { AppRouteModule } from '/@/router/types';\nimport { getParentLayout, LAYOUT } from '/@/router/constant';\nimport { t } from '/@/hooks/web/useI18n';\nconst permission: AppRouteModule = {\n path: '/level',\n name: 'Level',\n component: LAYOUT,\n redirect: '/level/menu1/menu1-1/menu1-1-1',\n meta: {\n icon: 'ion:menu-outline',\n title: t('routes.demo.level.level'),\n },\n\n children: [\n {\n path: 'tabs/:id', \n name: 'TabsParams',\n component: getParentLayout('TabsParams'),\n meta: {\n carryParam: true,\n hidePathForChildren: true, // 本级path将会在子级菜单中合成完整path时会忽略这一层级\n },\n children: [\n path: 'tabs/id1', // 其上级有标记hidePathForChildren,所以本级在生成菜单时最终的path为 /level/tabs/id1\n name: 'TabsParams',\n component: getParentLayout('TabsParams'),\n meta: {\n carryParam: true,\n ignoreRoute: true, // 本路由仅用于菜单生成,不会在实际的路由表中出现\n },\n ]\n },\n {\n path: 'menu1',\n name: 'Menu1Demo',\n component: getParentLayout('Menu1Demo'),\n meta: {\n title: 'Menu1',\n },\n redirect: '/level/menu1/menu1-1/menu1-1-1',\n children: [\n {\n path: 'menu1-1',\n name: 'Menu11Demo',\n component: getParentLayout('Menu11Demo'),\n meta: {\n title: 'Menu1-1',\n },\n redirect: '/level/menu1/menu1-1/menu1-1-1',\n children: [\n {\n path: 'menu1-1-1',\n name: 'Menu111Demo',\n component: () => import('/@/views/demo/level/Menu111.vue'),\n meta: {\n title: 'Menu111',\n },\n },\n ],\n },\n ],\n },\n ],\n};\n\nexport default permission;\n
export interface RouteMeta {\n // 路由title 一般必填\n title: string;\n // 动态路由可打开Tab页数\n dynamicLevel?: number;\n // 动态路由的实际Path, 即去除路由的动态部分;\n realPath?: string;\n // 是否忽略权限,只在权限模式为Role的时候有效\n ignoreAuth?: boolean;\n // 可以访问的角色,只在权限模式为Role的时候有效\n roles?: RoleEnum[];\n // 是否忽略KeepAlive缓存\n ignoreKeepAlive?: boolean;\n // 是否固定标签\n affix?: boolean;\n // 图标,也是菜单图标\n icon?: string;\n // 内嵌iframe的地址\n frameSrc?: string;\n // 指定该路由切换的动画名\n transitionName?: string;\n // 隐藏该路由在面包屑上面的显示\n hideBreadcrumb?: boolean;\n // 如果该路由会携带参数,且需要在tab页上面显示。则需要设置为true\n carryParam?: boolean;\n // 隐藏所有子菜单\n hideChildrenInMenu?: boolean;\n // 当前激活的菜单。用于配置详情页时左侧激活的菜单路径\n currentActiveMenu?: string;\n // 当前路由不再标签页显示\n hideTab?: boolean;\n // 当前路由不再菜单显示\n hideMenu?: boolean;\n // 菜单排序,只对第一级有效\n orderNo?: number;\n // 忽略路由。用于在ROUTE_MAPPING以及BACK权限模式下,生成对应的菜单而忽略路由。2.5.3以上版本有效\n ignoreRoute?: boolean;\n // 是否在子级菜单的完整path中忽略本级path。2.5.3以上版本有效\n hidePathForChildren?: boolean;\n}\n
只需要将 frameSrc
设置为需要跳转的地址即可
const IFrame = () => import('/@/views/sys/iframe/FrameBlank.vue');\n{\n path: 'doc',\n name: 'Doc',\n component: IFrame,\n meta: {\n frameSrc: 'https://vvbin.cn/doc-next/',\n title: t('routes.demo.iframe.doc'),\n },\n},\n
只需要将 path
设置为需要跳转的HTTP 地址即可
{\n path: 'https://vvbin.cn/doc-next/',\n name: 'DocExternal',\n component: IFrame,\n meta: {\n title: t('routes.demo.iframe.docExternal'),\n },\n}\n
若需要开启该功能,需要在动态路由的meta
中设置如下两个参数:
dynamicLevel
最大能打开的Tab标签页数realPath
动态路由实际路径(考虑到动态路由有时候可能存在N层的情况, 例:/:id/:subId/:...
), 为了减少计算开销, 使用配置方式事先规定好路由的实际路径(注意: 该参数若不设置,将无法使用该功能){\n path: 'detail/:id',\n name: 'TabDetail',\n component: () => import('/@/views/demo/feat/tabs/TabDetail.vue'),\n meta: {\n currentActiveMenu: '/feat/tabs',\n title: t('routes.demo.feat.tabDetail'),\n hideMenu: true,\n dynamicLevel: 3,\n realPath: '/feat/tabs/detail',\n },\n}\n
这里的 icon
配置,会同步到 菜单(icon 的值可以查看此处)。
示例,新增 test.ts 文件
import type { AppRouteModule } from '/@/router/types';\nimport { LAYOUT } from '/@/router/constant';\nimport { t } from '/@/hooks/web/useI18n';\n\nconst dashboard: AppRouteModule = {\n path: '/about',\n name: 'About',\n component: LAYOUT,\n redirect: '/about/index',\n meta: {\n icon: 'simple-icons:about-dot-me',\n title: t('routes.dashboard.about'),\n },\n children: [\n {\n path: 'index',\n name: 'AboutPage',\n component: () => import('/@/views/sys/about/index.vue'),\n meta: {\n title: t('routes.dashboard.about'),\n icon: 'simple-icons:about-dot-me',\n },\n },\n ],\n};\n\nexport default dashboard;\n
此时路由已添加完成,不需要手动引入,放在src/router/routes/modules 内的文件会自动被加载。
访问 ip:端口/about/index 出现对应组件内容即代表成功
项目中采用的是重定向方式
import { useRedo } from '/@/hooks/web/usePage';\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n setup() {\n const redo = useRedo();\n // 执行刷新\n redo();\n return {};\n },\n});\n
src/views/sys/redirect/index.vue
import { defineComponent, unref } from 'vue';\nimport { useRouter } from 'vue-router';\nexport default defineComponent({\n name: 'Redirect',\n setup() {\n const { currentRoute, replace } = useRouter();\n const { params, query } = unref(currentRoute);\n const { path } = params;\n const _path = Array.isArray(path) ? path.join('/') : path;\n replace({\n path: '/' + _path,\n query,\n });\n return {};\n },\n});\n
页面跳转建议采用项目提供的 useGo
import { useGo } from '/@/hooks/web/usePage';\nimport { defineComponent } from 'vue';\nexport default defineComponent({\n setup() {\n const go = useGo();\n\n // 执行刷新\n go();\n go(PageEnum.Home);\n return {};\n },\n});\n
标签页使用的是 keep-alive
和 router-view
实现,实现切换 tab 后还能保存切换之前的状态。
开启缓存有 3 个条件
openKeepAlive
设置为 true
name
,且不能重复name
,与路由设置的 name
保持一致 {\n ...,\n // name\n name: 'Login',\n // 对应组件组件的name\n component: () => import('/@/views/sys/login/index.vue'),\n ...\n },\n\n // /@/views/sys/login/index.vue\n export default defineComponent({\n // 需要和路由的name一致\n name:"Login"\n });\n
注意
keep-alive 生效的前提是:需要将路由的 name
属性及对应的页面的 name
设置成一样。因为:
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
可在 router.meta 下配置
可以将 ignoreKeepAlive
配置成 true
即可关闭缓存。
export interface RouteMeta {\n // 是否忽略KeepAlive缓存\n ignoreKeepAlive?: boolean;\n}\n
首页路由指的是应用程序中的默认路由,当不输入其他任何路由时,会自动重定向到该路由下,并且该路由在Tab上是固定的,即使设置affix: false
也不允许关闭
例:首页路由配置的是/dashboard/analysis
,那么当直接访问 http://localhost:3100/
会自动跳转到http://localhost:3100/#/dashboard/analysis
上(用户已登录的情况下)
可以将pageEnum.ts
中的BASE_HOME
更改为需要你想设置的首页即可
export enum PageEnum {\n // basic home path\n // 更改此处即可\n BASE_HOME = '/dashboard',\n}\n\n
用于修改项目的配色、布局、缓存、多语言、组件默认配置
项目的环境变量配置位于项目根目录下的 .env、.env.development、.env.production
具体可以参考 Vite 文档
.env # 在所有的环境中被载入\n.env.local # 在所有的环境中被载入,但会被 git 忽略\n.env.[mode] # 只在指定的模式中被载入\n.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略\n\n
温馨提醒
VITE_
开头的变量会被嵌入到客户端侧的包中,你可以在项目代码中这样访问它们:console.log(import.meta.env.VITE_PROT);\n
VITE_GLOB_*
开头的的变量,在打包的时候,会被加入_app.config.js配置文件当中.所有环境适用
# 端口号\nVITE_PORT=3100\n# 网站标题\nVITE_GLOB_APP_TITLE=vben admin\n# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符\nVITE_GLOB_APP_SHORT_NAME=vben_admin\n
开发环境适用
# 是否开启mock数据,关闭时需要自行对接后台接口\nVITE_USE_MOCK=true\n# 资源公共路径,需要以 /开头和结尾\nVITE_PUBLIC_PATH=/\n# 是否删除Console.log\nVITE_DROP_CONSOLE=false\n# 本地开发代理,可以解决跨域及多地址代理\n# 如果接口地址匹配到,则会转发到http://localhost:3000,防止本地出现跨域问题\n# 可以有多个,注意多个不能换行,否则代理将会失效\nVITE_PROXY=[["/api","http://localhost:3000"],["api1","http://localhost:3001"],["/upload","http://localhost:3001/upload"]]\n\n::: tip\nv3.0.0开始,作者重构了vite.config.ts,新版本不再支持VITE_PROXY环境变量。\n:::\n\n# 接口地址\n# 如果没有跨域问题,直接在这里配置即可\nVITE_GLOB_API_URL=/api\n# 文件上传接口 可选\nVITE_GLOB_UPLOAD_URL=/upload\n# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换\nVITE_GLOB_API_URL_PREFIX=\n
注意
这里配置的 VITE_PROXY
以及 VITE_GLOB_API_URL
, /api 需要是唯一的,不要和接口有的名字冲突
如果你的接口是 http://localhost:3000/api
之类的,请考虑将 VITE_GLOB_API_URL=/xxxx
换成别的名字
生产环境适用
# 是否开启mock\nVITE_USE_MOCK=true\n# 接口地址 可以由nginx做转发或者直接写实际地址\nVITE_GLOB_API_URL=/api\n# 文件上传地址 可以由nginx做转发或者直接写实际地址\nVITE_GLOB_UPLOAD_URL=/upload\n# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换\nVITE_GLOB_API_URL_PREFIX=\n# 是否删除Console.log\nVITE_DROP_CONSOLE=true\n# 资源公共路径,需要以 / 开头和结尾\nVITE_PUBLIC_PATH=/\n# 打包是否输出gz|br文件\n# 可选: gzip | brotli | none\n# 也可以有多个, 例如 ‘gzip’|'brotli',这样会同时生成 .gz和.br文件\nVITE_BUILD_COMPRESS = 'gzip'\n# 打包是否压缩图片\nVITE_USE_IMAGEMIN = false\n# 打包是否开启pwa功能\nVITE_USE_PWA = false\n# 是否兼容旧版浏览器。开启后打包时间会慢一倍左右。会多打出旧浏览器兼容包,且会根据浏览器兼容性自动使用相应的版本\nVITE_LEGACY = false\n
当执行yarn build
构建项目之后,会自动生成 _app.config.js
文件并插入 index.html
。
注意: 开发环境不会生成
// _app.config.js\n// 变量名命名规则 __PRODUCTION__xxx_CONF__ xxx:为.env配置的VITE_GLOB_APP_SHORT_NAME\nwindow.__PRODUCTION__VUE_VBEN_ADMIN__CONF__ = {\n VITE_GLOB_APP_TITLE: 'vben admin',\n VITE_GLOB_APP_SHORT_NAME: 'vue_vben_admin',\n VITE_GLOB_API_URL: '/app',\n VITE_GLOB_API_URL_PREFIX: '/',\n VITE_GLOB_UPLOAD_URL: '/upload',\n};\n
_app.config.js
用于项目在打包后,需要动态修改配置的需求,如接口地址。不用重新进行打包,可在打包后修改 /dist/_app.config.js
内的变量,刷新即可更新代码内的局部变量。
想要获取 _app.config.js
内的变量,可以使用 src/hooks/setting/index.ts 提供的函数来进行获取
首先在 .env
或者对应的开发环境配置文件内,新增需要可动态配置的变量,需要以 VITE_GLOB_
开头
VITE_GLOB_
开头的变量会自动加入环境变量,通过在 types/config.d.ts
内修改 GlobEnvConfig
和 GlobConfig
两个环境变量的值来定义新添加的类型
useGlobSetting 函数中添加刚新增的返回值即可
const {\n VITE_GLOB_APP_TITLE,\n VITE_GLOB_API_URL,\n VITE_GLOB_APP_SHORT_NAME,\n VITE_GLOB_API_URL_PREFIX,\n VITE_GLOB_UPLOAD_URL,\n} = ENV;\n\nexport const useGlobSetting = (): SettingWrap => {\n // Take global configuration\n const glob: Readonly<GlobConfig> = {\n title: VITE_GLOB_APP_TITLE,\n apiUrl: VITE_GLOB_API_URL,\n shortName: VITE_GLOB_APP_SHORT_NAME,\n urlPrefix: VITE_GLOB_API_URL_PREFIX,\n uploadUrl: VITE_GLOB_UPLOAD_URL\n };\n return glob as Readonly<GlobConfig>;\n};\n\n
WARNING
项目配置文件用于配置项目内展示的内容、布局、文本等效果,存于localStorage
中。如果更改了项目配置,需要手动清空 localStorage
缓存,刷新重新登录后方可生效。
src/settings/projectSetting.ts
// ! 改动后需要清空浏览器缓存\nconst setting: ProjectConfig = {\n // 是否显示SettingButton\n showSettingButton: true,\n\n // 是否显示主题切换按钮\n showDarkModeToggle: true,\n\n // 设置按钮位置 可选项\n // SettingButtonPositionEnum.AUTO: 自动选择\n // SettingButtonPositionEnum.HEADER: 位于头部\n // SettingButtonPositionEnum.FIXED: 固定在右侧\n settingButtonPosition: SettingButtonPositionEnum.AUTO,\n\n // 权限模式,默认前端角色权限模式\n // ROUTE_MAPPING: 前端模式(菜单由路由生成,默认)\n // ROLE:前端模式(菜单路由分开)\n permissionMode: PermissionModeEnum.ROUTE_MAPPING,\n // 权限缓存存放位置。默认存放于localStorage\n permissionCacheType: CacheTypeEnum.LOCAL,\n // 会话超时处理方案\n // SessionTimeoutProcessingEnum.ROUTE_JUMP: 路由跳转到登录页\n // SessionTimeoutProcessingEnum.PAGE_COVERAGE: 生成登录弹窗,覆盖当前页面\n sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP,\n // 项目主题色\n themeColor: primaryColor,\n // 网站灰色模式,用于可能悼念的日期开启\n grayMode: false,\n // 色弱模式\n colorWeak: false,\n // 是否取消菜单,顶部,多标签页显示, 用于可能内嵌在别的系统内\n fullContent: false,\n // 主题内容宽度\n contentMode: ContentEnum.FULL,\n // 是否显示logo\n showLogo: true,\n // 是否显示底部信息 copyright\n showFooter: true,\n // 头部配置\n headerSetting: {\n // 背景色\n bgColor: '#ffffff',\n // 固定头部\n fixed: true,\n // 是否显示顶部\n show: true,\n // 主题\n theme: MenuThemeEnum.LIGHT,\n // 开启锁屏功能\n useLockPage: true,\n // 显示全屏按钮\n showFullScreen: true,\n // 显示文档按钮\n showDoc: true,\n // 显示消息中心按钮\n showNotice: true,\n // 显示菜单搜索按钮\n showSearch: true,\n },\n // 菜单配置\n menuSetting: {\n // 背景色\n bgColor: '#273352',\n // 是否固定住菜单\n fixed: true,\n // 菜单折叠\n collapsed: false,\n // 折叠菜单时候是否显示菜单名\n collapsedShowTitle: false,\n // 是否可拖拽\n canDrag: true,\n // 是否显示\n show: true,\n // 菜单宽度\n menuWidth: 180,\n // 菜单模式\n mode: MenuModeEnum.INLINE,\n // 菜单类型\n type: MenuTypeEnum.SIDEBAR,\n // 菜单主题\n theme: MenuThemeEnum.DARK,\n // 分割菜单\n split: false,\n // 顶部菜单布局\n topMenuAlign: 'start',\n // 折叠触发器的位置\n trigger: TriggerEnum.HEADER,\n // 手风琴模式,只展示一个菜单\n accordion: true,\n // 在路由切换的时候关闭左侧混合菜单展开菜单\n closeMixSidebarOnChange: false,\n // 左侧混合菜单模块切换触发方式\n mixSideTrigger: MixSidebarTriggerEnum.CLICK,\n // 是否固定左侧混合菜单\n mixSideFixed: false,\n },\n // 多标签\n multiTabsSetting: {\n // 刷新后是否保留已经打开的标签页\n cache: false,\n // 开启\n show: true,\n // 开启快速操作\n showQuick: true,\n // 是否可以拖拽\n canDrag: true,\n // 是否显示刷新按钮\n showRedo: true,\n // 是否显示折叠按钮\n showFold: true,\n },\n\n // 动画配置\n transitionSetting: {\n // 是否开启切换动画\n enable: true,\n // 动画名\n basicTransition: RouterTransitionEnum.FADE_SIDE,\n // 是否打开页面切换loading\n openPageLoading: true,\n // 是否打开页面切换顶部进度条\n openNProgress: false,\n },\n\n // 是否开启KeepAlive缓存 开发时候最好关闭,不然每次都需要清除缓存\n openKeepAlive: true,\n // 自动锁屏时间,为0不锁屏。 单位分钟 默认1个小时\n lockTime: 0,\n // 显示面包屑\n showBreadCrumb: true,\n // 显示面包屑图标\n showBreadCrumbIcon: false,\n // 是否使用全局错误捕获\n useErrorHandle: false,\n // 是否开启回到顶部\n useOpenBackTop: true,\n // 是否可以嵌入iframe页面\n canEmbedIFramePage: true,\n // 切换界面的时候是否删除未关闭的message及notify\n closeMessageOnSwitch: true,\n // 切换界面的时候是否取消已经发送但是未响应的http请求。\n // 如果开启,想对单独接口覆盖。可以在单独接口设置\n removeAllHttpPending: true,\n};\n
用于配置缓存内容加密信息,对缓存到浏览器的信息进行 AES 加密
在 /@/settings/encryptionSetting.ts 内可以配置 localStorage
及 sessionStorage
缓存信息
前提: 使用项目自带的缓存工具类 /@/utils/cache 来进行缓存操作
import { isDevMode } from '/@/utils/env';\n\n// 缓存默认过期时间\nexport const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;\n\n// 开启缓存加密后,加密密钥。采用aes加密\nexport const cacheCipher = {\n key: '_11111000001111@',\n iv: '@11111000001111_',\n};\n\n// 是否加密缓存,默认生产环境加密\nexport const enableStorageEncryption = !isDevMode();\n
用于配置多语言信息
在 src/settings/localeSetting.ts 内配置
export const LOCALE: { [key: string]: LocaleType } = {\n ZH_CN: 'zh_CN',\n EN_US: 'en',\n};\n\nexport const localeSetting: LocaleSetting = {\n // 是否显示语言选择器\n showPicker: true,\n // 当前语言\n locale: LOCALE.ZH_CN,\n // 默认语言\n fallback: LOCALE.ZH_CN,\n // 允许的语言\n availableLocales: [LOCALE.ZH_CN, LOCALE.EN_US],\n};\n\n// 语言列表\nexport const localeList: DropMenu[] = [\n {\n text: '简体中文',\n event: LOCALE.ZH_CN,\n },\n {\n text: 'English',\n event: LOCALE.EN_US,\n },\n];\n
默认全局主题色配置位于 build/config/glob/themeConfig.ts 内
只需要修改 primaryColor 为您需要的配色,然后重新执行 yarn serve
即可
/**\n * less global variable\n */\nexport const primaryColor = '#0960bd';\n
用于修改项目内组件 class 的统一前缀
export const prefixCls = 'vben';\n
@namespace: vben;\n
在 css 内
<style lang="less" scoped>\n /* namespace已经全局注入,不需要额外再引入 */\n @prefix-cls: ~'@{namespace}-app-logo';\n\n .@{prefix-cls} {\n width: 100%;\n }\n</style>\n
在 vue/ts 内
import { useDesign } from '/@/hooks/web/useDesign';\n\nconst { prefixCls } = useDesign('app-logo');\n\n// prefixCls => vben-app-logo\n
用于预设一些颜色数组
在 src/settings/designSetting.ts 内配置
// app主题色预设\nexport const APP_PRESET_COLOR_LIST: string[] = [\n '#0960bd',\n '#0084f4',\n '#009688',\n '#536dfe',\n '#ff5c93',\n '#ee4f12',\n '#0096c7',\n '#9c27b0',\n '#ff9800',\n];\n\n// 顶部背景色预设\nexport const HEADER_PRESET_BG_COLOR_LIST: string[] = [\n '#ffffff',\n '#009688',\n '#5172DC',\n '#1E9FFF',\n '#018ffb',\n '#409eff',\n '#4e73df',\n '#e74c3c',\n '#24292e',\n '#394664',\n '#001529',\n '#383f45',\n];\n\n// 左侧菜单背景色预设\nexport const SIDE_BAR_BG_COLOR_LIST: string[] = [\n '#001529',\n '#273352',\n '#ffffff',\n '#191b24',\n '#191a23',\n '#304156',\n '#001628',\n '#28333E',\n '#344058',\n '#383f45',\n];\n
在 src/settings/componentSetting.ts 内配置
// 用于配置某些组件的常规配置,而无需修改组件\nimport type { SorterResult } from '../components/Table';\n\nexport default {\n // 表格配置\n table: {\n // 表格接口请求通用配置,可在组件prop覆盖\n // 支持 xxx.xxx.xxx格式\n fetchSetting: {\n // 传给后台的当前页字段\n pageField: 'page',\n // 传给后台的每页显示多少条的字段\n sizeField: 'pageSize',\n // 接口返回表格数据的字段\n listField: 'items',\n // 接口返回表格总数的字段\n totalField: 'total',\n },\n // 可选的分页选项\n pageSizeOptions: ['10', '50', '80', '100'],\n //默认每页显示多少条\n defaultPageSize: 10,\n // 默认排序方法\n defaultSortFn: (sortInfo: SorterResult) => {\n const { field, order } = sortInfo;\n return {\n // 排序字段\n field,\n // 排序方式 asc/desc\n order,\n };\n },\n // 自定义过滤方法\n defaultFilterFn: (data: Partial<Recordable<string[]>>) => {\n return data;\n },\n },\n // 滚动组件配置\n scrollbar: {\n // 是否使用原生滚动样式\n // 开启后,菜单,弹窗,抽屉会使用原生滚动条组件\n native: false,\n },\n};\n
该分类主要说明一些地方为什么这样做,以及原因是什么
/@/
是 vite
内配置的别名
/@/settings
等同于 src/settings
为什么是/@/
因为项目是从 vite1.0
过渡过来的,vite1.0
只能以 /
开头,所以有一部分从 webpack
用户转过来的可能不习惯。
在 main.ts 内可以看到,本地开发会全量引入 antd.less,vite-plugin-style-import 在本地是没有作用的。
这样做的原因主要是加快本地开发刷新速度。如果在本地开发中也按需按需引入,则在浏览器控制台内可以看到,平均一个页面大概增加了 100 次 http 请求。如果全量引入,只增加了一个请求,所以为了减少请求数量,才这样种。
// src/main.ts\nif (import.meta.env.DEV) {\n import('ant-design-vue/dist/antd.less');\n}\n\n// build/vite/plugin/styleImport\nimport styleImport from 'vite-plugin-style-import';\nexport function configStyleImportPlugin(isBuild: boolean) {\n if (!isBuild) return [];\n const styleImportPlugin = styleImport({\n libs: [\n {\n libraryName: 'ant-design-vue',\n esModule: true,\n resolveStyle: (name) => {\n return `ant-design-vue/es/${name}/style/index`;\n },\n },\n ],\n });\n return styleImportPlugin;\n}\n
在 src/utils/dataUtil
内,使用的是 moment,其次在页面中对时间的操作也是使用 dateUtil,而不是直接 import moment from 'moment'
。
这样做主要是方便后续切换到 dayjs
,因为 api 一样,所以在后续切换中,只需更改 dataUtil 内的 import 即可,而不用全部更改。
TIP
列举了一些常见的问题。有问题可以先来这里寻找,如果没有可以在 issue 提。
遇到问题,可以先从以下几个方面查找
vben-admin 的项目配置默认是缓存在 localStorage
内,所以版本更新后可能有些配置没改变。
解决方式是每次更新代码的时候修改 package.json
内的 version
版本号. 因为 localStorage 的 key 是根据版本号来的。所以更新后版本不同前面的配置会失效。重新登录即可
VUE_VBEN_ADMIN__DEVELOPMENT__2.0.3__COMMON__LOCAL__KEY__
key 的组成是 [项目名]+[开发环境]+[版本号]+[key]
当修改 .env
等环境文件及 vite.config.ts
文件时,vite 会自动重启服务。
自动重启有几率出现问题,请重新运行项目即可解决.
如果将 \b build.minify 设置为 'esbuild',且不能启用 LEGACY,否则打包将会报错,两者选其一即可打包。
在控制台看到以下警告的原因是 ant-design-vue
会检测是否使用了 babel-plugin-import
来判断是否进行了组件库的按需引入。
但是项目使用的是 vite 的插件 vite-plugin-style-import 来进行按需引入。在 vite 内没必要使用 babel 在转换一次代码了。
所以想关闭这个警告,得等 ant-design-vue 提供可以关闭该警告的配置。
You are using a whole package of antd, please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size. Not support Vite !!!\n
菜单必须和路由匹配才会显示在界面上,所以得确保菜单和对应的路由存在即可显示.
由于 vite 在本地没有转换代码,且代码中用到了可选链等比较新的语法。所以本地开发需要使用版本较高的浏览器(Chrome 85+
)进行开发
这是由于开启了路由切换动画,且对应的页面组件存在多个根节点导致的,在页面最外层添加<div></div>
即可
错误示例
<template>\n <!-- 注释也算一个节点 -->\n <h1>text h1</h1>\n <h2>text h2</h2>\n</template>\n
正确示例
<template>\n <div>\n <h1>text h1</h1>\n <h2>text h2</h2>\n </div>\n</template>\n
提示
目前在 vite+vue3.0.5 版本中,如果组件命名携带关键字,则可能会导致内存溢出。例如 ImportExcel
excel 导入组件。
目前发现这个原因可能有以下,可以从以下原因来排查,如果还有别的可能,可以提交 pr 来告诉我
import { getCurrentInstance } from 'vue';\ngetCurrentInstance().ctx.xxxx;\n
目前在 safari 上面本地开发运行样式会有问题,还未找到原因,有知道的也可以告诉我。
如果出现依赖安装报错,启动报错等。先检查电脑环境有没有安装齐全。
12.0.0
不支持 13
, 推荐 14 版本。yarn.lock
和 node_modules
,然后重新运行 yarn install
.npmrc
文件,内容如下# .npmrc\nregistry = https://registry.npm.taobao.org\n
然后重新执行yarn run reinstall
等待安装完成即可
如果你使用了该项目进行项目开发。开发之中想同步最新的代码。你可以设置多个源的方式
git clone https://github.com/vbenjs/vben-admin-thin-next.git\n
# up 为源名称,可以随意设置\n# gitUrl为开源最新代码\ngit remote add up gitUrl;\n
# 提交代码到自己公司\n# main为分支名 需要自行根据情况修改\ngit push up main\n\n# 同步公司的代码\n# main为分支名 需要自行根据情况修改\ngit pull up main\n
git pull origin main\n
TIP
同步代码的时候会出现冲突。只需要把冲突解决即可
首先,完整版由于引用了比较多的库文件,所以打包会比较大。可以使用精简版来进行开发
其次建议开启 gzip,使用之后体积会只有原先 1/3 左右。
gzip 可以由服务器直接开启。如果是这样,前端不需要构建 .gz
格式的文件
如果前端构建了 .gz
文件,以 nginx 为例,nginx 需要开启 gzip_static: on
这个选项。
brotli
,比 gzip 更好的压缩。两者可以共存注意
gzip_static: 这个模块需要 nginx 另外安装,默认的 nginx 没有安装这个模块。
开启 brotli
也需要 nginx 另外安装模块
如果出现类似以下错误,请检查项目全路径(包含所有父级路径)不能出现中文、日文、韩文。否则将会出现路径访问 404 导致以下问题
[vite] Failed to resolve module import "ant-design-vue/dist/antd.css-vben-adminode_modulesant-design-vuedistantd.css". (imported by /@/setup/ant-design-vue/index.ts)\n
很多人问为什么不用dayjs
。在项目依赖中可以看到,它是 Ant-Design-Vue 内部自带的。
目前还没有基于 Vite 的 dayjs 替换 momentjs 方案,webpack 已经有了。等以后出现了在进行替换。
如果看到控制台有如下警告,且页面能正常打开 可以忽略该警告。
后续 vue-router
可能会提供配置项来关闭警告
2.6.1 及以上版本已移除此警告
[Vue Router warn]: No match found for location with path "xxxx"\n
当出现以下错误信息时,请检查你的 nodejs 版本号是否符合要求
TypeError: str.matchAll is not a function\nat Object.extractor (vue-vben-admin-main\\node_modules@purge-icons\\core\\dist\\index.js:146:27)\nat Extract (vue-vben-admin-main\\node_modules@purge-icons\\core\\dist\\index.js:173:54)\n\n
当页面出现以下报错,是因为 /xxx 对应的路由组件内部出现了错误。
Uncaught (in promise) Error: Couldn't resolve component "default" at "/xxx"\n\n
可以尝试从以下几点排查
// 正确的\nimport { cloneDeep } from 'lodash-es';\n\n// 报错\nimport _ from 'lodash-es';\n
这样就不会是使用的取值忘记 xxx.value 来进行数据获取
参考跨域问题
proxy 代理不成功,没有代理到实际地址?
代理只是服务请求代理,这个地址是不会变的。 原理可以简单的理解为,在本地启了一个服务,你先请求了本地的服务,本地的服务转发了你的请求到实际服务器。所以你在浏览器上看到的请求地址还是 http://localhost:8000/xxx
。以服务端是否收到请求为准。
跟组件库相关的问题可以查看常见问题
菜单数据的值被存放在 store/modules/permission
store 中, 你可以在这里进行修改
你可以在 store/modules/permission
下, 修改 routeFilter
方法来进行更灵活的菜单路由权限控制
const routeFilter = (route: AppRouteRecordRaw) => {\n const { meta } = route;\n // 抽出角色\n const { roles } = meta || {};\n\n // 添加你的自定义逻辑来过滤路由和菜单\n if (xxx) {\n return false;\n }\n\n if (!roles) return true;\n // 进行角色权限判断\n return roleList.some((role) => roles.includes(role));\n };\n\n
对接vben的项目地址,非官方项目,vben用户开源,开源协议请自行查看
在项目 /test/server
内有简单的 Node.js 测试后台接口服务,用 Koa2 实现
\ncd ./test/server\n\n# 安装依赖\nyarn\n\n# 运行服务\nyarn start\n\n
服务运行成功之后,就可以访问测试上传接口及 websocket 接口服务
',5);t.render=function(n,s,t,o,c,d){return e(),a("div",null,[r])};export default t;export{s as __pageData}; diff --git a/assets/other_server.md.0ae59f36.lean.js b/assets/other_server.md.0ae59f36.lean.js new file mode 100644 index 00000000..1bbfc25a --- /dev/null +++ b/assets/other_server.md.0ae59f36.lean.js @@ -0,0 +1 @@ +import{o as e,c as a,a as n}from"./app.8cddb23b.js";const s='{"title":"测试服务器","description":"","frontmatter":{},"headers":[{"level":2,"title":"使用","slug":"使用"}],"relativePath":"other/server.md","lastUpdated":1697592578593}',t={},r=n('',5);t.render=function(n,s,t,o,c,d){return e(),a("div",null,[r])};export default t;export{s as __pageData}; diff --git a/assets/style.84beecbd.css b/assets/style.84beecbd.css new file mode 100644 index 00000000..7927bb18 --- /dev/null +++ b/assets/style.84beecbd.css @@ -0,0 +1 @@ +:root{--c-white:#ffffff;--c-white-dark:#f8f8f8;--c-black:#000000;--c-divider-light:rgba(60, 60, 67, 0.12);--c-divider-dark:rgba(84, 84, 88, 0.48);--c-text-light-1:#2c3e50;--c-text-light-2:#476582;--c-text-light-3:#90a4b7;--c-brand:#3eaf7c;--c-brand-light:#4abf8a;--font-family-base:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',sans-serif;--font-family-mono:source-code-pro,Menlo,Monaco,Consolas,'Courier New',monospace;--z-index-navbar:10;--z-index-sidebar:6;--shadow-1:0 1px 2px rgba(0, 0, 0, 0.04),0 1px 2px rgba(0, 0, 0, 0.06);--shadow-2:0 3px 12px rgba(0, 0, 0, 0.07),0 1px 4px rgba(0, 0, 0, 0.07);--shadow-3:0 12px 32px rgba(0, 0, 0, 0.1),0 2px 6px rgba(0, 0, 0, 0.08);--shadow-4:0 14px 44px rgba(0, 0, 0, 0.12),0 3px 9px rgba(0, 0, 0, 0.12);--shadow-5:0 18px 56px rgba(0, 0, 0, 0.16),0 4px 12px rgba(0, 0, 0, 0.16);--header-height:3.6rem;--c-divider:var(--c-divider-light);--c-text:var(--c-text-light-1);--c-text-light:var(--c-text-light-2);--c-text-lighter:var(--c-text-light-3);--c-bg:var(--c-white);--c-bg-accent:var(--c-white-dark);--code-line-height:24px;--code-font-family:var(--font-family-mono);--code-font-size:14px;--code-inline-bg-color:rgba(27, 31, 35, 0.05);--code-bg-color:#282c34}*,::after,::before{box-sizing:border-box}html{line-height:1.4;font-size:16px;-webkit-text-size-adjust:100%}body{margin:0;width:100%;min-width:320px;min-height:100vh;line-height:1.4;font-family:var(--font-family-base);font-size:16px;font-weight:400;color:var(--c-text);background-color:var(--c-bg);direction:ltr;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;line-height:1.25}b,h1,h2,h3,h4,h5,h6,strong{font-weight:600}h1:focus .header-anchor,h1:hover .header-anchor,h2:focus .header-anchor,h2:hover .header-anchor,h3:focus .header-anchor,h3:hover .header-anchor,h4:focus .header-anchor,h4:hover .header-anchor,h5:focus .header-anchor,h5:hover .header-anchor,h6:focus .header-anchor,h6:hover .header-anchor{opacity:1}h1{margin-top:1.5rem;font-size:1.9rem}@media screen and (min-width:420px){h1{font-size:2.2rem}}h2{margin-top:2.25rem;margin-bottom:1.25rem;border-bottom:1px solid var(--c-divider);padding-bottom:.3rem;line-height:1.25;font-size:1.65rem}h2+h3{margin-top:1.5rem}h3{margin-top:2rem;font-size:1.35rem}h4{font-size:1.15rem}ol,p,ul{margin:1rem 0;line-height:1.7}[role=button],a,area,button,input,label,select,summary,textarea{touch-action:manipulation}a{text-decoration:none;color:var(--c-brand)}a:hover{text-decoration:underline}a.header-anchor{float:left;margin-top:.125em;margin-left:-.87em;padding-right:.23em;font-size:.85em;opacity:0}a.header-anchor:focus,a.header-anchor:hover{text-decoration:none}figure{margin:0}img{max-width:100%}ol,ul{padding-left:1.25em}li>ol,li>ul{margin:0}table{display:block;border-collapse:collapse;margin:1rem 0;overflow-x:auto}tr{border-top:1px solid #dfe2e5}tr:nth-child(2n){background-color:#f6f8fa}td,th{border:1px solid #dfe2e5;padding:.6em 1em}blockquote{margin:1rem 0;border-left:.2rem solid #dfe2e5;padding:.25rem 0 .25rem 1rem;font-size:1rem;color:#999}blockquote>p{margin:0}form{margin:0}.theme.sidebar-open .sidebar-mask{display:block}.theme.no-navbar>h1,.theme.no-navbar>h2,.theme.no-navbar>h3,.theme.no-navbar>h4,.theme.no-navbar>h5,.theme.no-navbar>h6{margin-top:1.5rem;padding-top:0}.theme.no-navbar aside{top:0}@media screen and (min-width:720px){.theme.no-sidebar aside{display:none}.theme.no-sidebar main{margin-left:0}}.sidebar-mask{position:fixed;z-index:2;display:none;width:100vw;height:100vh}code{margin:0;border-radius:3px;padding:.25rem .5rem;font-family:var(--code-font-family);font-size:.85em;color:var(--c-text-light);background-color:var(--code-inline-bg-color)}code .token.deleted{color:#ec5975}code .token.inserted{color:var(--c-brand)}div[class*=language-]{position:relative;margin:1rem -1.5rem;background-color:var(--code-bg-color);overflow-x:auto}li>div[class*=language-]{border-radius:6px 0 0 6px;margin:1rem -1.5rem 1rem -1.25rem}@media (min-width:420px){div[class*=language-]{margin:1rem 0;border-radius:6px}li>div[class*=language-]{margin:1rem 0 1rem 0;border-radius:6px}}[class*=language-] code,[class*=language-] pre{text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}[class*=language-] pre{position:relative;z-index:1;margin:0;padding:1.25rem 1.5rem;background:0 0;overflow-x:auto}[class*=language-] code{padding:0;line-height:var(--code-line-height);font-size:var(--code-font-size);color:#eee}.highlight-lines{position:absolute;top:0;bottom:0;left:0;padding:1.25rem 0;width:100%;line-height:var(--code-line-height);font-family:var(--code-font-family);font-size:var(--code-font-size);user-select:none;overflow:hidden}.highlight-lines .highlighted{background-color:rgba(0,0,0,.66)}div[class*=language-].line-numbers-mode{padding-left:3.5rem}.line-numbers-wrapper{position:absolute;top:0;bottom:0;left:0;z-index:3;border-right:1px solid rgba(0,0,0,.5);padding:1.25rem 0;width:3.5rem;text-align:center;line-height:var(--code-line-height);font-family:var(--code-font-family);font-size:var(--code-font-size);color:#888}[class*=language-]:before{position:absolute;top:.6em;right:1em;z-index:2;font-size:.8rem;color:#888}[class~=language-html]:before,[class~=language-markup]:before{content:'html'}[class~=language-markdown]:before,[class~=language-md]:before{content:'md'}[class~=language-css]:before{content:'css'}[class~=language-sass]:before{content:'sass'}[class~=language-scss]:before{content:'scss'}[class~=language-less]:before{content:'less'}[class~=language-stylus]:before{content:'styl'}[class~=language-javascript]:before,[class~=language-js]:before{content:'js'}[class~=language-ts]:before,[class~=language-typescript]:before{content:'ts'}[class~=language-json]:before{content:'json'}[class~=language-rb]:before,[class~=language-ruby]:before{content:'rb'}[class~=language-py]:before,[class~=language-python]:before{content:'py'}[class~=language-bash]:before,[class~=language-sh]:before{content:'sh'}[class~=language-php]:before{content:'php'}[class~=language-go]:before{content:'go'}[class~=language-rust]:before{content:'rust'}[class~=language-java]:before{content:'java'}[class~=language-c]:before{content:'c'}[class~=language-yaml]:before{content:'yaml'}[class~=language-dockerfile]:before{content:'dockerfile'}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}.custom-block.danger,.custom-block.tip,.custom-block.warning{margin:1rem 0;border-left:.5rem solid;padding:.1rem 1.5rem;overflow-x:auto}.custom-block.tip{background-color:#f3f5f7;border-color:var(--c-brand)}.custom-block.warning{border-color:#e7c000;color:#6b5900;background-color:rgba(255,229,100,.3)}.custom-block.warning .custom-block-title{color:#b29400}.custom-block.warning a{color:var(--c-text)}.custom-block.danger{border-color:#c00;color:#4d0000;background-color:#ffe6e6}.custom-block.danger .custom-block-title{color:#900}.custom-block.danger a{color:var(--c-text)}.custom-block.details{position:relative;display:block;border-radius:2px;margin:1.6em 0;padding:1.6em;background-color:#eee}.custom-block.details h4{margin-top:0}.custom-block.details figure:last-child,.custom-block.details p:last-child{margin-bottom:0;padding-bottom:0}.custom-block.details summary{outline:0;cursor:pointer}.custom-block-title{margin-bottom:-.4rem;font-weight:600}.sidebar-links{margin:0;padding:0;list-style:none}.sidebar-link-item{display:block;margin:0;border-left:.25rem solid transparent;color:var(--c-text)}a.sidebar-link-item:hover{text-decoration:none;color:var(--c-brand)}a.sidebar-link-item.active{color:var(--c-brand)}.sidebar>.sidebar-links{padding:.75rem 0 5rem}@media (min-width:720px){.sidebar>.sidebar-links{padding:1.5rem 0}}.sidebar>.sidebar-links>.sidebar-link+.sidebar-link{padding-top:.5rem}@media (min-width:720px){.sidebar>.sidebar-links>.sidebar-link+.sidebar-link{padding-top:1.25rem}}.sidebar>.sidebar-links>.sidebar-link>.sidebar-link-item{padding:.35rem 1.5rem .35rem 1.25rem;font-size:1.1rem;font-weight:700}.sidebar>.sidebar-links>.sidebar-link>a.sidebar-link-item.active{border-left-color:var(--c-brand);font-weight:600}.sidebar>.sidebar-links>.sidebar-link>.sidebar-links>.sidebar-link>.sidebar-link-item{display:block;padding:.35rem 1.5rem .35rem 2rem;line-height:1.4;font-size:1rem;font-weight:400}.sidebar>.sidebar-links>.sidebar-link>.sidebar-links>.sidebar-link>a.sidebar-link-item.active{border-left-color:var(--c-brand);font-weight:600}.sidebar>.sidebar-links>.sidebar-link>.sidebar-links>.sidebar-link>.sidebar-links>.sidebar-link>.sidebar-link-item{display:block;padding:.3rem 1.5rem .3rem 3rem;line-height:1.4;font-size:.9rem;font-weight:400}.sidebar>.sidebar-links>.sidebar-link>.sidebar-links>.sidebar-link>.sidebar-links>.sidebar-link>.sidebar-links>.sidebar-link>.sidebar-link-item{display:block;padding:.3rem 1.5rem .3rem 4rem;line-height:1.4;font-size:.9rem;font-weight:400}.debug[data-v-1be840de]{box-sizing:border-box;position:fixed;right:8px;bottom:8px;z-index:9999;border-radius:4px;width:74px;height:32px;color:#eee;overflow:hidden;cursor:pointer;background-color:rgba(0,0,0,.85);transition:all .15s ease}.debug[data-v-1be840de]:hover{background-color:rgba(0,0,0,.75)}.debug.open[data-v-1be840de]{right:0;bottom:0;width:100%;height:100%;margin-top:0;border-radius:0;padding:0 0;overflow:scroll}@media (min-width:512px){.debug.open[data-v-1be840de]{width:512px}}.debug.open[data-v-1be840de]:hover{background-color:rgba(0,0,0,.85)}.title[data-v-1be840de]{margin:0;padding:6px 16px 6px;line-height:20px;font-size:13px}.block[data-v-1be840de]{margin:2px 0 0;border-top:1px solid rgba(255,255,255,.16);padding:8px 16px;font-family:Hack,monospace;font-size:13px}.block+.block[data-v-1be840de]{margin-top:8px}.nav-bar-title[data-v-ffb90d4a]{font-size:1.3rem;font-weight:600;color:var(--c-text)}.nav-bar-title[data-v-ffb90d4a]:hover{text-decoration:none}.logo[data-v-ffb90d4a]{margin-right:.75rem;height:1.3rem;vertical-align:bottom}.icon.outbound{position:relative;top:-1px;display:inline-block;vertical-align:middle;color:var(--c-text-lighter)}.item[data-v-c272f228]{display:block;padding:0 1.5rem;line-height:36px;font-size:1rem;font-weight:600;color:var(--c-text);white-space:nowrap}.item.active[data-v-c272f228],.item[data-v-c272f228]:hover{text-decoration:none;color:var(--c-brand)}.item.external[data-v-c272f228]:hover{border-bottom-color:transparent;color:var(--c-text)}@media (min-width:720px){.item[data-v-c272f228]{border-bottom:2px solid transparent;padding:0;line-height:24px;font-size:.9rem;font-weight:500}.item.active[data-v-c272f228],.item[data-v-c272f228]:hover{border-bottom-color:var(--c-brand);color:var(--c-text)}}.item[data-v-7b16fcd4]{display:block;padding:0 1.5rem 0 2.5rem;line-height:32px;font-size:.9rem;font-weight:500;color:var(--c-text);white-space:nowrap}@media (min-width:720px){.item[data-v-7b16fcd4]{padding:0 24px 0 12px;line-height:32px;font-size:.85rem;font-weight:500;color:var(--c-text);white-space:nowrap}.item.active .arrow[data-v-7b16fcd4]{opacity:1}}.item.active[data-v-7b16fcd4],.item[data-v-7b16fcd4]:hover{text-decoration:none;color:var(--c-brand)}.item.external[data-v-7b16fcd4]:hover{border-bottom-color:transparent;color:var(--c-text)}@media (min-width:720px){.arrow[data-v-7b16fcd4]{display:inline-block;margin-right:8px;border-top:6px solid #ccc;border-right:4px solid transparent;border-bottom:0;border-left:4px solid transparent;vertical-align:middle;opacity:0;transform:translateY(-2px) rotate(-90deg)}}.nav-dropdown-link[data-v-312de885]{position:relative;height:36px;overflow:hidden;cursor:pointer}@media (min-width:720px){.nav-dropdown-link[data-v-312de885]{height:auto;overflow:visible}.nav-dropdown-link:hover .dialog[data-v-312de885]{display:block}}.nav-dropdown-link.open[data-v-312de885]{height:auto}.button[data-v-312de885]{display:block;border:0;padding:0 1.5rem;width:100%;text-align:left;line-height:36px;font-family:var(--font-family-base);font-size:1rem;font-weight:600;color:var(--c-text);white-space:nowrap;background-color:transparent;cursor:pointer}.button[data-v-312de885]:focus{outline:0}@media (min-width:720px){.button[data-v-312de885]{border-bottom:2px solid transparent;padding:0;line-height:24px;font-size:.9rem;font-weight:500}}.button-arrow[data-v-312de885]{display:inline-block;margin-top:-1px;margin-left:8px;border-top:6px solid #ccc;border-right:4px solid transparent;border-bottom:0;border-left:4px solid transparent;vertical-align:middle}.button-arrow.right[data-v-312de885]{transform:rotate(-90deg)}@media (min-width:720px){.button-arrow.right[data-v-312de885]{transform:rotate(0)}}.dialog[data-v-312de885]{margin:0;padding:0;list-style:none}@media (min-width:720px){.dialog[data-v-312de885]{display:none;position:absolute;top:26px;right:-8px;border-radius:6px;padding:12px 0;min-width:128px;background-color:var(--c-bg);box-shadow:var(--shadow-3)}}.nav-links[data-v-1e870408]{padding:.75rem 0;border-bottom:1px solid var(--c-divider)}@media (min-width:720px){.nav-links[data-v-1e870408]{display:flex;padding:6px 0 0;align-items:center;border-bottom:0}.item+.item[data-v-1e870408]{padding-left:24px}}.sidebar-button{position:absolute;top:.6rem;left:1rem;display:none;padding:.6rem;cursor:pointer}.sidebar-button .icon{display:block;width:1.25rem;height:1.25rem}@media screen and (max-width:719px){.sidebar-button{display:block}}.nav-bar[data-v-2332dbaa]{position:fixed;top:0;right:0;left:0;z-index:var(--z-index-navbar);display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--c-divider);padding:.7rem 1.5rem .7rem 4rem;height:var(--header-height);background-color:var(--c-bg)}@media (min-width:720px){.nav-bar[data-v-2332dbaa]{padding:.7rem 1.5rem}}.flex-grow[data-v-2332dbaa]{flex-grow:1}.nav[data-v-2332dbaa]{display:none}@media (min-width:720px){.nav[data-v-2332dbaa]{display:flex}.navbar__dark-mode[data-v-2332dbaa]{display:none}}.nav-icons[data-v-2332dbaa]{display:flex;padding:2px 0 0;align-items:center;border-bottom:0;margin-left:12px}.nav-icons .item[data-v-2332dbaa]{padding-left:12px}.sidebar[data-v-4668b452]{position:fixed;top:var(--header-height);bottom:0;left:0;z-index:var(--z-index-sidebar);border-right:1px solid var(--c-divider);width:16.4rem;background-color:var(--c-bg);overflow-y:auto;transform:translateX(-100%);transition:transform .25s ease}@media (min-width:720px){.sidebar[data-v-4668b452]{transform:translateX(0)}}@media (min-width:960px){.sidebar[data-v-4668b452]{width:20rem}}.sidebar.open[data-v-4668b452]{transform:translateX(0)}.nav[data-v-4668b452]{display:block}@media (min-width:720px){.nav[data-v-4668b452]{display:none}}.link[data-v-045573c2]{display:inline-block;font-size:1rem;font-weight:500;color:var(--c-text-light)}.link[data-v-045573c2]:hover{text-decoration:none;color:var(--c-brand)}.icon[data-v-045573c2]{margin-left:4px}.last-updated[data-v-03e55a27]{display:inline-block;margin:0;line-height:1.4;font-size:.9rem;color:var(--c-text-light)}@media (min-width:960px){.last-updated[data-v-03e55a27]{font-size:1rem}}.prefix[data-v-03e55a27]{display:inline-block;font-weight:500}.datetime[data-v-03e55a27]{display:inline-block;margin-left:6px;font-weight:400}.page-footer[data-v-22e60b1a]{padding-top:1rem;padding-bottom:1rem;overflow:auto}@media (min-width:960px){.page-footer[data-v-22e60b1a]{display:flex;justify-content:space-between;align-items:center}}.updated[data-v-22e60b1a]{padding-top:4px}@media (min-width:960px){.updated[data-v-22e60b1a]{padding-top:0}}.next-and-prev-link[data-v-0facf926]{padding-top:1rem}.container[data-v-0facf926]{display:flex;justify-content:space-between;border-top:1px solid var(--c-divider);padding-top:1rem}.next[data-v-0facf926],.prev[data-v-0facf926]{display:flex;flex-shrink:0;width:50%}.prev[data-v-0facf926]{justify-content:flex-start;padding-right:12px}.next[data-v-0facf926]{justify-content:flex-end;padding-left:12px}.link[data-v-0facf926]{display:inline-flex;align-items:center;max-width:100%;font-size:1rem;font-weight:500}.text[data-v-0facf926]{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.icon[data-v-0facf926]{display:block;flex-shrink:0;width:16px;height:16px;fill:var(--c-text);transform:translateY(1px)}.icon-prev[data-v-0facf926]{margin-right:8px}.icon-next[data-v-0facf926]{margin-left:8px}.page[data-v-7abc59e6]{padding-top:var(--header-height)}@media (min-width:720px){.page[data-v-7abc59e6]{margin-left:16.4rem}}@media (min-width:960px){.page[data-v-7abc59e6]{margin-left:20rem}}.container[data-v-7abc59e6]{margin:0 auto;padding:0 1.5rem 4rem;padding:.025rem 0 2rem 0;width:calc(100% - var(--slug-width))}.content[data-v-7abc59e6]{padding-bottom:1.5rem}@media (max-width:420px){.content[data-v-7abc59e6]{clear:both}}#ads-container{margin:0 auto}@media (min-width:420px){#ads-container{position:relative;right:0;float:right;margin:-8px -8px 24px 24px;width:146px}}@media (max-width:420px){#ads-container{height:105px;margin:1.75rem 0}}@media (min-width:1400px){#ads-container{position:fixed;right:8px;bottom:8px}}.border{border-width:1px}.flex{display:-webkit-box;display:-ms-flexbox;display:-webkit-flex;display:flex}.inline-flex{display:-webkit-inline-box;display:-ms-inline-flexbox;display:-webkit-inline-flex;display:inline-flex}.table{display:table}.justify-center{-webkit-box-pack:center;-ms-flex-pack:center;-webkit-justify-content:center;justify-content:center}.h-full{height:100%}.text-color-base{font-size:1rem;line-height:1.5rem}.m-3{margin:.75rem}.m-4{margin:1rem}.my-4{margin-top:1rem;margin-bottom:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mt-4{margin-top:1rem}.mr-4{margin-right:1rem}.mr-2{margin-right:.5rem}.mb-2{margin-bottom:.5rem}.ml-4{margin-left:1rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-10{padding:2.5rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.px-4{padding-left:1rem;padding-right:1rem}.absolute{position:absolute}.relative{position:relative}.content{content:""}.w-full{width:100%}.transition{-webkit-transition-property:background-color,border-color,color,fill,stroke,opacity,-webkit-box-shadow,-webkit-transform,filter,backdrop-filter;-o-transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,-webkit-box-shadow,transform,-webkit-transform,filter,backdrop-filter;-webkit-transition-timing-function:cubic-bezier(.4,0,.2,1);-o-transition-timing-function:cubic-bezier(.4,0,.2,1);transition-timing-function:cubic-bezier(.4,0,.2,1);-webkit-transition-duration:150ms;-o-transition-duration:150ms;transition-duration:150ms}[duration~="8000"]{-webkit-transition-duration:8s;-o-transition-duration:8s;transition-duration:8s}:root{--c-brand:#4569d4;--c-brand-light:#4f73dd;--c-white-dark:#f8f8f8;--c-black:#111827;--c-black-light:#161f32;--c-black-lighter:#262a44;--c-text-dark-1:#d9e6eb;--c-text-dark-2:#c4dde6;--c-text-dark-3:#abc4cc;--c-brand-text:var(--c-white);--c-bg-accent:var(--c-white-dark);--code-bg-color:var(--c-white-dark);--code-inline-bg-color:var(--c-white-dark);--code-font-family:'dm',source-code-pro,Menlo,Monaco,Consolas,'Courier New',monospace;--code-font-size:16px;--slug-width:10rem;--header-height:3.6rem;--sidebar-width:16.4rem}html:not(.light):root{--c-text:var(--c-text-dark-1);--c-text-light:var(--c-text-dark-2);--c-text-lighter:var(--c-text-dark-3);--c-divider:var(--c-divider-dark);--c-bg:var(--c-black);--c-bg-accent:var(--c-black-light);--code-bg-color:var(--c-black-light);--code-inline-bg-color:var(--c-black-light)}html:not(.light) .DocSearch{--docsearch-text-color:var(--c-white-dark);--docsearch-container-background:rgba(9, 10, 17, 0.8);--docsearch-modal-background:var(--c-black);--docsearch-modal-shadow:inset 1px 1px 0 0 #2c2e40,0 3px 8px 0 #000309;--docsearch-searchbox-background:var(--c-black-lighter);--docsearch-searchbox-focus-background:var(--c-black-light);--docsearch-hit-color:var(--c-text-dark-1);--docsearch-hit-active-color:var(--c-brand-text);--docsearch-hit-shadow:none;--docsearch-hit-background:var(--c-black-light);--docsearch-key-gradient:linear-gradient(-26.5deg, #565872, #31355b);--docsearch-key-shadow:inset 0 -2px 0 0 #282d55,inset 0 0 1px 1px #51577d,0 2px 2px 0 rgba(3, 4, 9, 0.3);--docsearch-footer-background:var(--c-black-light);--docsearch-footer-shadow:inset 0 1px 0 0 rgba(73, 76, 106, 0.5),0 -4px 8px 0 rgba(0, 0, 0, 0.2);--docsearch-logo-color:var(--c-white-dark);--docsearch-muted-color:var(--c-text-dark-1)}.home-hero .image{width:100px;height:100px}.nav-bar .logo{height:26px;margin-right:8px}.nav-bar .nav-bar-title{display:flex;align-items:center}.content img{border-radius:10px}.nav-dropdown-link-item .icon{display:none}.action.alt .icon.outbound{color:currentColor}.action .item.item.item{border-color:var(--c-brand);background-color:var(--c-brand)}.action.alt .item.item.item{background-color:transparent;color:var(--c-text);border-color:var(--c-brand);transition:background-color 150ms ease-in-out,border-color 150ms ease-in-out,color 150ms ease-in-out}.action .item.item.item,.action .item.item.item:hover{color:var(--c-brand-text)}html.dark .action.alt .item.item.item{border-color:var(--c-text)}.action.alt .item.item.item:hover{background-color:var(--c-brand-light);border-color:var(--c-brand-light);color:var(--c-brand-text)}.nav-bar.nav-bar{background-color:var(--c-bg)}.custom-block.tip{border-color:var(--c-brand-light);background-color:var(--c-bg-accent)}.carbon-ads{padding:8px}.bsa-cpc,.carbon-ads{background-color:var(--c-bg-accent)!important}html:not(.light) .custom-block.warning{color:var(--c-text)}html:not(.light) .custom-block.warning a{color:var(--c-brand)}.action.alt .item.item,.bsa-cpc,.carbon-ads,.custom-block.tip,.nav-bar,body,code,div[class*=language-]{transition:background-color .3s ease-in-out,color .3s ease-in-out}.DocSearch,.DocSearch-Form,.DocSearch-Search-Icon,.sidebar.sidebar.sidebar{transition:transform 250ms ease,background-color .3s ease-in-out,color .3s ease-in-out}.DocSearch-Button-Key,.DocSearch-Footer,.DocSearch-Modal{transition:background-color .3s ease-in-out,color .3s ease-in-out,box-shadow 250ms ease-in-out}code{font-size:.95em;padding:.175em .35em}input{border:1px solid #d9d9d9;padding:8px 6px;border-radius:4px;color:rgba(0,0,0,.65);outline:0}input:focus{border-color:#40a9ff}button{padding:5px 16px;border-radius:2px;color:rgba(0,0,0,.65);outline:0;border:1px solid #d9d9d9;cursor:pointer;background:#fff}button:hover{border-color:#40a9ff;color:#40a9ff}:-moz-placeholder,:-ms-input-placeholder,::-moz-placeholder,::-webkit-input-placeholder{color:var(--placeholder-color)}.nav-btn{display:flex;font-size:1.05rem;border:0;outline:0;background:0 0;color:var(--c-text);opacity:.8;cursor:pointer}.nav-btn:hover{opacity:1}.nav-btn svg{margin:auto}.slugs{position:fixed;top:var(--header-height);right:0;max-height:calc(100% - var(--header-height) - 10rem);width:var(--slug-width);padding:50px 24px 0 0;border-right:1px solid var(--border-color);background-color:#fff;z-index:3;overflow-y:auto}[class*=language-] code{color:inherit}code[class*=language-],pre[class*=language-]{color:#d6deeb;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:rgba(29,59,83,.99)}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:rgba(29,59,83,.99)}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{color:#fff;background:#011627}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.prolog{color:#637777}.token.punctuation{color:#c792ea}.namespace{color:#b2ccd6}.token.deleted{color:#ef5350}.token.property,.token.symbol{color:#80cbc4}.token.keyword,.token.operator,.token.tag{color:#7fdbca}.token.boolean{color:#ff5874}.token.number{color:#f78c6c}.token.builtin,.token.char,.token.constant,.token.function{color:#82aaff}.token.doctype,.token.function,.token.selector{color:#c792ea}.token.attr-name,.token.inserted,code .token.inserted{color:#addb67}.language-css .token.string,.style .token.string,.token.entity,.token.string,.token.url{color:#addb67}.token.atrule,.token.attr-value,.token.class-name{color:#ffcb8b}.token.important,.token.regex,.token.variable{color:#d6deeb}.token.bold,.token.important{font-weight:700}html.dark tr:nth-child(2n){background-color:transparent!important}html.light code[class*=language-],html.light pre[class*=language-]{color:#403f53;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}html.light code[class*=language-] ::-moz-selection,html.light code[class*=language-]::-moz-selection,html.light pre[class*=language-] ::-moz-selection,html.light pre[class*=language-]::-moz-selection{text-shadow:none;background:#fbfbfb}html.light code[class*=language-] ::selection,html.light code[class*=language-]::selection,html.light pre[class*=language-] ::selection,html.light pre[class*=language-]::selection{text-shadow:none;background:#fbfbfb}@media print{html.light code[class*=language-],html.light pre[class*=language-]{text-shadow:none}}html.light pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}html.light :not(pre)>code[class*=language-],html.light pre[class*=language-]{color:#fff;background:#fbfbfb}html.light :not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}html.light .token.cdata,html.light .token.comment,html.light .token.prolog{color:#989fb1}html.light .token.punctuation{color:#994cc3}html.light .namespace{color:#0c969b}html.light code .token.deleted{color:#ec5975}html.light .token.keyword,html.light .token.operator,html.light .token.property,html.light .token.symbol{color:#0c969b}html.light .token.tag{color:#994cc3}html.light .token.boolean{color:#bc5454}html.light .token.number{color:#aa0982}html.light .language-css .token.string,html.light .style .token.string,html.light .token.builtin,html.light .token.char,html.light .token.constant,html.light .token.entity,html.light .token.string,html.light .token.url{color:#4876d6}html.light .token.doctype,html.light .token.function,html.light .token.selector{color:#994cc3}html.light .token.attr-name,html.light .token.inserted{color:#4876d6}html.light .token.atrule,html.light .token.attr-value,html.light .token.class-name{color:#111}html.light .token.important,html.light .token.regex,html.light .token.variable{color:#c96765}html.light .token.bold,html.light .token.important{font-weight:700}.home-hero[data-v-e065f044]{margin:2.5rem 0 2.75rem;padding:0 1.5rem;text-align:center}@media (min-width:420px){.home-hero[data-v-e065f044]{margin:3.5rem 0}}@media (min-width:720px){.home-hero[data-v-e065f044]{margin:4rem 0 4.25rem}}.figure[data-v-e065f044]{padding:0 1.5rem}.image[data-v-e065f044]{display:block;margin:0 auto;width:auto;max-width:100%;max-height:280px}.title[data-v-e065f044]{margin-top:1.5rem;font-size:2rem}@media (min-width:420px){.title[data-v-e065f044]{font-size:3rem}}@media (min-width:720px){.title[data-v-e065f044]{margin-top:2rem}}.description[data-v-e065f044]{margin:0;margin-top:.25rem;line-height:1.3;font-size:1.2rem;color:var(--c-text-light)}@media (min-width:420px){.description[data-v-e065f044]{line-height:1.2;font-size:1.6rem}}.action[data-v-e065f044]{margin-top:1.5rem;display:inline-block}.action.alt[data-v-e065f044]{margin-left:1.5rem}@media (min-width:420px){.action[data-v-e065f044]{margin-top:2rem;display:inline-block}}.action[data-v-e065f044] .item{display:inline-block;border-radius:6px;padding:0 20px;line-height:44px;font-size:1rem;font-weight:500;color:var(--c-bg);background-color:var(--c-brand);border:2px solid var(--c-brand);transition:background-color .1s ease}.action.alt[data-v-e065f044] .item{background-color:var(--c-bg);color:var(--c-brand)}.action[data-v-e065f044] .item:hover{text-decoration:none;color:var(--c-bg);background-color:var(--c-brand-light)}@media (min-width:420px){.action[data-v-e065f044] .item{padding:0 24px;line-height:52px;font-size:1.2rem;font-weight:500}}.home-features[data-v-9c9c2344]{margin:0 auto;padding:2.5rem 0 2.75rem;max-width:960px}.home-hero+.home-features[data-v-9c9c2344]{padding-top:0}@media (min-width:420px){.home-features[data-v-9c9c2344]{padding:3.25rem 0 3.5rem}.home-hero+.home-features[data-v-9c9c2344]{padding-top:0}}@media (min-width:720px){.home-features[data-v-9c9c2344]{padding-right:1.5rem;padding-left:1.5rem}}.wrapper[data-v-9c9c2344]{padding:0 1.5rem}.home-hero+.home-features .wrapper[data-v-9c9c2344]{border-top:1px solid var(--c-divider);padding-top:2.5rem}@media (min-width:420px){.home-hero+.home-features .wrapper[data-v-9c9c2344]{padding-top:3.25rem}}@media (min-width:720px){.wrapper[data-v-9c9c2344]{padding-right:0;padding-left:0}}.container[data-v-9c9c2344]{margin:0 auto;max-width:392px}@media (min-width:720px){.container[data-v-9c9c2344]{max-width:960px}}.features[data-v-9c9c2344]{display:flex;flex-wrap:wrap;margin:-20px -24px}.feature[data-v-9c9c2344]{flex-shrink:0;padding:20px 24px;width:100%}@media (min-width:720px){.feature[data-v-9c9c2344]{width:calc(100% / 3)}}.title[data-v-9c9c2344]{margin:0;border-bottom:0;line-height:1.4;font-size:1.25rem;font-weight:500}@media (min-width:420px){.title[data-v-9c9c2344]{font-size:1.4rem}}.details[data-v-9c9c2344]{margin:0;line-height:1.6;font-size:1rem;color:var(--c-text-light)}.title+.details[data-v-9c9c2344]{padding-top:.25rem}.footer[data-v-44324124]{margin:0 auto;max-width:960px}@media (min-width:720px){.footer[data-v-44324124]{padding:0 1.5rem}}.container[data-v-44324124]{padding:2rem 1.5rem 2.25rem}.home-content+.footer .container[data-v-44324124],.home-features+.footer .container[data-v-44324124],.home-hero+.footer .container[data-v-44324124]{border-top:1px solid var(--c-divider)}@media (min-width:420px){.container[data-v-44324124]{padding:3rem 1.5rem 3.25rem}}.text[data-v-44324124]{margin:0;text-align:center;line-height:1.4;font-size:.9rem;color:var(--c-text-light)}.home[data-v-1fd43058]{padding-top:var(--header-height)}.home-content[data-v-1fd43058]{max-width:960px;margin:0 auto;padding:0 1.5rem}@media (max-width:720px){.home-content[data-v-1fd43058]{max-width:392px;padding:0}} \ No newline at end of file diff --git a/components/auth.html b/components/auth.html new file mode 100644 index 00000000..dc1820b3 --- /dev/null +++ b/components/auth.html @@ -0,0 +1,45 @@ + + + + + +用于项目权限的组件,一般用于按钮级等细粒度权限管理
<template>
+ <div>
+ <Authority :value="RoleEnum.ADMIN">
+ <a-button type="primary" block> 只有admin角色可见 </a-button>
+ </Authority>
+ </div>
+</template>
+<script>
+ import { Authority } from '/@/components/Authority';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { Authority },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
value | RoleEnum,RoleEnum[],string,string[] | - | 角色信息或者权限编码。会自动区分权限模式 |
一些比较基础的通用组件使用方式
用于显示标题,可以显示帮助按钮及文本
<template>
+ <div>
+ <BasicTitle helpMessage="提示1">标题</BasicTitle>
+ <BasicTitle :helpMessage="['提示1', '提示2']">标题</BasicTitle>
+ </div>
+</template>
+<script>
+ import { BasicTitle } from '/@/components/Basic/index';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { BasicTitle },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
helpMessage | string|string[] | - | 标题右侧帮助按钮信息 |
span | boolean | false | 是否显示标题左侧蓝色色块 |
normal | boolean | false | 将文字默认化,不加粗 |
名称 | 说明 |
---|---|
default | 标题文本 |
带动画的箭头组件
<template>
+ <div>
+ <BasicArrow :expand="false" />
+ </div>
+</template>
+<script>
+ import { BasicArrow } from '/@/components/Basic/index';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { BasicArrow },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
expand | boolean | false | 箭头展开状态 |
top | boolean | false | 箭头默认向上 |
bottom | boolean | false | 箭头默认向下 |
inset | boolean | false | 取消 padding/margin,用于内嵌 |
帮助按钮组件
<template>
+ <div>
+ <BasicHelp :text="['提示1', '提示2']" />
+ <BasicHelp text="提示" />
+ </div>
+</template>
+<script>
+ import { BasicHelp } from '/@/components/Basic/index';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { BasicHelp },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
fontSize | string | 14px | - | 字体大小 |
color | string | #fff | - | 颜色 |
text | string|string[] | - | - | 文本列表 |
showIndex | boolean | true | - | 是否显示序号,在 text 为 string[]情况下生效 |
maxWidth | string | 600px | - | 最大宽度 |
placement | string | right | - | 显示方向,参考 Tooltip 组件 |
名称 | 说明 |
---|---|
default | 默认图标 |
用于监听包裹的元素点击外部触发事件
<template>
+ <div>
+ <ClickOutSide @clickOutside="() => (showRef = false)">
+ <div @click="() => (showRef = true)">
+ {{ showRef ? '鼠标点击那部(点击边框外可以恢复)' : '点击该区域状态(初始状态)' }}
+ </div>
+ </ClickOutSide>
+ </div>
+</template>
+<script>
+ import { defineComponent, ref } from 'vue';
+ import { ClickOutSide } from '/@/components/ClickOutSide/';
+ export default defineComponent({
+ components: { ClickOutSide },
+ setup() {
+ const showRef = ref(false);
+ return {
+ showRef,
+ };
+ },
+ });
+</script>
+
事件 | 回调参数 | 说明 |
---|---|---|
clickOutside | ()=>void | 点击包裹元素外部区域触发 |
名称 | 说明 |
---|---|
default | 被包裹的元素 |
代码编辑器
<template>
+ <CodeEditor v-model:value="value" :mode="modeValue" />
+</template>
+<script>
+ import { defineComponent, ref } from 'vue';
+ export default defineComponent({
+ components: { CodeEditor },
+ setup() {
+ const modeValue = ref('application/json');
+ return { value, modeValue };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value(v-model:value) | any | - | - | 绑定值 |
mode | string | application/json | 'application/json' ,'htmlmixed' ,'javascript' | 代码类型 |
readonly | boolean | - | - | 是否只读 |
区域折叠卡片容器
<template>
+ <div>
+ <CollapseContainer> content </CollapseContainer>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { CollapseContainer } from '/@/components/Container/index';
+
+ export default defineComponent({
+ components: {
+ CollapseContainer,
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
title | string | - | - | 标题 |
canExpan | boolean | true | - | 是否可以展开,为true 显示折叠按钮 |
helpMessage | string[],string | - | - | 标题右侧温馨提醒 |
triggerWindowResize | boolean | false | - | 展开收缩的时候是否触发 window.resize |
loading | boolean | false | - | 显示加载骨架屏 |
lazyTime | number | 0 | - | 延迟加载时间 |
名称 | 说明 |
---|---|
title | 自定义标题 |
action | 自定义右侧操作按钮 |
default | 默认区域 |
footer | 自定义底部区域 |
倒计时组件
倒计时按钮组件
<template>
+ <CountButton />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { CountButton } from '/@/components/CountDown';
+
+ export default defineComponent({
+ components: { CountButton },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | any | - | - | 绑定值 |
count | number | 60 | - | 倒计时时间 |
beforeStartFunc | ()=>promise | - | - | 倒计时之前执行的函数,返回 true 才会开始执行 |
倒计时输入框按钮组件
<template>
+ <CountdownInput />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { CountdownInput } from '/@/components/CountDown';
+
+ export default defineComponent({
+ components: { CountdownInput },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | any | - | - | 绑定值 |
size | string | 'default', 'large', 'small' | - | 输入框即按钮大小 |
count | number | 60 | - | 倒计时时间 |
sendCodeApi | ()=>promise | - | - | 倒计时之前执行的函数,返回 true 才会开始执行 |
数字动画组件
该组件对 vue-countTo 进行了重构,改造成适配 vue3 语法的组件。
<template>
+ <CountTo prefix="$" :color="'#409EFF'" :startVal="1" :endVal="200000" :duration="8000" />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { CountTo } from '/@/components/CountTo/index';
+
+ export default defineComponent({
+ components: {
+ CountTo,
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
startVal | number | 0 | 起始值 |
endVal | number | 2021 | 结束值 |
duration | number | 1500 | 动画持续时间 |
autoplay | boolean | true | 自动执行 |
prefix | string | - | 前缀 |
suffix | string | - | 后缀 |
separator | string | , | 分隔符 |
color | string | - | 字体颜色 |
useEasing | boolean | true | 是否开启动画 |
transition | string | linear | 动画效果 |
decimals | number | 0 | 保留小数点位数 |
名称 | 回调参数 | 说明 |
---|---|---|
start | ()=>void | 开始执行动画 |
reset | ()=>void | 重置 |
图片裁剪组件
图片裁剪组件
<template>
+ <CropperImage ref="refCropper" :src="img" @cropend="handleCropend" style="width: 40vw" />
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { CropperImage } from '/@/components/Cropper';
+ import img from '/@/assets/images/header.jpg';
+
+ export default defineComponent({
+ components: {
+ CropperImage,
+ },
+ setup() {
+ const info = ref('');
+ const cropperImg = ref('');
+
+ function handleCropend({ imgBase64, imgInfo }) {
+ info.value = imgInfo;
+ cropperImg.value = imgBase64;
+ }
+
+ return {
+ img,
+ info,
+ cropperImg,
+ handleCropend,
+ };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
src | string | - | 图片源 |
alt | string | - | 图片 alt |
circled | boolean | false | 圆形裁剪框 |
realTimePreview | boolean | true | 实时触发预览 |
height | string | 360px | 高度 |
crossorigin | string | - | crossorigin |
imageStyle | object | `` | 图片样式 |
options | object | DefaultOptions | corpperjs 配置项 |
{
+ aspectRatio: 1,
+ zoomable: true,
+ zoomOnTouch: true,
+ zoomOnWheel: true,
+ cropBoxMovable: true,
+ cropBoxResizable: true,
+ toggleDragModeOnDblclick: true,
+ autoCrop: true,
+ background: true,
+ highlight: true,
+ center: true,
+ responsive: true,
+ restore: true,
+ checkCrossOrigin: true,
+ checkOrientation: true,
+ scalable: true,
+ modal: true,
+ guides: true,
+ movable: true,
+ rotatable: true,
+}
+
头像裁剪组件
<template>
+ <CropperAvatar :uploadApi="uploadApi" />
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { CropperAvatar } from '/@/components/Cropper';
+ import { uploadApi } from '/@/api/sys/upload';
+
+ export default defineComponent({
+ components: {
+ CropperAvatar,
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 | 版本 |
---|---|---|---|---|
width | string,number | 200px | 图片源 | |
uploadApi | ({ file: Blob, name: string }) => Promise<void> | - | 图片上传接口 | |
value | String | - | 当前头像地址(v-model) | 2.5.3 |
showBtn | Boolean | true | 是否显示按钮 | 2.5.3 |
btnText | String | - | 按钮文案 | 2.5.3 |
btnProps | ButtonProps | - | 按钮的其它属性 | 2.5.3 |
名称 | 参数 | 说明 | 版本 |
---|---|---|---|
change | value: String | 当头像上传完成时触发 | 2.5.3 |
名称 | 定义 | 说明 | 版本 |
---|---|---|---|
openModal | ()=>void | 打开上传Modal | 2.5.3 |
closeModal | ()=>void | 关闭上传Modal | 2.5.3 |
对 antv
的 Descriptions 组件进行封装
<template>
+ <div class="p-4">
+ <Description
+ title="基础示例"
+ :collapseOptions="{ canExpand: true, helpMessage: 'help me' }"
+ :column="3"
+ :data="mockData"
+ :schema="schema"
+ />
+ <Description @register="register" class="mt-4" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { Alert } from 'ant-design-vue';
+ import { Description, DescItem, useDescription } from '/@/components/Description/index';
+ const mockData: any = {
+ username: 'test',
+ nickName: 'VB',
+ age: 123,
+ phone: '15695909xxx',
+ email: '190848757@qq.com',
+ addr: '厦门市思明区',
+ sex: '男',
+ certy: '3504256199xxxxxxxxx',
+ tag: 'orange',
+ };
+ const schema: DescItem[] = [
+ {
+ field: 'username',
+ label: '用户名',
+ },
+ {
+ field: 'nickName',
+ label: '昵称',
+ render: (curVal, data) => {
+ return `${data.username}-${curVal}`;
+ },
+ },
+ {
+ field: 'phone',
+ label: '联系电话',
+ },
+ {
+ field: 'email',
+ label: '邮箱',
+ },
+ {
+ field: 'addr',
+ label: '地址',
+ },
+ ];
+ export default defineComponent({
+ components: { Description, Alert },
+ setup() {
+ const [register] = useDescription({
+ title: 'useDescription',
+ data: mockData,
+ schema: schema,
+ });
+ return { mockData, schema, register };
+ },
+ });
+</script>
+
参考以上示例
const [register] = useDescription(Props);
+
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv Description
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
title | string | - | - | 标题 |
size | string | small | - | 大小 |
bordered | boolean | true | - | 是否展示边框 |
column | Number, Object | { xxl: 4, xl: 3, lg: 3, md: 3, sm: 2, xs: 1 } | - | 一行的 DescriptionItems 数量 |
useCollapse | boolean | - | - | 是否包裹 CollapseContainer 组件 |
collapseOptions | Object | - | - | CollapseContainer 组件属性 |
schema | DescItem[] | - | - | 详情项配置,见下方 DescItem 配置 |
data | object | - | - | 数据源 |
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
field | string | - | - | 字段名 |
label | string | - | - | 标签名 |
labelMinWidth | number | - | - | label 最小宽度 |
contentMinWidth | number | - | - | content 最小宽度 |
labelStyle | any | - | - | label 样式 |
span | number | - | - | 和并列数量 |
show | (data)=>boolean | - | - | 动态判断当前组件是否显示 |
render | (val: string, data: any)=>VNode,undefined,Element,string,number | - | - | 自定义渲染 content |
对 antv
的 drawer 组件进行封装,扩展拖拽,全屏,自适应高度等功能。
由于 drawer 内部代码一般独立成单独文件,推荐独立成组件来进行开发,所以示例都是以独立的文件来进行说明
独立组件代码,用于写组件内部的内容
<template>
+ <BasicDrawer v-bind="$attrs" title="Drawer Title" width="50%"> Drawer Info. </BasicDrawer>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicDrawer } from '/@/components/Drawer';
+ export default defineComponent({
+ components: { BasicDrawer },
+ });
+</script>
+
页面引用弹窗
<template>
+ <div>
+ <Drawer @register="register" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { Alert } from 'ant-design-vue';
+ import { useDrawer } from '/@/components/Drawer';
+ import Drawer from './Drawer.vue';
+
+ export default defineComponent({
+ components: { Drawer },
+ setup() {
+ const [register, { openDrawer }] = useDrawer();
+ return {
+ register,
+ openDrawer,
+ };
+ },
+ });
+</script>
+
useDrawer 用于操作组件
const [register, { openDrawer, setDrawerProps }] = useDrawer();
+
register
register 用于注册 useDrawer
,如果需要使用 useDrawer
提供的 api,必须将 register
传入组件的 onRegister
。
原理其实很简单,就是 vue 的组件子传父通信,内部通过 emit("register",instance)
实现。
同时,独立出去的组件需要将 attrs
绑定到 Drawer 的上面。
<BasicDrawer v-bind="$attrs"> Drawer Info. </BasicDrawer>
+
openDrawer
用于打开/关闭弹窗
// true/false: 打开关闭弹窗
+// data: 传递到子组件的数据
+openDrawer(true, data);
+
closeDrawer
用于关闭弹窗
closeDrawer();
+
setDrawerProps
用于更改 drawer 的 props 参数因为 drawer 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供 setDrawerProps 方便更改内部 drawer 的 props
Props 内容可以见下方
setDrawerProps(props);
+
用于独立的 Drawer 内部调用
<template>
+ <BasicDrawer v-bind="$attrs" @register="register" title="Drawer Title" width="50%">
+ Drawer Info.
+ <a-button type="primary" @click="closeDrawer">内部关闭drawer</a-button>
+ </BasicDrawer>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
+ export default defineComponent({
+ components: { BasicDrawer },
+ setup() {
+ const [register, { closeDrawer }] = useDrawerInner();
+ return { register, closeDrawer };
+ },
+ });
+</script>
+
useModalInner用于操作独立组件
const [register, { closeModal, setModalProps }] = useModalInner(callback);
+
callback
type: (data:any)=>void
回调函数用于接收 openDrawer 第二个参数传递的值
openDrawer((data: any) => {
+ console.log(data);
+});
+
closeDrawer
用于关闭抽屉
closeDrawer();
+
changeOkLoading
用于修改确认按钮的 loading 状态
// true or false
+changeOkLoading(true);
+
changeLoading
用于修改 modal 的 loading 状态
// true or false
+changeLoading(true);
+
setDrawerProps
用于更改 drawer 的 props 参数因为 modal 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供setDrawerProps 方便更改内部 drawer 的 props
Props 内容可以见下方
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv drawer
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
isDetail | boolean | false | - | 是否为详情模式 |
loading | boolean | false | - | loading 状态 |
loadingText | string | `` | - | loading 文本 s |
showDetailBack | boolean | true | - | isDetail=true 状态下是否显示返回按钮 |
closeFunc | () => Promise<boolean> | - | - | 自定义关闭函数,返回true 关闭,否则不关闭 |
showFooter | boolean | - | - | 是否显示底部 |
footerHeight | number | 60 | - | 底部区域高度 |
事件 | 回调参数 | 说明 |
---|---|---|
close | (e)=>void | 点击关闭回调 |
visible-change | (visible:boolean)=>void | 弹窗打开关闭时触发 |
ok | (e)=>void | 点击确定回调 |
excel 导入导出操作
项目中使用到的是 XLSX,具体文档可以参考XLSX 文档
<template>
+ <ImpExcel @success="loadDataSuccess">
+ <a-button class="m-3">导入Excel</a-button>
+ </ImpExcel>
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { ImpExcel, ExcelData } from '/@/components/Excel';
+ export default defineComponent({
+ components: { ImpExcel },
+ setup() {
+ function loadDataSuccess(excelDataList: ExcelData[]) {
+ tableListRef.value = [];
+ console.log(excelDataList);
+ for (const excelData of excelDataList) {
+ const {
+ header,
+ results,
+ meta: { sheetName },
+ } = excelData;
+ const columns: BasicColumn[] = [];
+ for (const title of header) {
+ columns.push({ title, dataIndex: title });
+ }
+ tableListRef.value.push({ title: sheetName, dataSource: results, columns });
+ }
+ }
+ return {
+ loadDataSuccess,
+ };
+ },
+ });
+</script>
+
事件 | 回调参数 | 说明 |
---|---|---|
success | (res:ExcelData)=>void | 导入成功回调 |
error | ()=>void | 导出错误 |
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
header: | string[]; | table 表头 | |
results: | T[]; | table 数据 | |
meta: | { sheetName: string }; | table title |
具体详情可以参考完整版示例
import { jsonToSheetXlsx, aoaToSheetXlsx } from '/@/components/Excel';
+
import { aoaToSheetXlsx } from '/@/components/Excel';
+// 保证data顺序与header一致
+aoaToSheetXlsx({
+ data: [],
+ header: [],
+ filename: '二维数组方式导出excel.xlsx',
+});
+
import { jsonToSheetXlsx } from '/@/components/Excel';
+
+jsonToSheetXlsx({
+ data,
+ filename,
+ write2excelOpts: {
+ // 可以是 xlsx/html/csv/txt
+ bookType,
+ },
+});
+
import { jsonToSheetXlsx } from '/@/components/Excel';
+
+jsonToSheetXlsx({
+ data,
+ filename: '使用key作为默认头部.xlsx',
+});
+
+jsonToSheetXlsx({
+ data,
+ header: {
+ id: 'ID',
+ name: '姓名',
+ age: '年龄',
+ no: '编号',
+ address: '地址',
+ beginTime: '开始时间',
+ endTime: '结束时间',
+ },
+ filename: '自定义头部.xlsx',
+ json2sheetOpts: {
+ // 指定顺序
+ header: ['name', 'id'],
+ },
+});
+
方法 | 回调参数 | 返回值 | 说明 |
---|---|---|---|
jsonToSheetXlsx | Function(JsonToSheet) | json 格式数据,导出到 excel | |
aoaToSheetXlsx | Function(AoAToSheet) | 数组格式,导出到 excel |
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
data | T[] | JSON 对象数组 | |
header?: | T ; | 表头未设置则取 JSON 对象的 key 作为 header | |
filename?: | string | excel-list.xlsx | 导出的文件名 |
json2sheetOpts?: | JSON2SheetOpts | 调用 XLSX.utils.json_to_sheet 的可选参数 | |
write2excelOpts?: | WritingOptions | { bookType: 'xlsx' } | 调用 XLSX.writeFile 的可选参数,具体参 XLSX 文档 |
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
data | T[][]; | 二维数组 | |
header?: | T; | 表头 ;未设置则没有表头 | |
filename?: | string; | excel-list.xlsx | 导出的文件名 |
write2excelOpts?: | WritingOptions; | { bookType: 'xlsx' } | 调用 XLSX.writeFile 的可选参数 |
流程图组件,基于 didi/LogicFlow
的简单封装。详细配置请参考文档 FlowChart
<template>
+ <FlowChart :data="demoData" />
+</template>
+
+<script lang="ts">
+ import { FlowChart } from '/@/components/FlowChart';
+ import { PageWrapper } from '/@/components/Page';
+
+ import demoData from './dataTurbo.json';
+ export default {
+ components: { FlowChart, PageWrapper },
+ setup() {
+ return { demoData };
+ },
+ };
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
flowOptions | object | - | - | FlowCharts 配置项 |
data | object | - | - | 流程数据 |
toolbar | boolean | true | - | 是否显示工具栏 |
patternItems | [] | - | - | 左侧拖拽列表数据 |
对 antv
的 form 组件进行封装,扩展一些常用的功能
如果文档内没有,可以尝试在在线示例内寻找
下面是一个使用简单表单的示例,只有一个输入框
<template>
+ <div class="m-4">
+ <BasicForm
+ :labelWidth="100"
+ :schemas="schemas"
+ :actionColOptions="{ span: 24 }"
+ @submit="handleSubmit"
+ />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicForm, FormSchema } from '/@/components/Form';
+ import { CollapseContainer } from '/@/components/Container';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ const schemas: FormSchema[] = [
+ {
+ field: 'field',
+ component: 'Input',
+ label: '字段1',
+ colProps: {
+ span: 8,
+ },
+ defaultValue: '1',
+ componentProps: {
+ placeholder: '自定义placeholder',
+ onChange: (e) => {
+ console.log(e);
+ },
+ },
+ },
+ ];
+
+ export default defineComponent({
+ components: { BasicForm, CollapseContainer },
+ setup() {
+ const { createMessage } = useMessage();
+ return {
+ schemas,
+ handleSubmit: (values: any) => {
+ createMessage.success('click search,values:' + JSON.stringify(values));
+ },
+ };
+ },
+ });
+</script>
+
所有可调用函数见下方 Methods
说明
<template>
+ <div class="m-4">
+ <BasicForm
+ :schemas="schemas"
+ ref="formElRef"
+ :labelWidth="100"
+ @submit="handleSubmit"
+ :actionColOptions="{ span: 24 }"
+ />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { BasicForm, FormSchema, FormActionType, FormProps } from '/@/components/Form';
+ import { CollapseContainer } from '/@/components/Container';
+ const schemas: FormSchema[] = [];
+
+ export default defineComponent({
+ components: { BasicForm, CollapseContainer },
+ setup() {
+ const formElRef = ref<Nullable<FormActionType>>(null);
+ return {
+ formElRef,
+ schemas,
+ setProps(props: FormProps) {
+ const formEl = formElRef.value;
+ if (!formEl) return;
+ formEl.setProps(props);
+ },
+ };
+ },
+ });
+</script>
+
form 组件还提供了 useForm
,方便调用函数内部方法
<template>
+ <BasicForm @register="register" @submit="handleSubmit" />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+ import { CollapseContainer } from '/@/components/Container/index';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ const schemas: FormSchema[] = [
+ {
+ field: 'field1',
+ component: 'Input',
+ label: '字段1',
+ colProps: {
+ span: 8,
+ },
+ componentProps: {
+ placeholder: '自定义placeholder',
+ onChange: (e: any) => {
+ console.log(e);
+ },
+ },
+ },
+ ];
+
+ export default defineComponent({
+ components: { BasicForm, CollapseContainer },
+ setup() {
+ const { createMessage } = useMessage();
+ const [register, { setProps }] = useForm({
+ labelWidth: 120,
+ schemas,
+ actionColOptions: {
+ span: 24,
+ },
+ });
+ return {
+ register,
+ schemas,
+ handleSubmit: (values: any) => {
+ createMessage.success('click search,values:' + JSON.stringify(values));
+ },
+ setProps,
+ };
+ },
+ });
+</script>
+
const [register, methods] = useForm(props);
+
参数 props 内的值可以是 computed 或者 ref 类型
register
register 用于注册 useForm
,如果需要使用 useForm
提供的 api,必须将 register 传入组件的 onRegister
<template>
+ <BasicForm @register="register" @submit="handleSubmit" />
+</template>
+<script>
+ export default defineComponent({
+ components: { BasicForm },
+ setup() {
+ const [register] = useForm();
+ return {
+ register,
+ };
+ },
+ });
+</script>
+
Methods
见下方说明
getFieldsValue
类型: () => Recordable;
说明: 获取表单值
setFieldsValue
类型: <T>(values: T) => Promise<void>
说明: 设置表单字段值
resetFields
类型: ()=> Promise<void>
说明: 重置表单值
validateFields
类型: (nameList?: NamePath[]) => Promise<any>
说明: 校验指定表单项
validate
类型: (nameList?: NamePath[]) => Promise<any>
说明: 校验整个表单
submit
类型: () => Promise<void>
说明: 提交表单
scrollToField
类型: (name: NamePath, options?: ScrollOptions) => Promise<void>
说明: 滚动到对应字段位置
clearValidate
类型: (name?: string | string[]) => Promise<void>
说明: 清空校验
setProps
TIP
设置表单的 props 可以直接在标签上传递,也可以使用 setProps,或者初始化直接写 useForm(props)
类型: (formProps: Partial<FormProps>) => Promise<void>
说明: 设置表单 Props
removeSchemaByField
类型: (field: string | string[]) => Promise<void>
说明: 根据 field 删除 Schema
appendSchemaByField
类型: ( schema: FormSchema, prefixField: string | undefined, first?: boolean | undefined ) => Promise<void>
说明: 插入到指定 filed 后面,如果没传指定 field,则插入到最后,当 first = true 时插入到第一个位置
updateSchema
类型: (data: Partial<FormSchema> | Partial<FormSchema>[]) => Promise<void>
说明: 更新表单的 schema, 只更新函数所传的参数
e.g
updateSchema({ field: 'filed', componentProps: { disabled: true } });
+updateSchema([
+ { field: 'filed', componentProps: { disabled: true } },
+ { field: 'filed1', componentProps: { disabled: false } },
+]);
+
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv form
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
schemas | Schema[] | - | - | 表单配置,见下方 FormSchema 配置 | |
submitOnReset | boolean | false | - | 重置时是否提交表单 | |
labelCol | Partial<ColEx> | - | - | 整个表单通用 LabelCol 配置 | |
wrapperCol | Partial<ColEx> | - | - | 整个表单通用 wrapperCol 配置 | |
baseColProps | Partial<ColEx> | - | - | 配置所有选子项的 ColProps,不需要逐个配置,子项也可单独配置优先与全局 | |
baseRowStyle | object | - | - | 配置所有 Row 的 style 样式 | |
labelWidth | number , string | - | - | 扩展 form 组件,增加 label 宽度,表单内所有组件适用,可以单独在某个项覆盖或者禁用 | |
labelAlign | string | - | left ,right | label 布局 | |
mergeDynamicData | object | - | - | 额外传递到子组件的参数 values | |
autoFocusFirstItem | boolean | false | - | 是否聚焦第一个输入框,只在第一个表单项为 input 的时候作用 | |
compact | boolean | false | true/false | 紧凑类型表单,减少 margin-bottom | |
size | string | default | 'default' , 'small' , 'large' | 向表单内所有组件传递 size 参数,自定义组件需自行实现 size 接收 | |
disabled | boolean | false | true/false | 向表单内所有组件传递 disabled 属性,自定义组件需自行实现 disabled 接收 | |
autoSetPlaceHolder | boolean | true | true/false | 自动设置表单内组件的 placeholder,自定义组件需自行实现 | |
autoSubmitOnEnter | boolean | false | true/false | 在 input 中输入时按回车自动提交 | 2.4.0 |
rulesMessageJoinLabel | boolean | false | true/false | 如果表单项有校验,会自动生成校验信息,该参数控制是否将字段中文名字拼接到自动生成的信息后方 | |
showAdvancedButton | boolean | false | true/false | 是否显示收起展开按钮 | |
emptySpan | number , Partial<ColEx> | 0 | - | 空白行格,可以是数值或者 col 对象 数 | |
autoAdvancedLine | number | 3 | - | 如果 showAdvancedButton 为 true,超过指定行数行默认折叠 | |
alwaysShowLines | number | 1 | - | 折叠时始终保持显示的行数 | 2.7.1 |
showActionButtonGroup | boolean | true | true/false | 是否显示操作按钮(重置/提交) | |
actionColOptions | Partial<ColEx> | - | - | 操作按钮外层 Col 组件配置,如果开启 showAdvancedButton,则不用设置,具体见下方 actionColOptions | |
showResetButton | boolean | true | - | 是否显示重置按钮 | |
resetButtonOptions | object | - | 重置按钮配置见下方 ActionButtonOption | ||
showSubmitButton | boolean | true | - | 是否显示提交按钮 | |
submitButtonOptions | object | - | 确认按钮配置见下方 ActionButtonOption | ||
resetFunc | () => Promise<void> | - | 重置表单行为前执行自定义重置按钮逻辑() => Promise<void>; | ||
submitFunc | () => Promise<void> | - | 自定义提交按钮逻辑() => Promise<void>; | ||
fieldMapToTime | [string, [string, string], string?][] | 'timestamp' ,'timestampStartDay' ,momentjs 时间格式 | 用于将表单内时间区域的应设成 2 个字段,见下方说明 |
见src/components/Form/src/types/index.ts
export interface ButtonProps extends BasicButtonProps {
+ text?: string;
+}
+
将表单内时间区域的值映射成 2 个字段
如果表单内有时间区间组件,获取到的值是一个数组,但是往往我们传递到后台需要是 2 个字段
useForm({
+ fieldMapToTime: [
+ // data为时间组件在表单内的字段,startTime,endTime为转化后的开始时间与结束时间
+ // 'YYYY-MM-DD'为时间格式,参考moment
+ ['datetime', ['startTime', 'endTime'], 'YYYY-MM-DD'],
+ // 支持多个字段
+ ['datetime1', ['startTime1', 'endTime1'], 'YYYY-MM-DD HH:mm:ss'],
+ ],
+});
+
+// fieldMapToTime没写的时候表单获取到的值
+{
+ datetime: [Date(),Date()]
+}
+// ['datetime', ['startTime', 'endTime'], 'YYYY-MM-DD'],等同于 dayjs(Date()).format('YYYY-MM-DD'). 之后
+{
+ startTime: '2020-08-12',
+ endTime: '2020-08-15',
+}
+
+// ['datetime', ['startTime', 'endTime'], 'timestamp'],等同于 dayjs(Date()).unix(). 之后
+{
+ startTime: 1597190400,
+ endTime: 1597449600,
+}
+
+// ['datetime', ['startTime', 'endTime'], 'timestampStartDay'],等同于 dayjs(Date()).startOf('day').unix(). 之后
+{
+ startTime: 1597190400,
+ endTime: 1597449600,
+}
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
field | string | - | - | 字段名 |
label | string | - | - | 标签名 |
subLabel | string | - | - | 二级标签名灰色 |
suffix | string , number , ((values: RenderCallbackParams) => string / number); | - | - | 组件后面的内容 |
changeEvent | string | - | - | 表单更新事件名称 |
helpMessage | string , string[] | - | - | 标签名右侧温馨提示 |
helpComponentProps | HelpComponentProps | - | - | 标签名右侧温馨提示组件 props,见下方 HelpComponentProps |
labelWidth | string , number | - | - | 覆盖统一设置的 labelWidth |
disabledLabelWidth | boolean | false | true/false | 禁用 form 全局设置的 labelWidth,自己手动设置 labelCol 和 wrapperCol |
component | string | - | - | 组件类型,见下方 ComponentType |
componentProps | any,()=>{} | - | - | 所渲染的组件的 props |
rules | ValidationRule[] | - | - | 校验规则,见下方 ValidationRule |
required | boolean | - | - | 简化 rules 配置,为 true 则转化成 [{required:true}]。2.4.0 之前的版本只支持 string 类型的值 |
rulesMessageJoinLabel | boolean | false | - | 校验信息是否加入 label |
itemProps | any | - | - | 参考下方 FormItem |
colProps | ColEx | - | - | 参考上方 actionColOptions |
defaultValue | object | - | - | 所渲渲染组件的初始值 |
render | (renderCallbackParams: RenderCallbackParams) => VNode / VNode[] / string | - | - | 自定义渲染组件 |
renderColContent | (renderCallbackParams: RenderCallbackParams) => VNode / VNode[] / string | - | - | 自定义渲染组件(需要自行包含 formItem) |
renderComponentContent | (renderCallbackParams: RenderCallbackParams) => any / string | - | - | 自定义渲染组内部的 slot |
slot | string | - | - | 自定义 slot,渲染组件 |
colSlot | string | - | - | 自定义 slot,渲染组件 (需要自行包含 formItem) |
show | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判断当前组件是否显示,css 控制,不会删除 dom |
ifShow | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判断当前组件是否显示,js 控制,会删除 dom |
dynamicDisabled | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判断当前组件是否禁用 |
dynamicRules | boolean / ((renderCallbackParams: RenderCallbackParams) => boolean) | - | - | 动态判返当前组件你校验规则 |
RenderCallbackParams
export interface RenderCallbackParams {
+ schema: FormSchema;
+ values: any;
+ model: any;
+ field: string;
+}
+
componentProps
当值为对象类型时,该对象将作为component
所对应组件的的 props 传入组件
当值为一个函数时候
参数有 4 个
schema
: 表单的整个 schemas
formActionType
: 操作表单的函数。与 useForm 返回的操作函数一致
formModel
: 表单的双向绑定对象,这个值是响应式的。所以可以方便处理很多操作
tableAction
: 操作表格的函数,与 useTable 返回的操作函数一致。注意该参数只在表格内开启搜索表单的时候有值,其余情况为null
,
{
+ // 简单例子,值改变的时候操作表格或者修改表单内其他元素的值
+ component:'Input',
+ componentProps: ({ schema, tableAction, formActionType, formModel }) => {
+ return {
+ // xxxx props
+ onChange:e=>{
+ const {reload}=tableAction
+ reload()
+ // or
+ formModel.xxx='123'
+ }
+ };
+ };
+}
+
HelpComponentProps
export interface HelpComponentProps {
+ maxWidth: string;
+ // 是否显示序号
+ showIndex: boolean;
+ // 文本列表
+ text: any;
+ // 颜色
+ color: string;
+ // 字体大小
+ fontSize: string;
+ icon: string;
+ absolute: boolean;
+ // 定位
+ position: any;
+}
+
ComponentType
schema 内组件的可选类型
export type ComponentType =
+ | 'Input'
+ | 'InputGroup'
+ | 'InputPassword'
+ | 'InputSearch'
+ | 'InputTextArea'
+ | 'InputNumber'
+ | 'InputCountDown'
+ | 'Select'
+ | 'ApiSelect'
+ | 'TreeSelect'
+ | 'RadioButtonGroup'
+ | 'RadioGroup'
+ | 'Checkbox'
+ | 'CheckboxGroup'
+ | 'AutoComplete'
+ | 'Cascader'
+ | 'DatePicker'
+ | 'MonthPicker'
+ | 'RangePicker'
+ | 'WeekPicker'
+ | 'TimePicker'
+ | 'Switch'
+ | 'StrengthMeter'
+ | 'Upload'
+ | 'IconPicker'
+ | 'Render'
+ | 'Slider'
+ | 'Rate'
+ | 'Divider'; // v2.7.2新增
+
Divider
类型用于在schemas
中占位,将会渲染成一个分割线(始终占一整行的版面),可以用于较长表单的版面分隔。请只将 Divider 类型的 schema 当作一个分割线,而不是一个常规的表单字段。
Divider
仅在showAdvancedButton
为 false 时才会显示(也就是说如果启用了表单收起和展开功能,Divider
将不会显示)Divider
使用schema
中的label
以及helpMessage
来渲染分割线中的提示内容Divider
可以使用componentProps
来设置除type
之外的 propsDivider
不会渲染AFormItem
,因此schema
中除label
、componentProps
、helpMessage
、helpComponentProps
以外的属性不会被用到在 src/components/Form/src/componentMap.ts
内,添加需要的组件,并在上方 ComponentType 添加相应的类型 key
这种写法适用与适用频率较高的组件
componentMap.set('componentName', 组件);
+
+// ComponentType
+export type ComponentType = xxxx | 'componentName';
+
使用 useComponentRegister 进行注册
这种写法只能在当前页使用,页面销毁之后会从 componentMap 删除相应的组件
import { useComponentRegister } from '@/components/form/index';
+
+import { StrengthMeter } from '@/components/strength-meter/index';
+
+useComponentRegister('StrengthMeter', StrengthMeter);
+
提示
方式 2 出现的原因是为了减少打包体积,如果某个组件体积很大,用方式 1 的话可能会使首屏体积增加
自定义渲染内容
<template>
+ <div class="m-4">
+ <BasicForm @register="register" @submit="handleSubmit" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, h } from 'vue';
+ import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ import { Input } from 'ant-design-vue';
+ const schemas: FormSchema[] = [
+ {
+ field: 'field1',
+ component: 'Input',
+ label: '字段1',
+ colProps: {
+ span: 8,
+ },
+ rules: [{ required: true }],
+ render: ({ model, field }) => {
+ return h(Input, {
+ placeholder: '请输入',
+ value: model[field],
+ onChange: (e: ChangeEvent) => {
+ model[field] = e.target.value;
+ },
+ });
+ },
+ },
+ {
+ field: 'field2',
+ component: 'Input',
+ label: '字段2',
+ colProps: {
+ span: 8,
+ },
+ rules: [{ required: true }],
+ renderComponentContent: () => {
+ return {
+ suffix: () => 'suffix',
+ };
+ },
+ },
+ ];
+ export default defineComponent({
+ components: { BasicForm },
+ setup() {
+ const { createMessage } = useMessage();
+ const [register, { setProps }] = useForm({
+ labelWidth: 120,
+ schemas,
+ actionColOptions: {
+ span: 24,
+ },
+ });
+ return {
+ register,
+ schemas,
+ handleSubmit: (values: any) => {
+ createMessage.success('click search,values:' + JSON.stringify(values));
+ },
+ setProps,
+ };
+ },
+ });
+</script>
+
自定义渲染内容
提示
使用插槽自定义表单域时,请注意 antdv 有关 FormItem 的相关说明。
<template>
+ <div class="m-4">
+ <BasicForm @register="register">
+ <template #customSlot="{ model, field }">
+ <a-input v-model:value="model[field]" />
+ </template>
+ </BasicForm>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'compatible-vue';
+ import { BasicForm, useForm } from '@/components/Form/index';
+ import { BasicModal } from '@/components/modal/index';
+ export default defineComponent({
+ name: 'FormDemo',
+ setup(props) {
+ const [register] = useForm({
+ labelWidth: 100,
+ actionColOptions: {
+ span: 24,
+ },
+ schemas: [
+ {
+ field: 'field1',
+ label: '字段1',
+ slot: 'customSlot',
+ },
+ ],
+ });
+ return {
+ register,
+ };
+ },
+ });
+</script>
+
自定义显示/禁用
<template>
+ <div class="m-4">
+ <BasicForm @register="register" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicForm, FormSchema, useForm } from '/@/components/Form/index';
+ const schemas: FormSchema[] = [
+ {
+ field: 'field1',
+ component: 'Input',
+ label: '字段1',
+ colProps: {
+ span: 8,
+ },
+ show: ({ values }) => {
+ return !!values.field5;
+ },
+ },
+ {
+ field: 'field2',
+ component: 'Input',
+ label: '字段2',
+ colProps: {
+ span: 8,
+ },
+ ifShow: ({ values }) => {
+ return !!values.field6;
+ },
+ },
+ {
+ field: 'field3',
+ component: 'DatePicker',
+ label: '字段3',
+ colProps: {
+ span: 8,
+ },
+ dynamicDisabled: ({ values }) => {
+ return !!values.field7;
+ },
+ },
+ ];
+
+ export default defineComponent({
+ components: { BasicForm },
+ setup() {
+ const [register, { setProps }] = useForm({
+ labelWidth: 120,
+ schemas,
+ actionColOptions: {
+ span: 24,
+ },
+ });
+ return {
+ register,
+ schemas,
+ setProps,
+ };
+ },
+ });
+</script>
+
名称 | 说明 |
---|---|
formFooter | 表单底部区域 |
formHeader | 表单顶部区域 |
resetBefore | 重置按钮前 |
submitBefore | 提交按钮前 |
advanceBefore | 展开按钮前 |
advanceAfter | 展开按钮后 |
远程下拉加载组件,该组件可以用于学习参考如何自定义组件集成到 Form 组件内,将自定义组件交由 Form 去管理
const schemas: FormSchema[] = [
+ {
+ field: 'field',
+ component: 'ApiSelect',
+ label: '字段',
+ },
+];
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
numberToString | boolean | false | 是否将number 值转化为string |
api | ()=>Promise<{ label: string; value: string; disabled?: boolean }[]> | - | 数据接口,接受一个 Promise 对象 |
params | object | - | 接口参数。此属性改变时会自动重新加载接口数据 |
resultField | string | - | 接口返回的字段,如果接口返回数组,可以不填。支持x.x.x 格式 |
labelField | string | label | 下拉数组项内label 显示文本的字段,支持x.x.x 格式 |
valueField | string | value | 下拉数组项内value 实际值的字段,支持x.x.x 格式 |
immediate | boolean | true | 是否立即请求接口,否则将在第一次点击时候触发请求 |
远程下拉树加载组件,和ApiSelect
类似,2.6.1 以上版本
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
api | ()=>Promise<{ label: string; value: string; children?: any[] }[]> | - | 数据接口,接受一个 Promise 对象 |
params | object | - | 接口参数。此属性改变时会自动重新加载接口数据 |
resultField | string | - | 接口返回的字段,如果接口返回数组,可以不填。支持x.x.x 格式 |
immediate | boolean | true | 是否立即请求接口。 |
Radio Button 风格的选择按钮
const schemas: FormSchema[] = [
+ {
+ field: 'field',
+ component: 'RadioButtonGroup',
+ label: '字段',
+ },
+];
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
options | { label: string; value: string; disabled?: boolean }[] | - | 数据字段 |
函数式创建右键菜单组件, 只要能拿到 dom 的 event
对象就能为其创建右键菜单。
<template>
+ <div>
+ <a-button type="primary" @contextmenu="handleContext">Right Click on me</a-button>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { useContextMenu } from '/@/hooks/web/useContextMenu';
+ import { CollapseContainer } from '/@/components/Container';
+ import { useMessage } from '/@/hooks/web/useMessage';
+ export default defineComponent({
+ components: { CollapseContainer },
+ setup() {
+ const [createContextMenu] = useContextMenu();
+ const { createMessage } = useMessage();
+ function handleContext(e: MouseEvent) {
+ createContextMenu({
+ event: e,
+ items: [
+ {
+ label: 'New',
+ icon: 'ant-design:plus-outlined',
+ handler: () => {
+ createMessage.success('click new');
+ },
+ },
+ {
+ label: 'Open',
+ icon: 'ant-design:folder-open-filled',
+ handler: () => {
+ createMessage.success('click open');
+ },
+ },
+ ],
+ });
+ }
+ return { handleContext };
+ },
+ });
+</script>
+
Options
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
event | Event | - | - | 需要创建的 dom 的 Event 对象 |
items | ContextMenuItem[] | - | - | 右键菜单列表,ContextMenuItem 见下方说明 |
ContextMenuItem
属性 | 类型 | 说明 |
---|---|---|
label | string | 文本 |
icon | string | 图标,参考图标组件 |
disabled | boolean | 是否禁用 |
handler | ()=>void | 点击触发函数 |
<template>
+ <div class="p-5" ref="wrapEl" v-loading="loadingRef" loading-tip="加载中...">
+ <a-alert message="函数方式" />
+
+ <a-button class="my-4 mr-4" type="primary" @click="openFnFullLoading">全屏 Loading</a-button>
+ <a-button class="my-4" type="primary" @click="openFnWrapLoading">容器内 Loading</a-button>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, reactive, toRefs, ref } from 'vue';
+ import { Loading, useLoading } from '/@/components/Loading';
+ export default defineComponent({
+ components: { Loading },
+ setup() {
+ const [openFullLoading, closeFullLoading] = useLoading({
+ tip: '加载中...',
+ });
+
+ const [openWrapLoading, closeWrapLoading] = useLoading({
+ target: wrapEl,
+ props: {
+ tip: '加载中...',
+ absolute: true,
+ },
+ });
+
+ function openFnFullLoading() {
+ openFullLoading();
+
+ setTimeout(() => {
+ closeFullLoading();
+ }, 2000);
+ }
+
+ function openFnWrapLoading() {
+ openWrapLoading();
+
+ setTimeout(() => {
+ closeWrapLoading();
+ }, 2000);
+ }
+
+ return {
+ openFnFullLoading,
+ openFnWrapLoading,
+ ...toRefs(compState),
+ };
+ },
+ });
+</script>
+
使用
import { useLoading } from '/@/components/Loading';
+
+const [open, close, setTip] = useLoading(opt: Partial<LoadingProps> | Partial<UseLoadingOptions>);
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
target | HTMLElement or Ref<HTMLElement> | - | - | 挂载的 dom 节点 |
props | LoadingProps | - | - | loading 组件参数 |
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
tip | string | - | - | 加载文本 |
size | default, small , large | default | - | 大小 |
absolute | boolean | false | - | 绝对定位,为 false 时可以全屏 |
loading | boolean | - | - | 当前加载状态 |
background | string | - | - | 背景色, |
theme | 'dark' or 'light' | light | - | 背景色主题 ,当背景色不为空时使用背景色 |
open
打开 loading
close
关闭 loading
setTip
设置加在提示文案(v2.6.2以上版本)
将图片预览组件组件函数化。通过函数方便创建组件
<template>
+ <div class="p-4">
+ <Alert message="有预览图" type="info" />
+ <div class="flex justify-center mt-4">
+ <img :src="img" v-for="img in imgList" :key="img" class="mr-2" @click="handleClick(img)" />
+ </div>
+ <Alert message="无预览图" type="info" />
+ <a-button @click="handlePreview" type="primary" class="mt-4">预览图片</a-button>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { Alert } from 'ant-design-vue';
+ import { createImgPreview } from '/@/components/Preview/index';
+ const imgList: string[] = [
+ 'https://picsum.photos/id/66/346/216',
+ 'https://picsum.photos/id/67/346/216',
+ 'https://picsum.photos/id/68/346/216',
+ ];
+ export default defineComponent({
+ components: { Alert },
+ setup() {
+ function handleClick(img: string) {
+ createImgPreview({ imageList: [img] });
+ }
+
+ function handlePreview() {
+ createImgPreview({ imageList: imgList });
+ }
+ return { imgList, handleClick, handlePreview };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
imgList | string[] | - | - | 图片列表 |
index | number | 0 | - | 初始预览时的图片索引 |
scaleStep | number | - | - | 缩放步进值(每次缩放的幅度)。默认为自动(当前缩放值的10%) |
defaultWidth | number | - | - | 默认宽度(单位px)。当提供此值时,所有图片初始时都会被缩放至此宽度 |
maskClosable | boolean | false | true/false | 点击遮罩时是否自动关闭预览 |
rememberState | boolean | false | true/false | 是否记住每张图片各自的缩放状态 |
onImgLoad | ({ index: number, url: string, dom: HTMLImageElement }) => void | - | - | 图片加载成功时的回调函数 |
onImgError | ({ index: number, url: string, dom: HTMLImageElement }) => void | - | - | 图片加载失败时的回调函数 |
可用于控制当前预览状态
interface PreviewActions {
+ // 重置状态
+ resume: () => void;
+ // 关闭预览
+ close: () => void;
+ // 显示前一张
+ prev: () => void;
+ // 显示后一张
+ next: () => void;
+ // 设置缩放比例(针对当前图片)
+ setScale: (scale: number) => void;
+ // 设置旋转角度(针对当前图片)
+ setRotate: (rotate: number) => void;
+}
+
二次封装按钮组件,且使用相同的组件名替换全局的 a-button
组件
TIP
a-button
标签即可<template>
+ <a-button color="success">成功按钮</a-button>
+ <a-button color="error">错误按钮</a-button>
+ <a-button color="warning">警告按钮</a-button>
+</template>
+
提示
保持 ant design button 组件 原有功能的情况下扩展以下属性
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
color | 'error','warning', 'success' | - | 按钮的颜色场景状态颜色, |
preIcon | string | - | 按钮文本前图标,参考 Icon 组件 |
postIcon | string | - | 按钮文本后图标,参考 Icon 组件 |
iconSize | number | 14 | 按钮图标大小 |
用于项目内组件的展示,基本支持所有图标库(支持按需加载,只打包所用到的图标)
icon 组件位于 src/components/Icon 内
<template>
+ <Icon icon="gg:loadbar-doc"></Icon>
+</template>
+
+<script>
+ import { defineComponent } from 'vue';
+ import { Icon } from '/@/components/Icon';
+ export default defineComponent({
+ components: { Icon },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
icon | string | - | 图标名 |
color | string | - | 图标颜色 |
size | number | 16 | 图标大小 |
prefix | string | - | 图标前缀 |
提示
如果 icon
值以 |svg
结尾,则会渲染成 SvgIcon 组件
用于使用项目 svg 雪碧图
<template>
+ <div>
+ <SvgIcon name="test"> </SvgIcon>
+ </div>
+</template>
+<script>
+ import { SvgIcon } from '/@/components/Icon';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { SvgIcon },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
name | string | - | svg 图标名 |
size | number | 16 | 图标大小 |
本组件详细说明请参阅图标选择器
<template>
+ <div>
+ <IconPicker />
+ </div>
+</template>
+<script>
+ import { IconPicker } from '/@/components/Icon';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { IconPicker },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
width | string | 100% | 宽度 |
pageSize | number | 140 | 每页显示的图标数 |
copy | boolean | false | 是否可以复制 |
mode | string | iconify | 备选图标池,为 svg 时,会读取所有 svg sprite 图标。详见下方说明 |
mode 说明
mode
为iconify
时,会使用预生成的图标集数据作为备选图标池mode
为svg
时,会使用 /src/assets/icons
下的所有svg图标(可包含一级子目录)作为备选图标池,详见vite-plugin-svg-icons。注意事项
组件的 defaultXXX
属性不要使用,ant-design-vue 2.2
版本之后将会逐步移除。二次封装的组件也不兼容 defaultXXX
属性。
该项目的组件大部分没有进行全局注册。采用了按需引入注册方式,如下
<template>
+ <ConfigProvider>
+ <router-view />
+ </ConfigProvider>
+</template>
+
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { ConfigProvider } from 'ant-design-vue';
+ export default defineComponent({
+ name: 'App',
+ components: { ConfigProvider },
+ });
+</script>
+
json 数据预览组件
<template>
+ <JsonPreview :data="data" />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { JsonPreview } from '/@/components/CodeEditor';
+
+ export default defineComponent({
+ components: { JsonPreview },
+ setup() {
+ return {
+ data: {},
+ };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
data | object | - | - | 需要预览的 Json 数据 |
延时加载/懒加载组件, 只在组件可见或者延迟一段时间才进行加载
<template>
+ <div class="p-4 lazy-base-demo">
+ <div class="lazy-base-demo-wrap">
+ <h1>向下滚动</h1>
+ <LazyContainer @init="() => {}">
+ <TargetContent />
+ <template #skeleton>
+ <Skeleton :rows="10" />
+ </template>
+ </LazyContainer>
+ </div>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { Skeleton } from 'ant-design-vue';
+ import TargetContent from './TargetContent.vue';
+ import { LazyContainer } from '/@/components/Container/index';
+ export default defineComponent({
+ components: { LazyContainer, TargetContent, Skeleton },
+ });
+</script>
+<style lang="less" scoped>
+ .lazy-base-demo {
+ &-wrap {
+ display: flex;
+ width: 50%;
+ height: 2000px;
+ margin: 20px auto;
+ text-align: center;
+ background: #fff;
+ justify-content: center;
+ flex-direction: column;
+ align-items: center;
+ }
+
+ h1 {
+ height: 1300px;
+ margin: 20px 0;
+ }
+ }
+</style>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
timeout | number | - | - | 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 |
viewport | HTMLElement | - | - | 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 |
threshold | string | 0px | - | 预加载阈值, css 单位 |
direction | 'vertical', 'horizontal' , vertical | - | 视口的滚动方向, vertical 代表垂直方向,horizontal 代表水平方向 | |
tag | string' | div | - | 包裹组件的外层容器的标签名 |
transitionName | string' | lazy-container | - | transition 动画 name |
maxWaitingTime | number' | 80 | - | 最大等待时间 |
事件 | 回调参数 | 说明 |
---|---|---|
init | ()=>void | 初始化之后 |
名称 | 说明 |
---|---|
default | 默认区域 |
skeleton | 懒加载骨架屏 |
<template>
+ <div class="p-5" ref="wrapEl" v-loading="loadingRef" loading-tip="加载中...">
+ <a-button class="my-4 mr-4" type="primary" @click="openCompFullLoading">全屏 Loading</a-button>
+ <a-button class="my-4" type="primary" @click="openCompAbsolute">容器内 Loading</a-button>
+ <Loading :loading="loading" :absolute="absolute" :tip="tip" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, reactive, toRefs, ref } from 'vue';
+ import { Loading } from '/@/components/Loading';
+ export default defineComponent({
+ components: { Loading },
+ setup() {
+ const compState = reactive({
+ absolute: false,
+ loading: false,
+ tip: '加载中...',
+ });
+
+ function openLoading(absolute: boolean) {
+ compState.absolute = absolute;
+ compState.loading = true;
+ setTimeout(() => {
+ compState.loading = false;
+ }, 2000);
+ }
+
+ function openCompFullLoading() {
+ openLoading(false);
+ }
+
+ function openCompAbsolute() {
+ openLoading(true);
+ }
+
+ return {
+ openCompFullLoading,
+ openCompAbsolute,
+ ...toRefs(compState),
+ };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
tip | string | - | - | 加载文本 |
size | default, small , large | default | - | 大小 |
absolute | boolean | false | - | 绝对定位,为 false 时可以全屏 |
loading | boolean | - | - | 当前加载状态 |
background | string | - | - | 背景色 |
theme | 'dark' or 'light' | light | - | 背景色主题,当背景色不为空时使用背景色 |
基于 Vditor 的 MarkDown 编辑器
<template>
+ <div class="p-4">
+ <a-button @click="toggleTheme" class="mb-2" type="primary">黑暗主题</a-button>
+ <MarkDown v-model:value="value" ref="markDownRef" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, ref, unref } from 'vue';
+ import { MarkDown, MarkDownActionType } from '/@/components/Markdown';
+ export default defineComponent({
+ components: { MarkDown },
+ setup() {
+ const markDownRef = ref<Nullable<MarkDownActionType>>(null);
+ const valueRef = ref(`
+# title
+
+# content
+`);
+
+ function toggleTheme() {
+ const markDown = unref(markDownRef);
+ if (!markDown) return;
+ const vditor = markDown.getVditor();
+ vditor.setTheme('dark');
+ }
+ return {
+ value: valueRef,
+ toggleTheme,
+ markDownRef,
+ };
+ },
+ });
+</script>
+
TIP
除以下两个外,props 还可以传入 vidtor 的所有属性。可用 v-bind 统一绑定
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
v-model | string | - | - | 双向绑定文本值 |
height | number | - | - | 高度 |
名称 | 回调参数 | 说明 |
---|---|---|
getVditor | Function | 获取 vditor 实例 |
对 antv 的 modal 组件进行封装,扩展拖拽,全屏,自适应高度等功能
代码路径 src/components/Modal
由于弹窗内代码一般作为单文件组件存在,也推荐这样做,所以示例都为单文件组件形式
TIP
注意 v-bind="$attrs"
记得写,用于将弹窗组件的 attribute
传入 BasicModal
组件
// Modal.vue
+<template>
+ <BasicModal v-bind="$attrs" title="Modal Title" :helpMessage="['提示1', '提示2']">
+ Modal Info.
+ </BasicModal>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicModal } from '/@/components/Modal';
+ export default defineComponent({
+ components: { BasicModal },
+ setup() {
+ return {};
+ },
+ });
+</script>
+
页面引用弹窗
// Page.vue
+<template>
+ <div class="px-10">
+ <Modal @register="register" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { useModal } from '/@/components/Modal';
+ import Modal from './Modal.vue';
+ export default defineComponent({
+ components: { Modal },
+ setup() {
+ const [register, { openModal }] = useModal();
+ return {
+ register,
+ openModal,
+ };
+ },
+ });
+</script>
+
用于外部组件调用
useModal 用于操作组件
const [register, { openModal, setModalProps }] = useModal();
+
register
register 用于注册 useModal
,如果需要使用 useModal
提供的 api,必须将 register
传入组件的 onRegister
。
原理其实很简单,就是 vue 的组件子传父通信,内部通过 emit("register",instance)
实现。
同时独立出去的组件需要将 attrs
绑定到 BasicModal
上面。
<template>
+ <BasicModal v-bind="$attrs"></BasicModal>
+</template>
+
openModal
用于打开/关闭弹窗
// true/false: 打开关闭弹窗
+// data: 传递到子组件的数据
+openModal(true, data);
+
closeModal
用于关闭弹窗
closeModal();
+
setModalProps
用于更改 modal 的 props 参数因为 modal 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供 setModalProps 方便更改内部 modal 的 props
Props 内容可以见下方
setModalProps(props);
+
用于独立的 Modal 内部调用
<template>
+ <BasicModal
+ v-bind="$attrs"
+ @register="register"
+ title="Modal Title"
+ :helpMessage="['提示1', '提示2']"
+ >
+ <a-button type="primary" @click="closeModal" class="mr-2">从内部关闭弹窗</a-button>
+
+ <a-button type="primary" @click="setModalProps">从内部修改title</a-button>
+ </BasicModal>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicModal, useModalInner } from '/@/components/Modal';
+ export default defineComponent({
+ components: { BasicModal },
+ setup() {
+ const [register, { closeModal, setModalProps }] = useModalInner();
+ return {
+ register,
+ closeModal,
+ setModalProps: () => {
+ setModalProps({ title: 'Modal New Title' });
+ },
+ };
+ },
+ });
+</script>
+
useModalInner用于操作独立组件
const [register, { closeModal, setModalProps }] = useModalInner(callback);
+
callback
type: (data:any)=>void
回调函数用于接收 openModal 第二个参数传递的值
useModal((data: any) => {
+ console.log(data);
+});
+
closeModal
用于关闭弹窗
closeModal();
+
changeOkLoading
用于修改确认按钮的 loading 状态
changeOkLoading(true);
+
changeLoading
用于修改 modal 的 loading 状态
// true or false
+changeLoading(true);
+
setModalProps
用于更改 modal 的 props 参数因为 modal 内容独立成组件,如果在外部页面需要更改 props 可能比较麻烦,所以提供 setModalProps 方便更改内部 modal 的 props
Props 内容可以见下方
TIP
除以下参数外,组件库文档内的 props 也都支持,具体可以参考 antv modal
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
title | string | - | - | modal 标题 |
height | number | - | - | 固定 modal 的高度 |
minHeight | number | - | - | 设置 modal 的最小高度 |
draggable | boolean | true | true/false | 是否开启拖拽 |
useWrapper | boolean | true | true/false | 是否开启自适应高度,开启后会跟随屏幕变化自适应内容,并出现滚动条 |
wrapperFooterOffset | number | 0 | - | 开启是适应高度后,如果超过屏幕高度,底部和顶部会保持一样的间距,该参数可以用来缩小底部的间距 |
canFullscreen | boolean | true | true/false | 是否可以进行全屏 |
defaultFullscreen | boolean | false | true/false | 默认全屏 |
loading | boolean | false | true/false | loading 状态 |
loadingTip | string | - | - | loading 文本 |
showCancelBtn | boolean | true | true/false | 显示关闭按钮 |
showOkBtn | boolean | true | true/false | 显示确认按钮 |
helpMessage | string , string[] | - | - | 标题右侧提示文本 |
centered | boolean | false | true/false | 是否居中弹窗 |
cancelText | string | '关闭' | - | 关闭按钮文本 |
okText | string | '保存' | - | 确认按钮文本 |
closeFunc | () => Promise<boolean> | 关闭函数 | - | 关闭前执行,返回 true 则关闭,否则不关闭 |
事件 | 回调参数 | 说明 |
---|---|---|
ok | function(e) | 点击确定回调 |
cancel | function(e) | 点击取消回调 |
visible-change | (visible:boolean)=>{} | 打开或者关闭触发 |
名称 | 说明 |
---|---|
default | 默认区域 |
footer | 底部区域(会替换掉默认的按钮) |
insertFooter | 关闭按钮的左边(不使用footer插槽时有效) |
centerFooter | 关闭按钮和确认按钮的中间(不使用footer插槽时有效) |
appendFooter | 确认按钮的右边(不使用footer插槽时有效) |
页面相关组件
用于包裹页面组件
<template>
+ <div>
+ <PageWrapper>
+ <template #left>left</template>
+ <template #right>right</template>
+ </PageWrapper>
+ </div>
+</template>
+<script>
+ import { PageWrapper } from '/@/components/Page';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { PageWrapper },
+ setup() {
+ return {};
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
title | string | - | pageHeader title |
dense | 是否缩小主体区域 | false | 为 true 将会取消 padding/margin |
content | string | - | pageHeader Content 内容 |
contentStyle | object | - | 主体区域样式 |
contentClass | string | - | 主体区域 class |
contentBackground | boolean | - | 主体区域背景 |
contentFullHeight | boolean | false | 主体区域是否占满整个屏幕高度 |
fixedHeight | boolean | false | 固定主体区域高度 |
pageHeader 的 slot 都支持
名称 | 说明 |
---|---|
leftFooter | PageFooter 左侧区域 |
rightFooter | PageFooter 右侧区域 |
headerContent | pageHeader 主体内容 |
default | 主体区域 |
用于页面底部工具栏
<template>
+ <div>
+ <PageFooter>
+ <template #left>left</template>
+ <template #right>right</template>
+ </PageFooter>
+ </div>
+</template>
+<script>
+ import { PageFooter } from '/@/components/Page';
+ import { defineComponent } from 'vue';
+ export default defineComponent({
+ components: { PageFooter },
+ setup() {
+ return {};
+ },
+ });
+</script>
+
名称 | 说明 |
---|---|
left | 左侧区域 |
right | 右侧区域 |
带有 PopConfirm 下拉菜单功能的按钮
<template>
+ <PopConfirmButton>按钮文本</PopConfirmButton>
+</template>
+
+<script>
+ import { defineComponent } from 'vue';
+ import { PopConfirmButton } from '/@/components/Button';
+ export default defineComponent({
+ components: { PopConfirmButton },
+ });
+</script>
+
提示
保持 anv design popconfirm 组件 原有功能的情况下扩展以下属性
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
enable | boolean | true | 是否启用下拉菜单,为 false 则显示默认按钮 |
用于生成二维码的组件
<template>
+ <QrCode :value="qrCodeUrl" />
+</template>
+<script lang="ts">
+ import { defineComponent, ref, unref } from 'vue';
+ import { QrCode, QrCodeActionType } from '/@/components/Qrcode/index';
+ import LogoImg from '/@/assets/images/logo.png';
+ const qrCodeUrl = 'https://www.vvbin.cn';
+ export default defineComponent({
+ components: { QrCode },
+ setup() {
+ const qrRef = ref<Nullable<QrCodeActionType>>(null);
+ function download() {
+ const qrEl = unref(qrRef);
+ if (!qrEl) return;
+ qrEl.download('文件名');
+ }
+ return {
+ qrCodeUrl,
+ LogoImg,
+ download,
+ qrRef,
+ };
+ },
+ });
+</script>
+<style scoped>
+ .qrcode-demo-item {
+ width: 30%;
+ margin-right: 1%;
+ }
+</style>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string | - | - | 二维码地址 |
options | QRCodeRenderersOptions | - | - | 二维码配置 ,见 QRCodeRenderersOptions |
width | number | 2 | - | 宽度 |
logo | string|LogoType | - | - | 中间 logo 配置,见 LogoType |
tag | 渲染标签 | canvas | canvas | img | img 不支持内嵌 logo |
QRCodeRenderersOptions
/**
+ * 定义margin的宽度。.
+ * Default: 4
+ */
+margin?: number;
+/**
+ * 比例因子。值1表示每个模块1像素(黑点)。
+ * Default: 4
+ */
+scale?: number;
+/**
+ * 为输出图像强制指定宽度。
+ * 如果宽度太小而不能包含qr符号,则此选项将被忽略。
+ * 优先于规模。
+ */
+width?: number;
+color?: {
+ /**
+ * 暗模块的颜色。值必须为十六进制格式(RGBA).
+ * 注意:深色应始终比color.light暗。.
+ * Default: #000000ff
+ */
+ dark?: string;
+ /**
+ * 照明模块的颜色。值必须为十六进制格式(RGBA).
+ * Default: #ffffffff
+ */
+ light?: string;
+};
+
+
LogoType
{
+ // logo图片
+ src: string;
+ // logo大小
+ logoSize: number;
+ // 背景颜色
+ bgColor: string;
+ // logo圆角
+ logoRadius: number;
+}
+
名称 | 回调参数 | 说明 |
---|---|---|
download | Function(fileName:string) | 下载 |
名称 | 回调参数 | 说明 |
---|---|---|
done | (data: QrcodeDoneEventParams)=>void | 绘制完成 |
error | (error)=>void | 生成二维码时发生错误 |
QrcodeDoneEventParams
{
+ url: string; // 二维码DataURL数据
+ ctx?: CanvasRenderingContext2D; // 该对象为画布的2D渲染上下文,仅在tag为canvas时有效,可用于自定义绘制
+}
+
done
事件回调中可以对二维码进行自定义的绘制,示例代码如下:
<QrCode
+ :value="qrCodeUrl"
+ :width="200"
+ @done="onQrcodeDone"
+/>
+
function onQrcodeDone({ ctx }) {
+ if (ctx instanceof CanvasRenderingContext2D) {
+ // 额外绘制
+ ctx.fillStyle = 'black';
+ ctx.font = '16px "微软雅黑"';
+ ctx.textBaseline = 'bottom';
+ ctx.textAlign = 'center';
+ ctx.fillText('你帅你先扫', 100, 195, 200);
+ }
+}
+
有关 CanvasRenderingContext2D
的更多资料以及绘制方法,请参考MDN
参考 element-ui
的 el-scrollbar 组件实现
滚动容器组件
<template>
+ <div class="p-4">
+ <div class="my-4">
+ <a-button @click="scrollTo(100)">滚动到100px位置</a-button>
+ <a-button @click="scrollTo(800)">滚动到800px位置</a-button>
+ <a-button @click="scrollTo(0)">滚动到顶部</a-button>
+ <a-button @click="scrollBottom()">滚动到底部</a-button>
+ </div>
+ <div class="scroll-wrap">
+ <ScrollContainer ref="scrollRef">
+ <ul>
+ <template v-for="index in 100" :key="index">
+ <li>{{ index }}</li>
+ </template>
+ </ul>
+ </ScrollContainer>
+ </div>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, ref, unref } from 'vue';
+ import { CollapseContainer } from '/@/components/Container/index';
+ import { ScrollContainer, ScrollActionType } from '/@/components/Container/index';
+ export default defineComponent({
+ components: { CollapseContainer, ScrollContainer },
+ setup() {
+ const scrollRef = ref<Nullable<ScrollActionType>>(null);
+ const getScroll = () => {
+ const scroll = unref(scrollRef);
+ if (!scroll) {
+ throw new Error('scroll is Null');
+ }
+ return scroll;
+ };
+
+ function scrollTo(top: number) {
+ getScroll()?.scrollTo(top);
+ }
+
+ function scrollBottom() {
+ getScroll()?.scrollBottom();
+ }
+
+ return {
+ scrollTo,
+ scrollRef,
+ scrollBottom,
+ };
+ },
+ });
+</script>
+<style lang="less" scoped>
+ .scroll-wrap {
+ width: 50%;
+ height: 300px;
+ background: #fff;
+ }
+</style>
+
名称 | 回调参数 | 说明 |
---|---|---|
getScrollWrap | ()=>HtmlElement | 获取滚动容器 el |
scrollBottom | Function | 滚动到底部 |
scrollTo | Function(to:number,duration = 500) | 滚动到指定位置 |
名称 | 说明 |
---|---|
default | 默认区域 |
用于校验密码强度
<template>
+ <div class="p-4 flex justify-center">
+ <div class="demo-wrap p-10">
+ <StrengthMeter placeholder="默认" />
+ <StrengthMeter placeholder="禁用" disabled />
+ <br />
+ <StrengthMeter placeholder="隐藏input" :show-input="false" value="!@#qwe12345" />
+ </div>
+ </div>
+</template>
+
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import StrengthMeter from '/@/components/StrengthMeter/index';
+ export default defineComponent({
+ components: {
+ StrengthMeter,
+ },
+ });
+</script>
+<style lang="less" scoped>
+ .demo-wrap {
+ width: 50%;
+ background: #fff;
+ border-radius: 10px;
+ }
+</style>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string | - | - | 校验的值 |
showInput | boolean | true | - | 是否显示 input |
disabled | boolean | false | - | 是否禁用 |
事件 | 回调参数 | 说明 |
---|---|---|
score-change | number | 强度值改变触发 |
change | string | input 值改变触发 |
对 antv
的 table 组件进行封装
如果文档内没有,可以尝试在在线示例内寻找
<template>
+ <div class="p-4">
+ <BasicTable
+ title="基础示例"
+ titleHelpMessage="温馨提醒"
+ :columns="columns"
+ :dataSource="data"
+ :canResize="canResize"
+ :loading="loading"
+ :striped="striped"
+ :bordered="border"
+ :pagination="{ pageSize: 20 }"
+ >
+ <template #toolbar>
+ <a-button type="primary"> 操作按钮 </a-button>
+ </template>
+ </BasicTable>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { BasicTable } from '/@/components/Table';
+ import { getBasicColumns, getBasicData } from './tableData';
+
+ export default defineComponent({
+ components: { BasicTable },
+ setup() {
+ return {
+ columns: getBasicColumns(),
+ data: getBasicData(),
+ };
+ },
+ });
+</script>
+
所有可调用函数见下方 Methods
说明
<template>
+ <div class="p-4">
+ <BasicTable
+ :canResize="false"
+ title="RefTable示例"
+ titleHelpMessage="使用Ref调用表格内方法"
+ ref="tableRef"
+ :api="api"
+ :columns="columns"
+ rowKey="id"
+ :rowSelection="{ type: 'checkbox' }"
+ />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, ref, unref } from 'vue';
+ import { BasicTable, TableActionType } from '/@/components/Table';
+ import { getBasicColumns, getBasicShortColumns } from './tableData';
+ import { demoListApi } from '/@/api/demo/table';
+ export default defineComponent({
+ components: { BasicTable },
+ setup() {
+ const tableRef = ref<Nullable<TableActionType>>(null);
+
+ function getTableAction() {
+ const tableAction = unref(tableRef);
+ if (!tableAction) {
+ throw new Error('tableAction is null');
+ }
+ return tableAction;
+ }
+ function changeLoading() {
+ getTableAction().setLoading(true);
+ setTimeout(() => {
+ getTableAction().setLoading(false);
+ }, 1000);
+ }
+ return {
+ tableRef,
+ api: demoListApi,
+ columns: getBasicColumns(),
+ changeLoading,
+ };
+ },
+ });
+</script>
+
<template>
+ <div class="p-4">
+ <BasicTable @register="registerTable">
+ <template #action="{ record }">
+ <TableAction
+ :actions="[
+ {
+ label: '编辑',
+ onClick: handleEdit.bind(null, record),
+ auth: 'other', // 根据权限控制是否显示: 无权限,不显示
+ },
+ {
+ label: '删除',
+ icon: 'ic:outline-delete-outline',
+ onClick: handleDelete.bind(null, record),
+ auth: 'super', // 根据权限控制是否显示: 有权限,会显示
+ },
+ ]"
+ :dropDownActions="[
+ {
+ label: '启用',
+ popConfirm: {
+ title: '是否启用?',
+ confirm: handleOpen.bind(null, record),
+ },
+ ifShow: (_action) => {
+ return record.status !== 'enable'; // 根据业务控制是否显示: 非enable状态的不显示启用按钮
+ },
+ },
+ {
+ label: '禁用',
+ popConfirm: {
+ title: '是否禁用?',
+ confirm: handleOpen.bind(null, record),
+ },
+ ifShow: () => {
+ return record.status === 'enable'; // 根据业务控制是否显示: enable状态的显示禁用按钮
+ },
+ },
+ {
+ label: '同时控制',
+ popConfirm: {
+ title: '是否动态显示?',
+ confirm: handleOpen.bind(null, record),
+ },
+ auth: 'super', // 同时根据权限和业务控制是否显示
+ ifShow: () => {
+ return true; // 根据业务控制是否显示
+ },
+ },
+ ]"
+ />
+ </template>
+ </BasicTable>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicTable, useTable, BasicColumn, TableAction } from '/@/components/Table';
+
+ import { demoListApi } from '/@/api/demo/table';
+ const columns: BasicColumn[] = [
+ {
+ title: '姓名',
+ dataIndex: 'name',
+ auth: 'test', // 根据权限控制是否显示: 无权限,不显示
+ },
+ {
+ title: '地址',
+ dataIndex: 'address',
+ auth: 'super', // 同时根据权限控制是否显示
+ ifShow: (_column) => {
+ return true; // 根据业务控制是否显示
+ },
+ },
+ ];
+ export default defineComponent({
+ components: { BasicTable, TableAction },
+ setup() {
+ const [registerTable] = useTable({
+ title: 'TableAction组件及固定列示例',
+ api: demoListApi,
+ columns: columns,
+ bordered: true,
+ actionColumn: {
+ width: 250,
+ title: 'Action',
+ dataIndex: 'action',
+ slots: { customRender: 'action' },
+ },
+ });
+ function handleEdit(record: Recordable) {
+ console.log('点击了编辑', record);
+ }
+ function handleDelete(record: Recordable) {
+ console.log('点击了删除', record);
+ }
+ function handleOpen(record: Recordable) {
+ console.log('点击了启用', record);
+ }
+ return {
+ registerTable,
+ handleEdit,
+ handleDelete,
+ handleOpen,
+ };
+ },
+ });
+</script>
+
使用组件自带的 useTable 可以方便使用表单
下面是一个使用简单表格的示例,
<template>
+ <BasicTable @register="registerTable" />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicTable, useTable } from '/@/components/Table';
+ import { getBasicColumns, getBasicShortColumns } from './tableData';
+ import { demoListApi } from '/@/api/demo/table';
+ export default defineComponent({
+ components: { BasicTable },
+ setup() {
+ const [
+ registerTable,
+ {
+ setLoading,
+ },
+ ] = useTable({
+ api: demoListApi,
+ columns: getBasicColumns(),
+ });
+
+ function changeLoading() {
+ setLoading(true);
+ setTimeout(() => {
+ setLoading(false);
+ }, 1000);
+ }
+ }
+ return {
+ registerTable,
+ changeLoading,
+ };
+ },
+ });
+</script>
+
用于调用 Table 内部方法及 table 参数配置
// 表格的props也可以直接注册到useTable内部
+const [register, methods] = useTable(props);
+
register
register 用于注册 useTable,如果需要使用useTable
提供的 api,必须将 register 传入组件的 onRegister
<template>
+ <BasicTable @register="register" />
+</template>
+<script>
+ export default defineComponent({
+ components: { BasicForm },
+ setup() {
+ const [register] = useTable();
+ return { register };
+ },
+ });
+</script>
+
setProps
类型:(props: Partial<BasicTableProps>) => void
说明: 用于设置表格参数
reload
类型:(opt?: FetchParams) => Promise<void>
说明: 刷新表格
redoHeight
类型:() => void
说明: 重新计算表格高度
setLoading
类型:(loading: boolean) => void
说明: 设置表格 loading 状态
getDataSource
获取表格数据
类型:<T = Recordable>() => T[]
说明: 获取表格数据
getRawDataSource
获取后端接口原始数据
类型:<T = Recordable>() => T
说明: 获取后端接口原始数据
getColumns
类型:(opt?: GetColumnsParams) => BasicColumn[]
说明: 获取表格数据
setColumns
类型:(columns: BasicColumn[] | string[]) => void
说明: 设置表头数据
setTableData
类型:<T = Recordable>(values: T[]) => void
说明: 设置表格数据
setPagination
类型:(info: Partial<PaginationProps>) => void
说明: 设置分页信息
deleteSelectRowByKey
类型:(key: string) => void
说明: 根据 key 删除取消选中行
getSelectRowKeys
类型:() => string[]
说明: 获取选中行的 keys
getSelectRows
类型:<T = Recordable>() => T[]
说明: 获取选中行的 rows
clearSelectedRowKeys
类型:() => void
说明: 清空选中行
setSelectedRowKeys
类型:(rowKeys: string[] | number[]) => void
说明: 设置选中行
getPaginationRef
类型:() => PaginationProps | boolean
说明: 获取当前分页信息
getShowPagination
类型:() => boolean
说明: 获取当前是否显示分页
setShowPagination
类型:(show: boolean) => Promise<void>
说明: 设置当前是否显示分页
getRowSelection
类型:() => TableRowSelection<Recordable>
说明: 获取勾选框信息
updateTableData
类型:(index: number, key: string, value: any)=>void
说明: 更新表格数据
updateTableDataRecord
类型: (rowKey: string | number, record: Recordable) => Recordable | void
说明: 根据唯一的 rowKey
更新指定行的数据.可用于不刷新整个表格而局部更新数据
deleteTableDataRecord
类型: (rowKey: string | number | string[] | number[]) => void
说明: 根据唯一的rowKey
动态删除指定行的数据.可用于不刷新整个表格而局部更新数据
insertTableDataRecord
类型: (record: Recordable, index?: number) => Recordable | void
说明: 可根据传入的 index
值决定插入数据行的位置,不传则是顺序插入,可用于不刷新整个表格而局部更新数据
getForm
类型:() => FormActionType
说明: 如果开启了搜索区域。可以通过该函数获取表单对象函数进行操作
expandAll
类型:() => void
说明: 展开树形表格
collapseAll
类型:() => void
说明: 折叠树形表格
温馨提醒
defaultExpandAllRows
、defaultExpandedRowKeys
属性在basicTable中不受支持,并且在antv table
v2.2.0之后也被移除。属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
clickToRowSelect | boolean | true | - | 点击行是否选中 checkbox 或者 radio。需要开启 | |
sortFn | (sortInfo: SorterResult<any>) => any | - | - | 自定义排序方法。见下方全局配置说明 | |
filterFn | (sortInfo: Partial<Recordable<string[]>>) => any | - | - | 自定义过滤方法。见下方全局配置说明 | |
showTableSetting | boolean | false | - | 显示表格设置工具 | |
tableSetting | TableSetting | - | - | 表格设置工具配置,见下方 TableSetting | |
striped | boolean | true | - | 斑马纹 | |
inset | boolean | false | - | 取消表格的默认 padding | |
autoCreateKey | boolean | true | - | 是否自动生成 key | |
showSummary | boolean | false | - | 是否显示合计行 | |
summaryData | any[] | - | - | 自定义合计数据。如果有则显示该数据 | |
emptyDataIsShowTable | boolean | true | - | 在启用搜索表单的前提下,是否在表格没有数据的时候显示表格 | |
summaryFunc | (...arg) => any[] | - | - | 计算合计行的方法 | |
boolean | false | - | |||
boolean | false | - | |||
isTreeTable | boolean | false | - | 是否树表 | |
api | (...arg: any) => Promise<any> | - | - | 请求接口,可以直接将src/api内的函数直接传入 | |
beforeFetch | (T)=>T | - | - | 请求之前对参数进行处理 | |
afterFetch | (T)=>T | - | - | 请求之后对返回值进行处理 | |
handleSearchInfoFn | (T)=>T | - | - | 开启表单后,在请求之前处理搜索条件参数 | |
fetchSetting | FetchSetting | - | - | 接口请求配置,可以配置请求的字段和响应的字段名,见下方全局配置说明 | |
immediate | boolean | true | - | 组件加载后是否立即请求接口,在 api 有传的情况下,如果为 false,需要自行使用 reload 加载表格数据 | |
searchInfo | any | - | - | 额外的请求参数 | |
useSearchForm | boolean | false | - | 使用搜索表单 | |
formConfig | any | - | - | 表单配置,参考表单组件的 Props | |
columns | any | - | - | 表单列信息 BasicColumn[] | |
showIndexColumn | boolean | ture | - | 是否显示序号列 | |
indexColumnProps | any | - | - | 序号列配置 BasicColumn | |
actionColumn | any | - | - | 表格右侧操作列配置 BasicColumn | |
ellipsis | boolean | true | - | 文本超过宽度是否显示... | |
canResize | boolean | true | - | 是否可以自适应高度(如果置于PageWrapper组件内,请勿启用PageWrapper的fixedHeight属性,二者不可同时使用) | |
clearSelectOnPageChange | boolean | false | - | 切换页码是否重置勾选状态 | |
resizeHeightOffset | number | 0 | - | 表格自适应高度计算结果会减去这个值 | |
rowSelection | any | - | - | 选择列配置 | |
title | string | - | - | 表格标题 | |
titleHelpMessage | string | string[] | - | - | 表格标题右侧温馨提醒 | |
maxHeight | number | - | - | 表格最大高度,超出会显示滚动条 | |
dataSource | any[] | - | - | 表格数据,非 api 加载情况 | |
bordered | boolean | false | - | 是否显示表格边框 | |
pagination | any | - | - | 分页信息配置,为 false 不显示分页 | |
loading | boolean | false | - | 表格 loading 状态 | |
scroll | any | - | - | 参考官方文档 scroll | |
beforeEditSubmit | ({record: Recordable,index: number,key: string | number,value: any}) => Promise<any> | - | - | 单元格编辑状态提交回调,返回false将阻止单元格提交数据到table。该回调在行编辑模式下无效。 | 2.7.2 |
{
+ // 是否显示刷新按钮
+ redo?: boolean;
+ // 是否显示尺寸调整按钮
+ size?: boolean;
+ // 是否显示字段调整按钮
+ setting?: boolean;
+ // 是否显示全屏按钮
+ fullScreen?: boolean;
+}
+
除 参考官方 Column 配置外,扩展以下参数
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
defaultHidden | boolean | false | - | 默认隐藏,可在列配置显示 |
helpMessage | string|string[] | - | - | 列头右侧帮助文本 |
edit | boolean | - | - | 是否开启单元格编辑 |
editRow | boolean | - | - | 是否开启行编辑 |
editable | boolean | false | - | 是否处于编辑状态 |
editComponent | ComponentType | Input | - | 编辑组件 |
editComponentProps | any | - | - | 对应编辑组件的 props |
editRule | ((text: string, record: Recordable) => Promise<string>) | - | - | 对应编辑组件的表单校验 |
editValueMap | (value: any) => string | - | - | 对应单元格值枚举 |
onEditRow | ()=>void | - | - | 触发行编辑 |
format | CellFormat | - | - | 单元格格式化 |
auth | RoleEnum | RoleEnum[] | string | string[] | - | - | 根据权限编码来控制当前列是否显示 |
ifShow | boolean | ((action: ActionItem) => boolean) | - | - | 根据业务状态来控制当前列是否显示 |
export type ComponentType =
+ | 'Input'
+ | 'InputNumber'
+ | 'Select'
+ | 'ApiSelect'
+ | 'Checkbox'
+ | 'Switch'
+ | 'DatePicker' // v2.5.0 以上
+ | 'TimePicker'; // v2.5.0 以上
+
export type CellFormat =
+ | string
+ | ((text: string, record: Recordable, index: number) => string | number)
+ | Map<string | number, any>;
+
温馨提醒
除以下事件外,官方文档内的 event 也都支持,具体可以参考 antv table
事件 | 回调参数 | 说明 |
---|---|---|
fetch-success | Function({items,total}) | 接口请求成功后触发 |
fetch-error | Function(error) | 错误信息 |
selection-change | Function({keys,rows}) | 勾选事件触发 |
row-click | Function(record, index, event) | 行点击触发 |
row-dbClick | Function(record, index, event) | 行双击触发 |
row-contextmenu | Function(record, index, event) | 行右键触发 |
row-mouseenter | Function(record, index, event) | 行移入触发 |
row-mouseleave | Function(record, index, event) | 行移出触发 |
edit-end | Function({record, index, key, value}) | 单元格编辑完成触发 |
edit-cancel | Function({record, index, key, value}) | 单元格取消编辑触发 |
edit-row-end | Function() | 行编辑结束触发 |
edit-change | Function({column,value,record}) | 单元格编辑组件的 value 发生变化时触发 |
edit-change 说明
从版本 2.4.2
起,对于 edit-change
事件,record
中的 editValueRefs
装载了当前行的所有编辑组件(如果有的话)的值的 ref
对象,可用于处理同一行中的编辑组件的联动。请看下面的例子
function onEditChange({ column, record }) {
+ // 当同一行的单价或者数量发生变化时,更新合计金额(三个数据均为当前行编辑组件的值)
+ if (column.dataIndex === 'qty' || column.dataIndex === 'price') {
+ const { editValueRefs: { total, qty, price } } = record;
+ total.value = unref(qty) * unref(price);
+ }
+ }
+
温馨提醒
除以下参数外,官方文档内的 slot 也都支持,具体可以参考 antv table
名称 | 说明 | 版本 |
---|---|---|
tableTitle | 表格顶部左侧区域 | |
toolbar | 表格顶部右侧区域 | |
expandedRowRender | 展开行区域 | |
headerTop | 表格顶部区域(标题上方) | 2.6.1 |
当开启 form 表单后。以form-xxxx
为前缀的 slot 会被视为 form 的 slot
xxxx 为 form 组件的 slot。具体参考form 组件文档
e.g
form-submitBefore
+
字段调整组件
提供了可视化操作表格每一列的是否展示、位置、固定;包括序号列、勾选列。会响应tableMethods
中setColumns
和setProps
方法的更改内容。
值得注意的是
序号列
和勾选列
是在table的props中定义的,对应的字段分别是showIndexColumn
、rowSelection
。因此在动态改变表格列配置的时候,建议使用setProps方法,并显式地设置这两个字段的值来保证达到预期效果
// ...
+const [registerTable, { setProps }] = useTable({...})
+
+setProps({
+ columns: [], // 表格的列配置 BasicColumn[]
+ showIndexColumn: false, // 是否展示序号列
+ rowSelection: false // 勾选列配置
+})
+
用于表格右侧操作列渲染
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
actions | ActionItem[] | - | - | 右侧操作列按钮列表 | |
dropDownActions | ActionItem[] | - | - | 右侧操作列更多下拉按钮列表 | |
stopButtonPropagation | boolean | false | true/false | 是否阻止操作按钮的click事件冒泡 | 2.5.0 |
ActionItem
export interface ActionItem {
+ // 按钮文本
+ label: string;
+ // 是否禁用
+ disabled?: boolean;
+ // 按钮颜色
+ color?: 'success' | 'error' | 'warning';
+ // 按钮类型
+ type?: string;
+ // button组件props
+ props?: any;
+ // 按钮图标
+ icon?: string;
+ // 气泡确认框
+ popConfirm?: PopConfirm;
+ // 是否显示分隔线,v2.0.0+
+ divider?: boolean;
+ // 根据权限编码来控制当前列是否显示,v2.4.0+
+ auth?: RoleEnum | RoleEnum[] | string | string[];
+ // 根据业务状态来控制当前列是否显示,v2.4.0+
+ ifShow?: boolean | ((action: ActionItem) => boolean);
+ // 点击回调
+ onClick?: Fn;
+ // Tooltip配置,2.5.3以上版本支持,可以配置为string,或者完整的tooltip属性
+ tooltip?: string | TooltipProps
+}
+
有关TooltipProps的说明,请参考tooltip
PopConfirm
export interface PopConfirm {
+ title: string;
+ okText?: string;
+ cancelText?: string;
+ confirm: Fn;
+ cancel?: Fn;
+ icon?: string;
+}
+
用于渲染单元格图片,支持图片预览
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
imgList | string[] | - | - | 图片地址列表 | |
size | number | - | - | 图片大小 | |
simpleShow | boolean | false | true/false | 简单显示模式(只显示第一张图片) | 2.5.0 |
showBadge | boolean | true | true/false | 简单模式下是否显示计数Badge | 2.5.0 |
margin | number | 4 | - | 常规模式下的图片间距 | 2.5.0 |
srcPrefix | string | - | - | 在每一个图片src前插入的内容 | 2.5.0 |
在componentsSettings 可以配置全局参数。用于统一整个项目的风格。可以通过 props 传值覆盖
相对时间组件
<template>
+ <Time :value="time" />
+</template>
+<script lang="ts">
+ import { defineComponent, reactive, toRefs } from 'vue';
+ import { Time } from '/@/components/Time';
+
+ export default defineComponent({
+ components: { Time },
+ setup() {
+ const now = new Date().getTime();
+ const state = reactive({
+ time: now - 60 * 3 * 1000,
+ });
+ return {
+ ...toRefs(state),
+ now,
+ };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string,Date,number | - | - | 时间值 |
step | number | 60 | - | 刷新时间 |
mode | string | relative | - | 模式,date:日期,datetime:时间戳,relative:相对时间 |
富文本组件位于 src/components/TinyMce
富文本组件使用的是 CDN 方式引入
可在 /@/components/TinyMce/src/Editor.vue 更改下面 CDN 地址
const CDN_URL = 'https://cdn.bootcdn.net/ajax/libs/tinymce/5.5.1';
+
<template>
+ <Tinymce v-model="value" @change="handleChange" width="100%" />
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { Tinymce } from '/@/components/Tinymce/index';
+
+ export default defineComponent({
+ components: { Tinymce },
+ setup() {
+ const value = ref('hello world!');
+ function handleChange(value: string) {
+ console.log(value);
+ }
+ return { handleChange, value };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
options | any | {} | tinymce 的配置项 |
value(v-model) | string | - | 双向绑定值 |
height | number , string | 400 | 高度 |
width | number , string | auto | 宽度 |
toolbar | string[] | - | 工具栏 |
plugins | string[] | - | 插件 |
showImageUpload | boolean | true | 是否显示上传按钮 |
事件 | 回调参数 | 返回值 | 说明 |
---|---|---|---|
change | (str:string)=>{} | 富文本内容改变触发事件 |
用于页面/组件切换动画
<template>
+ <div class="p-4">
+ <div class="flex">
+ <Select
+ :options="options"
+ v-model:value="value"
+ placeholder="选择动画"
+ :style="{ width: '150px' }"
+ />
+ <a-button type="primary" class="ml-4" @click="start"> start </a-button>
+ </div>
+ <component :is="`${value}Transition`">
+ <div class="box" v-show="show"></div>
+ </component>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { Select } from 'ant-design-vue';
+ import {
+ FadeTransition,
+ ScaleTransition,
+ SlideYTransition,
+ ScrollYTransition,
+ SlideYReverseTransition,
+ ScrollYReverseTransition,
+ SlideXTransition,
+ ScrollXTransition,
+ SlideXReverseTransition,
+ ScrollXReverseTransition,
+ ScaleRotateTransition,
+ ExpandXTransition,
+ ExpandTransition,
+ } from '/@/components/Transition/index';
+
+ const transitionList = [
+ 'Fade',
+ 'Scale',
+ 'SlideY',
+ 'ScrollY',
+ 'SlideYReverse',
+ 'ScrollYReverse',
+ 'SlideX',
+ 'ScrollX',
+ 'SlideXReverse',
+ 'ScrollXReverse',
+ 'ScaleRotate',
+ 'ExpandX',
+ 'Expand',
+ ];
+ const options = transitionList.map((item) => ({
+ label: item,
+ value: item,
+ key: item,
+ }));
+
+ export default defineComponent({
+ components: {
+ Select,
+ FadeTransition,
+ ScaleTransition,
+ SlideYTransition,
+ ScrollYTransition,
+ SlideYReverseTransition,
+ ScrollYReverseTransition,
+ SlideXTransition,
+ ScrollXTransition,
+ SlideXReverseTransition,
+ ScrollXReverseTransition,
+ ScaleRotateTransition,
+ ExpandXTransition,
+ ExpandTransition,
+ },
+ setup() {
+ const value = ref('Fade');
+ const show = ref(true);
+ function start() {
+ show.value = false;
+ setTimeout(() => {
+ show.value = true;
+ }, 300);
+ }
+ return { options, value, start, show };
+ },
+ });
+</script>
+<style lang="less" scoped>
+ .box {
+ width: 150px;
+ height: 150px;
+ margin-top: 20px;
+ background: pink;
+ }
+</style>
+
对 antv
的 tree 组件进行封装
<template>
+ <BasicTree :treeData="treeData" />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicTree } from '/@/components/Tree/index';
+ import { treeData } from './data';
+ import { CollapseContainer } from '/@/components/Container/index';
+ import { TreeItem } from '/@/components/Tree/index';
+
+ export const treeData: TreeItem[] = [
+ {
+ title: 'parent 1',
+ key: '0-0',
+ icon: 'home|svg',
+ children: [
+ { title: 'leaf', key: '0-0-0' },
+ {
+ title: 'leaf',
+ key: '0-0-1',
+ children: [
+ { title: 'leaf', key: '0-0-0-0' },
+ { title: 'leaf', key: '0-0-0-1' },
+ ],
+ },
+ ],
+ },
+ {
+ title: 'parent 2',
+ key: '1-1',
+ icon: 'home|svg',
+ children: [
+ { title: 'leaf', key: '1-1-0' },
+ { title: 'leaf', key: '1-1-1' },
+ ],
+ },
+ {
+ title: 'parent 3',
+ key: '2-2',
+ icon: 'home|svg',
+ children: [
+ { title: 'leaf', key: '2-2-0' },
+ { title: 'leaf', key: '2-2-1' },
+ ],
+ },
+ ];
+ export default defineComponent({
+ components: { BasicTree, CollapseContainer },
+ setup() {
+ return { treeData };
+ },
+ });
+</script>
+
温馨提醒
除以下参数外,官方文档内的 props 也都支持,具体可以参考 antv tree
属性 | 类型 | 默认值 | 可选值 | 说明 | 版本 |
---|---|---|---|---|---|
treeData | TreeItem[] | - | - | 树组件数据 | |
rightMenuList | ContextMenuItem[] | - | - | 右键菜单列表 | |
checkedKeys | string[] | - | - | 勾选的节点 | |
selectedKeys | string[] | - | - | 选中的节点 | |
expandedKeys | string[] | - | - | 展开的节点 | |
actionList | ActionItem[] | - | - | 鼠标移动上去右边操作按钮列表 | |
title | string | - | - | 定制标题字符串 | |
toolbar | boolean | - | - | 是否显示工具栏 | |
search | boolean | - | - | 显示搜索框 | |
clickRowToExpand | boolean | - | - | 是否在点击行时自动展开 | |
beforeRightClick | (node, event)=>ContextMenuItem[] | - | - | 右键点击回调,可返回右键菜单列表数据来生成右键菜单 | |
rightMenuList | ContextMenuItem[] | - | - | 右键菜单列表数据 | |
defaultExpandLevel | string | number | - | - | 初次渲染后默认展开的层级 | 2.4.1 |
defaultExpandAll | boolean | false | true/false | 初次渲染后默认全部 | 2.4.1 |
searchValue(v-model) | string | - | - | 当前搜索词 | 2.7.1 |
注意
defaultExpandLevel
、defaultExpandAll
仅在初次渲染时生效。如果basicTree
是在创建完毕之后才设置的treeData
(如异步数据),需要在更新后自己调用basicTree
提供的expandAll
、filterByLevel
来执行展开
ActionItem
{
+ // 渲染的图标
+ render: (record: any) => any;
+ // 是否显示
+ show?: boolean | ((record: Recordable) => boolean);
+}
+
ContextMenuItem
{
+ // 文本
+ label: string;
+ // 图标
+ icon?: string;
+ // 是否禁用
+ disabled?: boolean;
+ // 事件
+ handler?: (...arg) => any;
+ // 是否显示分隔线
+ divider?: boolean;
+ // 子级菜单数据
+ children?: ContextMenuItem[];
+}
+
温馨提醒
官方文档内的 slot 都支持,具体可以参考 antv tree
名称 | 回调参数 | 说明 |
---|---|---|
checkAll | (checkAll: boolean) => void | 选择所有 |
expandAll | (expandAll: boolean) => void | 展开所有 |
setExpandedKeys | (keys: Keys) => void | 设置展开节点 |
getExpandedKeys | () => Keys | 获取展开节点 |
setSelectedKeys | (keys: Keys) => void | 设置选中节点 |
getSelectedKeys | () => Keys | 获取选中节点 |
setCheckedKeys | (keys: CheckKeys) => void | 设置勾选节点 |
getCheckedKeys | () => CheckKeys | 获取勾选节点 |
filterByLevel | (level: number) => void | 显示指定等级 |
insertNodeByKey | (opt: InsertNodeParams) => void | 插入子节点到指定节点内 |
deleteNodeByKey | (key: string) => void | 根据 key 删除节点 |
updateNodeByKey | (key: string, node: Omit<TreeItem, 'key'>) => void | 根据 key 更新节点 |
setSearchValue | (value: string) => void | 设置当前搜索词(v2.7.1) |
getSearchValue | () => string | 获取当前搜索词(v2.7.1) |
文件上传组件
<template>
+ <BasicUpload :maxSize="20" :maxNumber="10" @change="handleChange" :api="uploadApi" />
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { BasicUpload } from '/@/components/Upload';
+ import { uploadApi } from '/@/api/sys/upload';
+
+ export default defineComponent({
+ components: { BasicUpload },
+ setup() {
+ return {
+ uploadApi,
+ handleChange: (list: string[]) => {
+ createMessage.info(`已上传文件${JSON.stringify(list)}`);
+ },
+ };
+ },
+ });
+</script>
+
.env.development
和 .env.production
配置开发和生产的文件上传地址
# .env.development
+
+VITE_PROXY=[["/upload","http://localhost:3001/upload"]]
+
+::: tip
+v3.0.0开始,作者重构了vite.config.ts,新版本不再支持VITE_PROXY环境变量。
+:::
+
+# 如果没有跨域问题,则直接使用真实上传地址
+VITE_GLOB_UPLOAD_URL=/upload
+
+# .env.production
+VITE_GLOB_UPLOAD_URL=/upload
+
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
value | string[] | - | - | 已上传的文件列表,支持v-model |
showPreviewNumber | boolean | true | - | 是否显示预览数量 |
emptyHidePreview | boolean | false | - | 没有上传文件时是否隐藏预览 |
helpText | string | - | - | 帮助文本 |
maxSize | number | 2 | - | 单个文件最大体积,单位 M |
maxNumber | number | Infinity | - | 最大上传数量,Infinity 则不限制 |
accept | string[] | - | - | 限制上传格式,可使用文件后缀名(点号可选)或MIME字符串。例如 ['.doc,','docx','application/msword','image/*'] |
multiple | boolean | - | - | 开启多文件上传 |
uploadParams | any | - | - | 上传携带的参数 |
api | Fn | - | - | 上传接口,为上面配置的接口 |
事件 | 回调参数 | 返回值 | 说明 | 版本 |
---|---|---|---|---|
change | (fileList)=>void | 文件列表内容改变触发事件 | ||
delete | (record)=>void | 在上传列表中删除文件的事件 | ||
preview-delete | (url:string)=>void | 在预览列表中删除文件的事件 | 2.5.3 |
拖动校验组件
<template>
+ <div class="p-10">
+ <BasicDragVerify @success="handleSuccess" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent, ref } from 'vue';
+ import { BasicDragVerify, DragVerifyActionType, PassingData } from '/@/components/Verify/index';
+ export default defineComponent({
+ components: { BasicDragVerify },
+ setup() {
+ function handleSuccess(data: PassingData) {
+ const { time } = data;
+ createMessage.success(`校验成功,耗时${time}秒`);
+ }
+ return {
+ handleSuccess,
+ handleBtnClick,
+ };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
value | boolean | - | 是否通过 |
text | string | 请按住滑块拖动 | 未拖动时候显示文字 |
successText | string | 验证通过 | 验证成功后显示文本 |
height | string|string | 40 | 高度 |
width | string|string | 260 | 宽度 |
circle | boolean | false | 是否圆角 |
wrapStyle | any | - | 外层容器样式 |
contentStyle | any | - | 主体内容样式 |
barStyle | any | - | bar 样式 |
actionStyle | any | - | 拖拽按钮样式 |
名称 | 回调参数 | 说明 |
---|---|---|
resume | ()=>{} | 还原初始值 |
图片还原正方向校验组件
<template>
+ <div class="p-10">
+ <RotateDragVerify :src="img" ref="el" @success="handleSuccess" />
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { RotateDragVerify } from '/@/components/Verify/index';
+
+ import img from '/@/assets/images/header.jpg';
+ export default defineComponent({
+ components: { RotateDragVerify },
+ setup() {
+ const handleSuccess = () => {
+ console.log('success!');
+ };
+ return {
+ handleSuccess,
+ img,
+ };
+ },
+ });
+</script>
+
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
src | string | - | 图片地址 |
imgWidth | number | - | 图片宽度 |
imgWrapStyle | any | - | 图片外层容器样式 |
minDegree | number | - | 最小旋转角度 |
maxDegree | number | - | 最大旋转角度 |
diffDegree | number | - | 误差角度 |
value | boolean | - | 是否通过 |
text | string | 请按住滑块拖动 | 未拖动时候显示文字 |
successText | string | 验证通过 | 验证成功后显示文本 |
height | string|string | 40 | 高度 |
width | string|string | 260 | 宽度 |
circle | boolean | false | 是否圆角 |
wrapStyle | any | - | 外层容器样式 |
contentStyle | any | - | 主体内容样式 |
barStyle | any | - | bar 样式 |
actionStyle | any | - | 拖拽按钮样式 |
名称 | 回调参数 | 说明 |
---|---|---|
resume | Function | 还原初始值 |
虚拟滚动组件(用于大量数据纯展示时使用)
<template>
+ <div class="p-4 virtual-scroll-demo">
+ <Divider>基础滚动示例</Divider>
+ <div class="virtual-scroll-demo-wrap">
+ <VirtualScroll :itemHeight="41" :items="data" :height="300" :width="300">
+ <template v-slot="{ item }">
+ <div class="virtual-scroll-demo__item">{{ item.title }}</div>
+ </template>
+ </VirtualScroll>
+ </div>
+
+ <Divider>即使不可见,也预先加载50条数据,防止空白</Divider>
+ <div class="virtual-scroll-demo-wrap">
+ <VirtualScroll :itemHeight="41" :items="data" :height="300" :width="300" :bench="50">
+ <template v-slot="{ item }">
+ <div class="virtual-scroll-demo__item">{{ item.title }}</div>
+ </template>
+ </VirtualScroll>
+ </div>
+ </div>
+</template>
+<script lang="ts">
+ import { defineComponent } from 'vue';
+ import { VirtualScroll } from '/@/components/VirtualScroll/index';
+
+ import { Divider } from 'ant-design-vue';
+ const data: any[] = (() => {
+ const arr: any[] = [];
+ for (let index = 1; index < 20000; index++) {
+ arr.push({
+ title: '列表项' + index,
+ });
+ }
+ return arr;
+ })();
+ export default defineComponent({
+ components: { VirtualScroll, Divider },
+ setup() {
+ return { data: data };
+ },
+ });
+</script>
+<style lang="less" scoped>
+ .virtual-scroll-demo {
+ &-wrap {
+ display: flex;
+ margin: 0 30%;
+ background: #fff;
+ justify-content: center;
+ }
+
+ /deep/ &__item {
+ height: 40px;
+ padding: 0 20px;
+ line-height: 40px;
+ border-bottom: 1px solid #ddd;
+ }
+ }
+</style>
+
属性 | 类型 | 默认值 | 可选值 | 说明 |
---|---|---|---|---|
height | string|number | - | - | 高度 |
width | string|number | - | - | 宽度 |
maxHeight | string|number | - | - | 最大高度 |
maxWidth | string|number | - | - | 最大宽度 |
minHeight | string|number | - | - | 最小高度 |
minWidth | string|number | - | - | 最小宽度 |
itemHeight | string|number | - | - | 每个选项高度,必传 |
items | any[] | - | - | 选项列表 |
名称 | 说明 |
---|---|
default | 默认 |
跨域产生的原因是由于前端地址与后台接口不是同源,从而导致 ajax 不能发送
非同源产生的问题
同源条件
协议,端口,主机 三者相同即为同源
反之,其中只要 某一个 不一样则为不同源
本地开发跨域
本地开发一般使用下面 3 种方式进行处理
项目内部自带第一种方式,具体可以参考服务端交互-本地开发环境接口地址修改
生产环境跨域
生产环境一般使用下面 2 种方式进行处理
后台开启 cors 不需要前端做任何改动
nginx 配置文件可以查看nginx 配置
项目已经内置了黑暗主题切换,只需配置自己需要的颜色变量,即可在项目中使用
通过 vite-plugin-theme 插件,将所有的颜色变量抽取到独立的 css 文件,并且全部在 html 上面加上 css 选择器。通过改变 html 标签的 data-theme
属性来进行黑暗主题切换
黑暗主题颜色配置通过 vite-plugin-theme 实现,具体代码在 build/vite/plugin/theme
antdDarkThemePlugin({
+ darkModifyVars: {
+ ...generateModifyVars(true),
+ 'text-color': '#c9d1d9',
+ 'text-color-base': '#c9d1d9',
+ 'component-background': '#151515',
+ 'text-color-secondary': '#8b949e',
+ 'border-color-base': '#303030',
+ 'item-active-bg': '#111b26',
+ 'app-content-background': 'rgb(255 255 255 / 4%)',
+ },
+});
+
只需要使用 vite-plugin-theme 提供的函数来进行切换即可
import { darkCssIsReady, loadDarkThemeCss } from 'vite-plugin-theme/es/client';
+
+export async function updateDarkTheme(mode: string | null = 'light') {
+ const htmlRoot = document.getElementById('htmlRoot');
+ if (mode === 'dark') {
+ if (import.meta.env.PROD && !darkCssIsReady) {
+ await loadDarkThemeCss();
+ }
+ htmlRoot?.setAttribute('data-theme', 'dark');
+ } else {
+ htmlRoot?.setAttribute('data-theme', 'light');
+ }
+}
+
如果你使用的 vscode 开发工具,则推荐安装 I18n-ally 这个插件
安装了该插件后,你的代码内可以实时看到对应的语言内容
在 src/settings/localeSetting.ts 内可以配置默认语言
export const LOCALE: { [key: string]: LocaleType } = {
+ ZH_CN: 'zh_CN',
+ EN_US: 'en',
+};
+
+export const localeSetting: LocaleSetting = {
+ // 是否显示语言选择器
+ showPicker: true,
+ // 当前语言
+ locale: LOCALE.ZH_CN,
+ // 默认语言
+ fallback: LOCALE.ZH_CN,
+ // 允许的语言
+ availableLocales: [LOCALE.ZH_CN, LOCALE.EN_US],
+};
+
+// 配置语言列表
+export const localeList: DropMenu[] = [
+ {
+ text: '简体中文',
+ event: 'zh_CN',
+ },
+ {
+ text: 'English',
+ event: 'en',
+ },
+];
+
在 src/locales/setupI18n.ts 内引入的 i18n 这个无需修改
在 src/locales/lang/ 可以配置具体的语言
# locales/lang/
+
+# 中文语言
+zh_CN:
+ component: 组件相关
+ layout: 布局相关
+ routes: 路由菜单相关
+ sys: 系统页面相关
+
+en: 同上
+
+
在 src/locales/setupI18n 内的根语言文件可以看到
const defaultLocal = await import(`./lang/${locale}.ts`);
+
这会导入 src/locales/lang/{lang}.ts
文件语言包,此文件会导入对应语言下的所有文件。
import { genMessage } from '../helper';
+import antdLocale from 'ant-design-vue/es/locale/zh_CN';
+import momentLocale from 'moment/dist/locale/zh-cn';
+
+const modules = import.meta.globEager('./zh_CN/**/*.ts');
+export default {
+ message: {
+ ...genMessage(modules, 'zh_CN'),
+ antdLocale,
+ },
+ momentLocale,
+ momentLocaleName: 'zh-cn',
+};
+
并将其按相应的目录结构转化为多层级的
例:
lang/zh_CN/components/modal.ts
的文件内容为
{
+ title: '标题';
+}
+
则在使用的使用直接使用 t('components.modal.title')
进行获取。
这样做的好处在于更容易管理大型项目的多语言。如果不需要分模块划分,可以直接自己手动导入即可。
引入项目自带的 useI18n
注意不要引入 vue-i18n 的 useI18n
import { useI18n } from '/@/hooks/web/useI18n';
+
+const { t } = useI18n();
+
+const title = t('components.modal.title');
+
切换语言需要使用 src/locales/useLocale.ts
import { useLocale } from '/@/locales/useLocale';
+
+const { changeLocale } = useLocale();
+
+changeLocale('en');
+
在 src/locales/lang/ 增加对应语言的文件即可
目前项目自带的语言只有 zh_CN
和 en
两种
如果需要新增,按以下操作即可
目前项目会在 src/main.ts
内等待 setupI18n
这个函数执行完之后才会渲染界面,所以只需在 setupI18n 内发送 ajax 请求,将对应的数据设置到 i18n 实例上即可
// src/main.ts
+await setupI18n(app);
+
+app.mount('#app', true);
+
如下所示,这里会先设置一个默认语言,默认语言可以设置在本地,也可以在这里等待接口返回默认语言
// setup i18n instance with glob
+export async function setupI18n(app: App) {
+ const options = await createI18nOptions();
+ i18n = createI18n(options) as I18n;
+ app.use(i18n);
+}
+
+async function createI18nOptions(): Promise<I18nOptions> {
+ const locale = localeStore.getLocale;
+
+ // 这里改成接口获取
+ const defaultLocal = await import(`./lang/${locale}.ts`);
+ const message = defaultLocal.default?.message ?? {};
+
+ return {
+ legacy: false,
+ locale,
+ fallbackLocale: fallback,
+ messages: {
+ [locale]: message,
+ },
+ availableLocales: availableLocales,
+ sync: true,
+ silentTranslationWarn: true,
+ missingWarn: false,
+ silentFallbackWarn: true,
+ };
+}
+
当手动切换语言的时候会触发 useLocale
函数,useLocale 也是异步函数,只需等待接口返回响应的数据后,再进行设置即可
async function changeLocale(locale: LocaleType) {
+ const globalI18n = i18n.global;
+ const currentLocale = unref(globalI18n.locale);
+ if (currentLocale === locale) return locale;
+
+ if (loadLocalePool.includes(locale)) {
+ setI18nLanguage(locale);
+ return locale;
+ }
+ // 这里改成接口获取
+ const langModule = ((await import(`./lang/${locale}.ts`)) as any).default as LangModule;
+ if (!langModule) return;
+
+ const { message, momentLocale, momentLocaleName } = langModule;
+
+ globalI18n.setLocaleMessage(locale, message);
+ moment.updateLocale(momentLocaleName, momentLocale);
+ loadLocalePool.push(locale);
+
+ setI18nLanguage(locale);
+ return locale;
+}
+
项目中有以下多种图标使用方式。
使用 ant-design-vue
提供的图标
<template>
+ <StarOutlined />
+ <StarFilled />
+ <StarTwoTone twoToneColor="#eb2f96" />
+</template>
+
+<script>
+ import { defineComponent } from 'vue';
+ import { StarOutlined, StarFilled, StarTwoTone } from '@ant-design/icons-vue';
+ export default defineComponent({
+ components: { StarOutlined, StarFilled, StarTwoTone },
+ });
+</script>
+
将需要的 svg 图标放到src/assets/icons
内
例: test.svg
SvgIcon
组件进行展示<template>
+ <SvgIcon name="test" />
+</template>
+
+<script>
+ import { defineComponent } from 'vue';
+ import { SvgIcon } from '/@/components/Icon';
+ export default defineComponent({
+ components: { SvgIcon },
+ });
+</script>
+
Icon
组件进行展示以 |svg
结尾会自动使用SvgIcon
组件
<template>
+ <Icon name="test|svg" />
+</template>
+
+<script>
+ import { defineComponent } from 'vue';
+ import { Icon } from '/@/components/Icon';
+ export default defineComponent({
+ components: { Icon },
+ });
+</script>
+
使用方式请参考 Icon 组件
项目中使用到的是 vite-plugin-purge-icons 这个插件来进行图标实现。
+yarn add @iconify/iconify
+
+yarn add @iconify/json @purge-icons/generated -D
+
+
vite.config.ts
内引入插件import PurgeIcons from 'vite-plugin-purge-icons';
+
+export default {
+ plugins: [PurgeIcons()],
+};
+
完整代码 src/components/Icon/src/Icon.vue
<template>
+ <SvgIcon :size="size" :name="getSvgIcon" v-if="isSvgIcon" :class="[$attrs.class]" :spin="spin" />
+ <span
+ v-else
+ ref="elRef"
+ :class="[$attrs.class, 'app-iconify anticon', spin && 'app-iconify-spin']"
+ :style="getWrapStyle"
+ ></span>
+</template>
+<script lang="ts">
+ import type { PropType } from 'vue';
+ import {
+ defineComponent,
+ ref,
+ watch,
+ onMounted,
+ nextTick,
+ unref,
+ computed,
+ CSSProperties,
+ } from 'vue';
+
+ import SvgIcon from './SvgIcon.vue';
+ import Iconify from '@purge-icons/generated';
+ import { isString } from '/@/utils/is';
+ import { propTypes } from '/@/utils/propTypes';
+
+ const SVG_END_WITH_FLAG = '|svg';
+ export default defineComponent({
+ name: 'GIcon',
+ components: { SvgIcon },
+ props: {
+ // icon name
+ icon: propTypes.string,
+ // icon color
+ color: propTypes.string,
+ // icon size
+ size: {
+ type: [String, Number] as PropType<string | number>,
+ default: 16,
+ },
+ spin: propTypes.bool.def(false),
+ prefix: propTypes.string.def(''),
+ },
+ setup(props) {
+ const elRef = ref<ElRef>(null);
+
+ const isSvgIcon = computed(() => props.icon?.endsWith(SVG_END_WITH_FLAG));
+ const getSvgIcon = computed(() => props.icon.replace(SVG_END_WITH_FLAG, ''));
+ const getIconRef = computed(() => `${props.prefix ? props.prefix + ':' : ''}${props.icon}`);
+
+ const update = async () => {
+ if (unref(isSvgIcon)) return;
+
+ const el = unref(elRef);
+ if (!el) return;
+
+ await nextTick();
+ const icon = unref(getIconRef);
+ if (!icon) return;
+
+ const svg = Iconify.renderSVG(icon, {});
+ if (svg) {
+ el.textContent = '';
+ el.appendChild(svg);
+ } else {
+ const span = document.createElement('span');
+ span.className = 'iconify';
+ span.dataset.icon = icon;
+ el.textContent = '';
+ el.appendChild(span);
+ }
+ };
+
+ const getWrapStyle = computed((): CSSProperties => {
+ const { size, color } = props;
+ let fs = size;
+ if (isString(size)) {
+ fs = parseInt(size, 10);
+ }
+
+ return {
+ fontSize: `${fs}px`,
+ color: color,
+ display: 'inline-flex',
+ };
+ });
+
+ watch(() => props.icon, update, { flush: 'post' });
+
+ onMounted(update);
+
+ return { elRef, getWrapStyle, isSvgIcon, getSvgIcon };
+ },
+ });
+</script>
+<style lang="less">
+ .app-iconify {
+ display: inline-block;
+ // vertical-align: middle;
+
+ &-spin {
+ svg {
+ animation: loadingCircle 1s infinite linear;
+ }
+ }
+ }
+
+ span.iconify {
+ display: block;
+ min-width: 1em;
+ min-height: 1em;
+ background-color: @iconify-bg-color;
+ border-radius: 100%;
+ }
+</style>
+
由于图标选择器这个比较特殊的存在,项目会打包一些比较多的图标,图标选择器的图标需要事先指定并生成相应的文件。
yarn gen:icon
+
local 表示本地,online 表示在线,回车确认
到这里图标集已经生成完成了,此时你的图标选择器已经是你所选的的图标集的图标了。
注意不要频繁更新
如果前面选择的是本地生成的话,频繁更换图标集,可能会导致图标丢失或者显示不出来
该方式会在图标选择器使用到图标的时候进行在线请求,然后缓存对应的图标到浏览器。可以有效减少代码打包体积。
如果你的项目可以访问外网,建议可以使用这种方式
缺点: 在局域网或者无法访问到外网的环境中图标显示不出来
该方式会在打包的时候将图标选择器的图标全部打包到 js 内。在使用的时候不会额外的请求在线图标
缺点: 打包体积会偏大,具体的体积增加得看前面选择图标集的时候选择的图标数量的多少决定
使用 lint 的好处
具备基本工程素养的同学都会注重编码规范,而代码风格检查(Code Linting,简称 Lint)是保障代码规范一致性的重要手段。
遵循相应的代码规范有以下好处
项目内集成了以下几种代码校验方式
WARNING
lint 不是必须的,但是很有必要,一个项目做大了以后或者参与人员过多后,就会出现各种风格迥异的代码,对后续的维护造成了一定的麻烦
ESLint 是一个代码规范和错误检查工具,有以下几个特性
# 执行下面代码.能修复的会自动修复,不能修复的需要手动修改
+yarn run lint:eslint
+
项目的 eslint 配置位于根目录下 .eslintrc.js 内,可以根据团队自行修改代码规范
推荐使用 vscode 进行开发,vscode 自带 eslint 插件,可以自动修改一些错误。
同时项目内也自带了 vscode eslint 配置,具体在 .vscode/setting.json
文件夹内部。只要使用 vscode 开发不用任何设置即可使用
在一个团队中,每个人的 git 的 commit 信息都不一样,五花八门,没有一个机制很难保证规范化,如何才能规范化呢?可能你想到的是 git 的 hook 机制,去写 shell 脚本去实现。这当然可以,其实 JavaScript 有一个很好的工具可以实现这个模板,它就是 commitlint(用于校验 git 提交信息规范)。
commit-lint 的配置位于项目根目录下 commitlint.config.js
feat
增加新功能fix
修复问题/BUGstyle
代码风格相关无影响运行结果的perf
优化/性能提升refactor
重构revert
撤销修改test
测试相关docs
文档/注释chore
依赖更新/脚手架配置修改等workflow
工作流改进ci
持续集成mod
不确定分类的修改wip
开发中types
类型修改在 .husky/commit-msg
内注释以下代码即可
# npx --no-install commitlint --edit "$1"
+
+git commit -m 'feat(home): add home page'
+
+
stylelint 用于校验项目内部 css 的风格,加上编辑器的自动修复,可以很好的统一项目内部 css 风格
stylelint 配置位于根目录下 stylelint.config.js
如果您使用的是 vscode 编辑器的话,只需要安装下面插件,即可在保存的时候自动格式化文件内部 css 样式
插件
prettier 可以用于统一项目代码风格,统一的缩进,单双引号,尾逗号等等风格
prettier 配置文件位于项目根目录下 prettier.config.js
如果您使用的是 vscode 编辑器的话,只需要安装下面插件,即可在保存的时候自动格式化文件内部 js 格式
插件
git hook 一般结合各种 lint,在 git 提交代码的时候进行代码风格校验,如果校验没通过,则不会进行提交。需要开发者自行修改后再次进行提交
有一个问题就是校验会校验全部代码,但是我们只想校验我们自己提交的代码,这个时候就可以使用 husky。
最有效的解决方案就是将 Lint 校验放到本地,常见做法是使用 husky 或者 pre-commit 在本地提交之前先做一次 Lint 校验。
项目在 .husky
内部定义了相应的 hooks
# 删除husky依赖即可
+yarn remove huksy
+
+
# 加上 --no-verify即可跳过git hook校验(--no-verify 简写为 -n)
+git commit -m "xxx" --no-verify
+
用于自动修复提交文件风格问题
lint-staged 配置位于项目 .husky
目录下 lintstagedrc.js
module.exports = {
+ // 对指定格式文件 在提交的时候执行相应的修复命令
+ '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
+ '{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
+ 'package.json': ['prettier --write'],
+ '*.vue': ['eslint --fix', 'stylelint --fix', 'prettier --write', 'git add .'],
+ '*.{scss,less,styl,css,html}': ['stylelint --fix', 'prettier --write', 'git add .'],
+ '*.md': ['prettier --write'],
+};
+
项目中集成了三种权限处理方式:
实现原理: 在前端固定写死路由的权限,指定路由有哪些权限可以查看。只初始化通用的路由,需要权限才能访问的路由没有被加入路由表内。在登陆后或者其他方式获取用户角色后,通过角色去遍历路由表,获取该角色可以访问的路由表,生成路由表,再通过 router.addRoutes
添加到路由实例,实现权限的过滤。
缺点: 权限相对不自由,如果后台改动角色,前台也需要跟着改动。适合角色较固定的系统
ROLE
模式// ! 改动后需要清空浏览器缓存
+const setting: ProjectConfig = {
+ // 权限模式
+ permissionMode: PermissionModeEnum.ROLE,
+};
+
import type { AppRouteModule } from '/@/router/types';
+
+import { getParentLayout, LAYOUT } from '/@/router/constant';
+import { RoleEnum } from '/@/enums/roleEnum';
+import { t } from '/@/hooks/web/useI18n';
+
+const permission: AppRouteModule = {
+ path: '/permission',
+ name: 'Permission',
+ component: LAYOUT,
+ redirect: '/permission/front/page',
+ meta: {
+ icon: 'ion:key-outline',
+ title: t('routes.demo.permission.permission'),
+ },
+
+ children: [
+ {
+ path: 'front',
+ name: 'PermissionFrontDemo',
+ component: getParentLayout('PermissionFrontDemo'),
+ meta: {
+ title: t('routes.demo.permission.front'),
+ },
+ children: [
+ {
+ path: 'auth-pageA',
+ name: 'FrontAuthPageA',
+ component: () => import('/@/views/demo/permission/front/AuthPageA.vue'),
+ meta: {
+ title: t('routes.demo.permission.frontTestA'),
+ roles: [RoleEnum.SUPER],
+ },
+ },
+ {
+ path: 'auth-pageB',
+ name: 'FrontAuthPageB',
+ component: () => import('/@/views/demo/permission/front/AuthPageB.vue'),
+ meta: {
+ title: t('routes.demo.permission.frontTestB'),
+ roles: [RoleEnum.TEST],
+ },
+ },
+ ],
+ },
+ ],
+};
+
+export default permission;
+
详细代码见 src/router/guard/permissionGuard.ts
// 这里只列举了主要代码
+const routes = await permissionStore.buildRoutesAction();
+
+routes.forEach((route) => {
+ router.addRoute(route as unknown as RouteRecordRaw);
+});
+
+const redirectPath = (from.query.redirect || to.path) as string;
+const redirect = decodeURIComponent(redirectPath);
+const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
+permissionStore.setDynamicAddedRoute(true);
+next(nextData);
+
permissionStore.buildRoutesAction 用于过滤动态路由,详细代码见 src/store/modules/permission.ts
// 主要代码
+if (permissionMode === PermissionModeEnum.ROLE) {
+ const routeFilter = (route: AppRouteRecordRaw) => {
+ const { meta } = route;
+ const { roles } = meta || {};
+ if (!roles) return true;
+ return roleList.some((role) => roles.includes(role));
+ };
+ routes = filter(asyncRoutes, routeFilter);
+ routes = routes.filter(routeFilter);
+ // Convert multi-level routing to level 2 routing
+ routes = flatMultiLevelRoutes(routes);
+}
+
系统提供 usePermission 方便角色相关操作
import { usePermission } from '/@/hooks/web/usePermission';
+import { RoleEnum } from '/@/enums/roleEnum';
+
+export default defineComponent({
+ setup() {
+ const { changeRole } = usePermission();
+ // 更换为test角色
+ // 动态更改角色,传入角色名称,可以是数组
+ changeRole(RoleEnum.TEST);
+ return {};
+ },
+});
+
函数方式
usePermission 还提供了按钮级别的权限控制。
<template>
+ <a-button v-if="hasPermission([RoleEnum.TEST, RoleEnum.SUPER])" color="error" class="mx-4">
+ 拥有[test,super]角色权限可见
+ </a-button>
+</template>
+<script lang="ts">
+ import { usePermission } from '/@/hooks/web/usePermission';
+ import { RoleEnum } from '/@/enums/roleEnum';
+
+ export default defineComponent({
+ setup() {
+ const { hasPermission } = usePermission();
+
+ return { hasPermission };
+ },
+ });
+</script>
+
组件方式
具体查看权限组件使用
指令方式
TIP
指令方式不能动态更改权限
<a-button v-auth="RoleEnum.SUPER" type="primary" class="mx-4"> 拥有super角色权限可见</a-button>
+
实现原理: 是通过接口动态生成路由表,且遵循一定的数据结构返回。前端根据需要处理该数据为可识别的结构,再通过 router.addRoutes
添加到路由实例,实现权限的动态生成。
BACK
模式// ! 改动后需要清空浏览器缓存
+const setting: ProjectConfig = {
+ // 权限模式
+ permissionMode: PermissionModeEnum.BACK,
+};
+
permissionStore.buildRoutesAction 用于过滤动态路由,详细代码见 /@/store/modules/permission.ts
// 主要代码
+if (permissionMode === PermissionModeEnum.BACK) {
+ const { createMessage } = useMessage();
+
+ createMessage.loading({
+ content: t('sys.app.menuLoading'),
+ duration: 1,
+ });
+
+ // !Simulate to obtain permission codes from the background,
+ // this function may only need to be executed once, and the actual project can be put at the right time by itself
+ let routeList: AppRouteRecordRaw[] = [];
+ try {
+ this.changePermissionCode();
+ routeList = (await getMenuList()) as AppRouteRecordRaw[];
+ } catch (error) {
+ console.error(error);
+ }
+
+ // Dynamically introduce components
+ routeList = transformObjToRoute(routeList);
+
+ // Background routing to menu structure
+ const backMenuList = transformRouteToMenu(routeList);
+ this.setBackMenuList(backMenuList);
+
+ routeList = flatMultiLevelRoutes(routeList);
+ routes = [PAGE_NOT_FOUND_ROUTE, ...routeList];
+}
+
getMenuList 返回值格式
返回值由多个路由模块组成
注意
后端接口返回的数据中必须包含PageEnum.BASE_HOME
指定的路由(path定义于src/enums/pageEnum.ts
)
[
+ {
+ path: '/dashboard',
+ name: 'Dashboard',
+ component: '/dashboard/welcome/index',
+ meta: {
+ title: 'routes.dashboard.welcome',
+ affix: true,
+ icon: 'ant-design:home-outlined',
+ },
+ },
+ {
+ path: '/permission',
+ name: 'Permission',
+ component: 'LAYOUT',
+ redirect: '/permission/front/page',
+ meta: {
+ icon: 'carbon:user-role',
+ title: 'routes.demo.permission.permission',
+ },
+ children: [
+ {
+ path: 'back',
+ name: 'PermissionBackDemo',
+ meta: {
+ title: 'routes.demo.permission.back',
+ },
+
+ children: [
+ {
+ path: 'page',
+ name: 'BackAuthPage',
+ component: '/demo/permission/back/index',
+ meta: {
+ title: 'routes.demo.permission.backPage',
+ },
+ },
+ {
+ path: 'btn',
+ name: 'BackAuthBtn',
+ component: '/demo/permission/back/Btn',
+ meta: {
+ title: 'routes.demo.permission.backBtn',
+ },
+ },
+ ],
+ },
+ ],
+ },
+];
+
系统提供 usePermission 方便角色相关操作
import { usePermission } from '/@/hooks/web/usePermission';
+import { RoleEnum } from '/@/enums/roleEnum';
+
+export default defineComponent({
+ setup() {
+ const { changeMenu } = usePermission();
+
+ // 更改菜单的实现需要自行去修改
+ changeMenu();
+ return {};
+ },
+});
+
函数方式
usePermission 还提供了按钮级别的权限控制。
<template>
+ <a-button v-if="hasPermission(['20000', '2000010'])" color="error" class="mx-4">
+ 拥有[20000,2000010]code可见
+ </a-button>
+</template>
+<script lang="ts">
+ import { usePermission } from '/@/hooks/web/usePermission';
+ import { RoleEnum } from '/@/enums/roleEnum';
+
+ export default defineComponent({
+ setup() {
+ const { hasPermission } = usePermission();
+ return { hasPermission };
+ },
+ });
+</script>
+
组件方式
具体查看权限组件使用
指令方式
TIP
指令方式不能动态更改权限
<a-button v-auth="'1000'" type="primary" class="mx-4"> 拥有code ['1000']权限可见 </a-button>
+
通常,如需做按钮级别权限,后台会提供相应的 code,或者类型的判断标识。这些编码只需要在登录后获取一次即可。
import { getPermCodeByUserId } from '/@/api/sys/user';
+import { permissionStore } from '/@/store/modules/permission';
+async function changePermissionCode(userId: string) {
+ // 从后台获取当前用户拥有的编码
+ const codeList = await getPermCodeByUserId({ userId });
+ permissionStore.commitPermCodeListState(codeList);
+}
+
项目目前的组件注册机制是按需注册,是在需要用到的页面才引入。
<template>
+ <Menu>
+ <SubMenu></SubMenu>
+ <Menu>
+
+ <menu>
+ <sub-menu></sub-menu>
+ <menu>
+</template>
+<script>
+import { Menu } from 'ant-design-vue';
+export default defineComponent({
+ components: {
+ Menu: Menu,
+ SubMenu: Menu.SubMenu
+ },
+})
+</script>
+
tsx 文件内不能使用全局注册组件
import { Menu } from 'ant-design-vue';
+
+export default defineComponent({
+ setup() {
+ return () => (
+ <Menu>
+ <Menu.SubMenu></Menu.SubMenu>
+ </Menu>
+ );
+ },
+});
+
如果不习惯按需引入方式,可以进行全局注册。全局注册也分两种方式
只注册需要的组件
代码地址:src/components/registerGlobComp.ts
import {
+ // Need
+ Button as AntButton,
+ Optional,
+ Select,
+ Alert,
+ Checkbox,
+ DatePicker,
+ Radio,
+ Switch,
+ Card,
+ List,
+ Tabs,
+ Descriptions,
+ Tree,
+ Table,
+ Divider,
+ Modal,
+ Drawer,
+ Dropdown,
+ Tag,
+ Tooltip,
+ Badge,
+ Popover,
+ Upload,
+ Transfer,
+ Steps,
+ PageHeader,
+ Result,
+ Empty,
+ Avatar,
+ Menu,
+ Breadcrumb,
+ Form,
+ Input,
+ Row,
+ Col,
+ Spin,
+} from 'ant-design-vue';
+
+export function registerGlobComp(app: App) {
+ app
+ .use(Select)
+ .use(Alert)
+ .use(Breadcrumb)
+ .use(Checkbox)
+ .use(DatePicker)
+ .use(Radio)
+ .use(Switch)
+ .use(Card)
+ .use(List)
+ .use(Descriptions)
+ .use(Tree)
+ .use(Table)
+ .use(Divider)
+ .use(Modal)
+ .use(Drawer)
+ .use(Dropdown)
+ .use(Tag)
+ .use(Tooltip)
+ .use(Badge)
+ .use(Popover)
+ .use(Upload)
+ .use(Transfer)
+ .use(Steps)
+ .use(PageHeader)
+ .use(Result)
+ .use(Empty)
+ .use(Avatar)
+ .use(Menu)
+ .use(Tabs)
+ .use(Form)
+ .use(Input)
+ .use(Row)
+ .use(Col)
+ .use(Spin);
+}
+
main.ts
内import { createApp } from 'vue';
+import Antd from 'ant-design-vue';
+import 'ant-design-vue/dist/antd.less';
+const app = createApp(App);
+app.use(Antd);
+
if (import.meta.env.DEV) {
+ import('ant-design-vue/dist/antd.less');
+}
+
前言
由于是展示项目,所以打包后相对较大,如果项目中没有用到的插件,可以删除对应的文件或者路由,不引用即可,没有引用就不会打包。
当然,你也可以使用精简版 vue-vben-admin-thin 进行开发。
项目开发完成之后,执行以下命令进行构建
yarn build
+
构建打包成功之后,会在根目录生成 dist 文件夹,里面就是构建打包好的文件
在 .env.production 内
设置 VITE_LEGACY=true
即可打包出兼容旧版浏览器的代码
VITE_LEGACY = true
+
发布之前可以在本地进行预览,有多种方式,这里介绍两种
不能直接打开构建后的 html 文件
# 先打包再进行预览
+yarn preview
+# 直接预览本地 dist 文件目录
+yarn preview:dist
+
# 1.全局安装live-server
+yarn global add live-server
+# 2. 进入打包的后目录
+cd ./dist
+# 本地预览,默认端口8080
+live-server
+# 指定端口
+live-server --port 9000
+
如果你的构建文件很大,可以通过项目内置 rollup-plugin-analyzer 插件进行代码体积分析,从而优化你的代码。
yarn report
+
+
运行之后,在自动打开的页面可以看到具体的体积分布,以分析哪些依赖有问题。
TIP
左上角可以切换 显示 gzip 或者 brotli
开启 gzip,并配合 nginx 的 gzip_static
功能可以大大加快页面访问速度
TIP
只需开启 VITE_BUILD_COMPRESS='gzip'
即可在打包的同时生成 .gz 文件
# 根据自己路径来配置更改
+# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
+VITE_PUBLIC_PATH=/
+
brotli 是比 gzip 压缩率更高的算法,可以与 gzip 共存不会冲突,需要 nginx 安装指定模块并开启即可。
TIP
只需开启 VITE_BUILD_COMPRESS='brotli'
即可在打包的同时生成 .br 文件
# 根据自己路径来配置更改
+# 例如部署在nginx /next/路径下 则VITE_PUBLIC_PATH=/next/
+VITE_PUBLIC_PATH=/
+
只需开启 VITE_BUILD_COMPRESS='brotli,gzip'
即可在打包的同时生成 .gz
和 .br
文件。
http {
+ # 开启gzip
+ gzip on;
+ # 开启gzip_static
+ # gzip_static 开启后可能会报错,需要安装相应的模块, 具体安装方式可以自行查询
+ # 只有这个开启,vue文件打包的.gz文件才会有效果,否则不需要开启gzip进行打包
+ gzip_static on;
+ gzip_proxied any;
+ gzip_min_length 1k;
+ gzip_buffers 4 16k;
+ #如果nginx中使用了多层代理 必须设置这个才可以开启gzip。
+ gzip_http_version 1.0;
+ gzip_comp_level 2;
+ gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;
+ gzip_vary off;
+ gzip_disable "MSIE [1-6]\.";
+
+ # 开启 brotli压缩
+ # 需要安装对应的nginx模块,具体安装方式可以自行查询
+ # 可以与gzip共存不会冲突
+ brotli on;
+ brotli_comp_level 6;
+ brotli_buffers 16 8k;
+ brotli_min_length 20;
+ brotli_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript image/svg+xml;
+}
+
注意
项目默认是在生产环境开启 Mock,这样做非常不好,只是为了演示环境有数据,不建议在生产环境使用 Mock,而应该使用真实的后台接口,并将 Mock 关闭。
简单的部署只需要将最终生成的静态文件,dist 文件夹的静态文件发布到你的 cdn 或者静态服务器即可,需要注意的是其中的 index.html 通常会是你后台服务的入口页面,在确定了 js 和 css 的静态之后可能需要改变页面的引入路径。
例如上传到 nginx
/srv/www/project/index.html
# nginx配置
+location / {
+ # 不缓存html,防止程序更新后缓存继续生效
+ if ($request_filename ~* .*\.(?:htm|html)$) {
+ add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate";
+ access_log on;
+ }
+ # 这里是vue打包文件dist内的文件的存放路径
+ root /srv/www/project/;
+ index index.html index.htm;
+}
+
+
部署时可能会发现资源路径不对,只需要修改.env.production
文件即可。
# 根据自己路径来配置更改
+# 注意需要以 / 开头和结尾
+VITE_PUBLIC_PATH=/
+VITE_PUBLIC_PATH=/xxx/
+
项目前端路由使用的是 vue-router,所以你可以选择两种方式:history 和 hash。
#
history
需要服务器配合可在 src/router/index.ts 内进行 mode 修改
import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router';
+
+createRouter({
+ history: createWebHashHistory(),
+ // or
+ history: createWebHistory(),
+});
+
开启 history 模式需要服务器配置,更多的服务器配置详情可以看 history-mode
这里以 nginx 配置为例
部署到根目录
server {
+ listen 80;
+ location / {
+ # 用于配合 History 使用
+ try_files $uri $uri/ /index.html;
+ }
+}
+
部署到非根目录
# 在.env.production内,配置子目录路径
+VITE_PUBLIC_PATH = /sub/
+
server {
+ listen 80;
+ server_name localhost;
+ location /sub/ {
+ # 这里是vue打包文件dist内的文件的存放路径
+ alias /srv/www/project/;
+ index index.html index.htm;
+ try_files $uri $uri/ /sub/index.html;
+ }
+}
+
使用 nginx 处理项目部署后的跨域问题
# 在.env.production内,配置接口地址
+VITE_GLOB_API_URL=/api
+
server {
+ listen 8080;
+ server_name localhost;
+ # 接口代理,用于解决跨域问题
+ location /api {
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ # 后台接口地址
+ proxy_pass http://110.110.1.1:8080/api;
+ proxy_redirect default;
+ add_header Access-Control-Allow-Origin *;
+ add_header Access-Control-Allow-Headers X-Requested-With;
+ add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
+ }
+}
+
主要介绍如何在项目中使用和规划样式文件。
默认使用 less 作为预处理语言,建议在使用前或者遇到疑问时学习一下 Less 的相关特性(如果想获取基础的 CSS 知识或查阅属性,请参考 MDN 文档)。
项目中使用的通用样式,都存放于 src/design/ 下面。
.
+├── ant # ant design 一些样式覆盖
+├── color.less # 颜色
+├── index.less # 入口
+├── public.less # 公共类
+├── theme.less # 主题相关
+├── config.less # 每个组件都会自动引入样式
+├── transition # 动画相关
+└── var # 变量
+
+
全局注入
config.less 这个文件会被全局注入到所有文件,所以在页面内可以直接使用变量而不需要手动引入
<style lang="less" scoped>
+ // 这里已经隐式注入了 config.less
+</style>
+
项目中引用到了 tailwindcss,具体可以见文件使用说明。
语法如下:
<div class="relative w-full h-full px-4"></div>
+
项目中使用了 windicss,具体参见文件使用说明。
语法如下:
<div class="relative w-full h-full px-4"></div>
+
注意事项
windcss 目前会造成本地开发内存溢出,所以后续可能会考虑切换到 TailwindCss,两者基本相同。
所以尽量少用 Windicss 新增的特性,防止后续切换成本高。
主要是因为 Ant Design 默认使用 less 作为样式语言,使用 Less 可以跟其保持一致。
没有加 scoped
属性,默认会编译成全局样式,可能会造成全局污染
<style></style>
+
+<style scoped></style>
+
温馨提醒
使用 scoped 后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受其父组件的 scoped CSS 和子组件的 scoped CSS 的影响。这样设计是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式。
有时我们可能想明确地制定一个针对子组件的规则。
如果你希望 scoped
样式中的一个选择器能够作用得“更深”,例如影响子组件,你可以使用 >>>
操作符。有些像 Sass 之类的预处理器无法正确解析 >>>
。这种情况下你可以使用 /deep/
或 ::v-deep
操作符取而代之——两者都是 >>>
的别名,同样可以正常工作。
详情可以查看 RFC0023-scoped-styles-changes。
使用 scoped 后,父组件的样式将不会渗透到子组件中,所以可以使用以下方式解决:
<style scoped>
+ /* deep selectors */
+ ::v-deep(.foo) {
+ }
+ /* shorthand */
+ :deep(.foo) {
+ }
+
+ /* targeting slot content */
+ ::v-slotted(.foo) {
+ }
+ /* shorthand */
+ :slotted(.foo) {
+ }
+
+ /* one-off global rule */
+ ::v-global(.foo) {
+ }
+ /* shorthand */
+ :global(.foo) {
+ }
+</style>
+
针对样式覆盖问题,还有一种方案是使用 CSS Modules 模块化方案。使用方式如下。
<template>
+ <span :class="$style.span1">hello</span>
+</template>
+
+<script>
+ import { useCSSModule } from 'vue';
+
+ export default {
+ setup(props, context) {
+ const $style = useCSSModule();
+ const moduleAStyle = useCSSModule('moduleA');
+ return {
+ $style,
+ moduleAStyle,
+ };
+ },
+ };
+</script>
+
+<style lang="less" module>
+ .span1 {
+ color: green;
+ font-size: 30px;
+ }
+</style>
+
+<style lang="less" module="moduleA">
+ .span1 {
+ color: green;
+ font-size: 30px;
+ }
+</style>
+
加上 reference 可以解决页面内重复引用导致实际生成的 style 样式表重复的问题。
这步已经全局引入了。所以可以不写,直接使用变量
<style lang="less" scoped>
+ /* 该行代码已全局引用。可以不用单独引入 */
+ @import (reference) '../../design/config.less';
+<style>
+
这种模式会先启动 vite 服务,Electron 使用 Url 地址来进行渲染
Electron 代码在 electron-main 分支
# clone electron-main分支代码
+git clone -b electron-main https://github.com/vbenjs/vue-vben-admin vben-admin-electron
+
yarn
+
提示
首次下载 Electron 依赖会比较慢,可以在项目根目录下新建.npmrc
文件,填入下方内容即可
ELETRON_MIRROR=https://npm.taobao.org/mirrors/electron/
+
yarn dev:app
+
yarn build:app
+
TODO: 待适配
本文会帮助你从头启动项目
关于组件
项目虽然二次封装了一些组件,但是可能不能满足大部分的要求。所以,如果组件不满足你的要求,完全可以不用甚至删除代码自己写,不必坚持使用项目自带的组件。
如果您使用的 IDE 是vscode(推荐)的话,可以安装以下工具来提高开发效率及代码格式化
注意
注意存放代码的目录及所有父级目录不能存在中文、韩文、日文以及空格,否则安装依赖后启动会出错。
# clone 代码
+git clone https://github.com/vbenjs/vue-vben-admin.git
+
+
如果从 github clone 代码较慢的话,可以尝试用 Gitee 同步代码到自己的仓库,再 clone 下来即可。
也可以通过下方地址进行 clone
git clone https://gitee.com/annsion/vue-vben-admin.git
+
注意
Gitee的代码可能不是最新的
如果您电脑未安装Node.js,请安装它。
验证
# 出现相应npm版本即可
+npm -v
+# 出现相应node版本即可
+node -v
+
+
如果你需要同时存在多个 node 版本,可以使用 Nvm 或者其他工具进行 Node.js 进行版本管理。
必须使用 pnpm进行依赖安装(若其他包管理器安装不了需要自行处理)。
如果未安装pnpm
,可以用下面命令来进行全局安装
# 全局安装pnpm
+npm install -g pnpm
+# 验证
+pnpm -v # 出现对应版本号即代表安装成功
+
在项目根目录下,打开命令窗口执行,耐心等待安装完成即可
# 安装依赖
+pnpm i
+
安装依赖时 husky 安装失败
请查看你的源码是否从 github 直接下载的,直接下载是没有 .git
文件夹的,而 husky
需要依赖 git
才能安装。此时需使用 git init
初始化项目,再尝试重新安装即可。
"scripts": {
+ # 安装依赖
+ "bootstrap": "pnpm install",
+ # 构建项目
+ "build": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build",
+ # 生成打包分析,在电脑上执行完成后会自动打开界面
+ "build:analyze": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode analyze",
+ # 构建成docker镜像
+ "build:docker": "vite build --mode docker",
+ # 清空缓存后构建项目
+ "build:no-cache": "pnpm clean:cache && npm run build",
+ "build:test": "cross-env NODE_OPTIONS=--max-old-space-size=8192 pnpm vite build --mode test",
+ # 用于生成标准化的git commit message
+ "commit": "czg",
+ # 运行项目
+ "dev": "pnpm vite",
+ "preinstall": "npx only-allow pnpm",
+ "postinstall": "turbo run stub",
+ "lint": "turbo run lint",
+ # 执行 eslint 校验,并修复部分问题
+ "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock}/**/*.{vue,ts,tsx}\" --fix",
+ # 执行 prettier 格式化(该命令会对项目所有代码进行 prettier 格式化,请谨慎执行)
+ "lint:prettier": "prettier --write .",
+ # 执行 stylelint 格式化
+ "lint:stylelint": "stylelint \"**/*.{vue,css,less,scss}\" --fix --cache --cache-location node_modules/.cache/stylelint/",
+ # 安装git hooks
+ "prepare": "husky install",
+ # 预览打包后的内容(先打包在进行预览)
+ "preview": "npm run build && vite preview",
+ # 重新安装依赖
+ "reinstall": "rimraf pnpm-lock.yaml && rimraf package.lock.json && rimraf node_modules && npm run bootstrap",
+ # 运行项目
+ "serve": "npm run dev",
+ # 对打包结果进行 gzip 测试
+ "test:gzip": "npx http-server dist --cors --gzip -c-1",
+ # 类型检查
+ "type:check": "vue-tsc --noEmit --skipLibCheck"
+},
+
该命令会生成所选择的图标集,提供给图标选择器使用。具体使用方式请查看 图标集生成
该命令会先删除 node_modules
、yarn.lock
、package.lock.json
后再进行依赖重新安装(安装速度会明显变慢)。
接下来你可以修改代码进行业务开发了。我们内建了模拟数据、HMR 实时预览、状态管理、国际化、全局路由等各种实用的功能辅助开发,请阅读其他章节了解更多。
+.
+├── build # 打包脚本相关
+│ ├── config # 配置文件
+│ ├── generate # 生成器
+│ ├── script # 脚本
+│ └── vite # vite配置
+├── mock # mock文件夹
+├── public # 公共静态资源目录
+├── src # 主目录
+│ ├── api # 接口文件
+│ ├── assets # 资源文件
+│ │ ├── icons # icon sprite 图标文件夹
+│ │ ├── images # 项目存放图片的文件夹
+│ │ └── svg # 项目存放svg图片的文件夹
+│ ├── components # 公共组件
+│ ├── design # 样式文件
+│ ├── directives # 指令
+│ ├── enums # 枚举/常量
+│ ├── hooks # hook
+│ │ ├── component # 组件相关hook
+│ │ ├── core # 基础hook
+│ │ ├── event # 事件相关hook
+│ │ ├── setting # 配置相关hook
+│ │ └── web # web相关hook
+│ ├── layouts # 布局文件
+│ │ ├── default # 默认布局
+│ │ ├── iframe # iframe布局
+│ │ └── page # 页面布局
+│ ├── locales # 多语言
+│ ├── logics # 逻辑
+│ ├── main.ts # 主入口
+│ ├── router # 路由配置
+│ ├── settings # 项目配置
+│ │ ├── componentSetting.ts # 组件配置
+│ │ ├── designSetting.ts # 样式配置
+│ │ ├── encryptionSetting.ts # 加密配置
+│ │ ├── localeSetting.ts # 多语言配置
+│ │ ├── projectSetting.ts # 项目配置
+│ │ └── siteSetting.ts # 站点配置
+│ ├── store # 数据仓库
+│ ├── utils # 工具类
+│ └── views # 页面
+├── types # 类型文件
+└── vite.config.ts # vite配置文件
+
+
Vue-Vben-Admin 是一个基于 Vue3.0、Vite、 Ant-Design-Vue、TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模版,以帮助你快速搭建企业级中后台产品原型。也可以作为一个示例,用于学习 vue3
、vite
、ts
等主流技术。该项目会持续跟进最新技术,并将其应用在项目中。
如需本地运行文档,请拉取代码到本地。
# 拉取代码
+git clone https://github.com/vbenjs/vue-vben-admin-doc
+
+# 安装依赖
+yarn
+
+# 运行项目
+yarn dev
+
本项目需要一定前端基础知识,请确保掌握 Vue 的基础知识,以便能处理一些常见的问题。建议在开发前先学一下以下内容,提前了解和学习这些知识,会对项目理解非常有帮助:
该版本主要是提供一些 Demo
示例及插件的使用集成方式,主要用于参考。如果对项目不是很熟悉,不建议在此基础上进行开发,请使用下方提供的精简版本。
vue-vben-admin
精简版本。删除了相关示例、无用文件及功能、依赖。可以根据自身需求安装对应的依赖。因为使用的是 vite
,依赖删除不会导致相关组件或者 hook
发出警告。只在需要的时候安装对应的库即可。
如果这些插件对你有帮助,可以给一个 star 支持下
mock
html
模版转换,可以在html
文件内进行书写模版语法.gz
|.br
文件svg sprite
本地开发推荐使用Chrome 最新版
浏览器,不支持Chrome 80
以下版本。
生产环境支持现代浏览器,不支持 IE。
IE | Edge | Firefox | Chrome | Safari |
---|---|---|---|---|
not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
除了自带组件以外,有时我们还需要引入其他外部模块。我们以 ant-design-vue
为例:
安装 ant-design-vue
# 在终端输入下面的命令完成安装
+yarn add ant-design-vue
+
import { createApp } from 'vue';
+import App from './App.vue';
+import Antd from 'ant-design-vue';
+const app = createApp(App);
+app.use(Antd);
+app.mount('#app');
+
<template>
+ <Button>text</Button>
+</template>
+
+<script>
+ import { defineComponent } from 'vue';
+ import { Button } from 'ant-design-vue';
+ export default defineComponent({
+ components: {
+ Button,
+ },
+ });
+</script>
+
项目菜单配置存放于 src/router/menus 下面
提示
菜单必须和路由匹配才能显示
export interface Menu {
+ // 菜单名
+ name: string;
+ // 菜单图标,如果没有,则会尝试使用route.meta.icon
+ icon?: string;
+ // 菜单图片,如果同时传递了icon和img,则只会显示img
+ img?: string;
+ // 菜单路径
+ path: string;
+ // 是否禁用
+ disabled?: boolean;
+ // 子菜单
+ children?: Menu[];
+ // 菜单标签设置
+ tag: {
+ // 为true则显示小圆点
+ dot: boolean;
+ // 内容
+ content: string';
+ // 类型
+ type: 'error' | 'primary' | 'warn' | 'success';
+ };
+ // 是否隐藏菜单
+ hideMenu?: boolean;
+}
+
一个菜单文件会被当作一个模块
提示
children 的 path 字段不需要以/
开头
import type { MenuModule } from '/@/router/types';
+import { t } from '/@/hooks/web/useI18n';
+const menu: MenuModule = {
+ orderNo: 10,
+ menu: {
+ name: t('routes.dashboard.dashboard'),
+ path: '/dashboard',
+
+ children: [
+ {
+ path: 'analysis',
+ name: t('routes.dashboard.analysis'),
+ },
+ {
+ path: 'workbench',
+ name: t('routes.dashboard.workbench'),
+ },
+ ],
+ },
+};
+export default menu;
+
以上模块会转化成以下结构
[
+ path: '/dashboard',
+ name: t('routes.dashboard.dashboard'),
+ children: [
+ {
+ path: 'dashboard/analysis',
+ name: t('routes.dashboard.analysis'),
+ },
+ {
+ path: 'dashboard/workbench',
+ name: t('routes.dashboard.workbench'),
+ },
+ ],
+]
+
直接在 src/router/routes/modules 内新增一个模块文件即可。
不需要手动引入,放在src/router/routes/modules 内的文件会自动被加载。
在菜单模块内,设置 orderNo
变量,数值越大,排序越靠后
如果前端应用和后端接口服务器没有运行在同一个主机上,你需要在开发环境下将接口请求代理到接口服务器。
如果是同一个主机,可以直接请求具体的接口地址。
开发环境时候,接口地址在项目根目录下
.env.development 文件配置
# vite 本地跨域代理
+VITE_PROXY=[["/basic-api","http://localhost:3000"]]
+# 接口地址
+VITE_GLOB_API_URL=/api
+
TIP
TIP
v3.0.0开始,作者重构了vite.config.ts,新版本不再支持VITE_PROXY环境变量。
如果你在 src/api/
下面的接口为下方代码,且 .env.development 文件配置如下注释,则在控制台看到的地址为 http://localhost:3100/basic-api/login
。
由于 /basic-api
匹配到了设置的 VITE_PROXY
,所以上方实际是请求 http://localhost:3000/login,这样同时也解决了跨域问题。(3100为项目端口号,http://localhost:3000为PROXY代理的目标地址)
// .env.development
+// VITE_PROXY=[["/basic-api","http://localhost:3000"]]
+// VITE_GLOB_API_URL=/basic-api
+
+enum Api {
+ Login = '/login',
+}
+
+/**
+ * @description: 用户登陆
+ */
+export function loginApi(params: LoginParams) {
+ return http.request<LoginResultModel>({
+ url: Api.Login,
+ method: 'POST',
+ params,
+ });
+}
+
如果没有跨域问题,可以直接忽略 VITE_PROXY 配置,直接将接口地址设置在 VITE_GLOB_API_URL
# 例如接口地址为 http://localhost:3000 则
+VITE_GLOB_API_URL=http://localhost:3000
+
如果有跨域问题,将 VITE_GLOB_API_URL 设置为跟 VITE_PROXY 内其中一个数组的第一个项一致的值即可。
下方的接口地址设置为 /basic-api
,当请求发出的时候会经过 Vite 的 proxy 代理,匹配到了我们设置的 VITE_PROXY 规则,将 /basic-api
转化为 http://localhost:3000
进行请求
# 例如接口地址为 http://localhost:3000 则
+VITE_PROXY=[["/basic-api","http://localhost:3000"]]
+# 接口地址
+VITE_GLOB_API_URL=/basic-api
+
在 vite.config.ts
配置文件中,提供了 server 的 proxy 功能,用于代理 API 请求。
server: {
+ proxy: {
+ "/basic-api":{
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ ws: true,
+ rewrite: (path) => path.replace(new RegExp(`^/basic-api`), ''),
+ }
+ },
+},
+
注意
从浏览器控制台的 Network 看,请求是 http://localhost:3000/basic-api/xxx
,这是因为 proxy 配置不会改变本地请求的 url。
生产环境接口地址在项目根目录下 .env.production 文件配置。
生产环境接口地址值需要修改 VITE_GLOB_API_URL,如果出现跨域问题,可以使用 nginx 或者后台开启 cors 进行处理
打包后如何进行地址修改?
VITE_GLOB_* 开头的变量会在打包的时候注入 _app.config.js 文件内。
在 dist/_app.config.js 修改相应的接口地址后刷新页面即可,不需要在根据不同环境打包多次,一次打包可以用于多个不同接口环境的部署。
在 vue-vben-admin 中:
接口统一存放于 src/api/ 下面管理
以登陆接口为例:
在 src/api/ 内新建模块文件,其中参数与返回值最好定义一下类型,方便校验。虽然麻烦,但是后续维护字段很方便。
TIP
类型定义文件可以抽取出去统一管理,具体参考项目
import { defHttp } from '/@/utils/http/axios';
+import { LoginParams, LoginResultModel } from './model/userModel';
+
+enum Api {
+ Login = '/login',
+}
+
+export function loginApi(params: LoginParams) {
+ return defHttp.request<LoginResultModel>({
+ url: Api.Login,
+ method: 'POST',
+ params,
+ });
+}
+
axios 请求封装存放于 src/utils/http/axios 文件夹内部
除 index.ts
文件内容需要根据项目自行修改外,其余文件无需修改
+├── Axios.ts // axios实例
+├── axiosCancel.ts // axiosCancel实例,取消重复请求
+├── axiosTransform.ts // 数据转换类
+├── checkStatus.ts // 返回状态值校验
+├── index.ts // 接口返回统一处理
+
+
const axios = new VAxios({
+ // 认证方案,例如: Bearer
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
+ authenticationScheme: '',
+ // 接口超时时间 单位毫秒
+ timeout: 10 * 1000,
+ // 接口可能会有通用的地址部分,可以统一抽取出来
+ prefixUrl: prefix,
+ headers: { 'Content-Type': ContentTypeEnum.JSON },
+ // 数据处理方式,见下方说明
+ transform,
+ // 配置项,下面的选项都可以在独立的接口请求中覆盖
+ requestOptions: {
+ // 默认将prefix 添加到url
+ joinPrefix: true,
+ // 是否返回原生响应头 比如:需要获取响应头时使用该属性
+ isReturnNativeResponse: false,
+ // 需要对返回数据进行处理
+ isTransformRequestResult: true,
+ // post请求的时候添加参数到url
+ joinParamsToUrl: false,
+ // 格式化提交参数时间
+ formatDate: true,
+ // 消息提示类型
+ errorMessageMode: 'message',
+ // 接口地址
+ apiUrl: globSetting.apiUrl,
+ // 是否加入时间戳
+ joinTime: true,
+ // 忽略重复请求
+ ignoreCancelToken: true,
+ },
+});
+
transform 数据处理说明
类型定义,见 axiosTransform.ts 文件
export abstract class AxiosTransform {
+ /**
+ * @description: 请求之前处理配置
+ */
+ beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
+
+ /**
+ * @description: 请求成功处理
+ */
+ transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
+
+ /**
+ * @description: 请求失败处理
+ */
+ requestCatch?: (e: Error) => Promise<any>;
+
+ /**
+ * @description: 请求之前的拦截器
+ */
+ requestInterceptors?: (config: AxiosRequestConfig) => AxiosRequestConfig;
+
+ /**
+ * @description: 请求之后的拦截器
+ */
+ responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
+
+ /**
+ * @description: 请求之前的拦截器错误处理
+ */
+ requestInterceptorsCatch?: (error: Error) => void;
+
+ /**
+ * @description: 请求之后的拦截器错误处理
+ */
+ responseInterceptorsCatch?: (error: Error) => void;
+}
+
+
+
项目默认 transform 处理逻辑,可以根据各自项目进行处理。一般需要更改的部分为下方代码,见代码注释说明
/**
+ * @description: 数据处理,方便区分多种处理方式
+ */
+const transform: AxiosTransform = {
+ /**
+ * @description: 处理请求数据。如果数据不是预期格式,可直接抛出错误
+ */
+ transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
+ const { t } = useI18n();
+ const { isTransformResponse, isReturnNativeResponse } = options;
+ // 是否返回原生响应头 比如:需要获取响应头时使用该属性
+ if (isReturnNativeResponse) {
+ return res;
+ }
+ // 不进行任何处理,直接返回
+ // 用于页面代码可能需要直接获取code,data,message这些信息时开启
+ if (!isTransformResponse) {
+ return res.data;
+ }
+ // 错误的时候返回
+
+ const { data } = res;
+ if (!data) {
+ // return '[HTTP] Request has no return value';
+ throw new Error(t('sys.api.apiRequestFailed'));
+ }
+ // 这里 code,result,message为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
+ const { code, result, message } = data;
+
+ // 这里逻辑可以根据项目进行修改
+ const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
+ if (hasSuccess) {
+ return result;
+ }
+
+ // 在此处根据自己项目的实际情况对不同的code执行不同的操作
+ // 如果不希望中断当前请求,请return数据,否则直接抛出异常即可
+ let timeoutMsg = '';
+ switch (code) {
+ case ResultEnum.TIMEOUT:
+ timeoutMsg = t('sys.api.timeoutMessage');
+ default:
+ if (message) {
+ timeoutMsg = message;
+ }
+ }
+
+ // errorMessageMode=‘modal’的时候会显示modal错误弹窗,而不是消息提示,用于一些比较重要的错误
+ // errorMessageMode='none' 一般是调用时明确表示不希望自动弹出错误提示
+ if (options.errorMessageMode === 'modal') {
+ createErrorModal({ title: t('sys.api.errorTip'), content: timeoutMsg });
+ } else if (options.errorMessageMode === 'message') {
+ createMessage.error(timeoutMsg);
+ }
+
+ throw new Error(timeoutMsg || t('sys.api.apiRequestFailed'));
+ },
+
+ // 请求之前处理config
+ beforeRequestHook: (config, options) => {
+ const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true } = options;
+
+ if (joinPrefix) {
+ config.url = `${urlPrefix}${config.url}`;
+ }
+
+ if (apiUrl && isString(apiUrl)) {
+ config.url = `${apiUrl}${config.url}`;
+ }
+ const params = config.params || {};
+ if (config.method?.toUpperCase() === RequestEnum.GET) {
+ if (!isString(params)) {
+ // 给 get 请求加上时间戳参数,避免从缓存中拿数据。
+ config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
+ } else {
+ // 兼容restful风格
+ config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
+ config.params = undefined;
+ }
+ } else {
+ if (!isString(params)) {
+ formatDate && formatRequestDate(params);
+ config.data = params;
+ config.params = undefined;
+ if (joinParamsToUrl) {
+ config.url = setObjToUrlParams(config.url as string, config.data);
+ }
+ } else {
+ // 兼容restful风格
+ config.url = config.url + params;
+ config.params = undefined;
+ }
+ }
+ return config;
+ },
+
+ /**
+ * @description: 请求拦截器处理
+ */
+ requestInterceptors: (config, options) => {
+ // 请求之前处理config
+ const token = getToken();
+ if (token) {
+ // jwt token
+ config.headers.Authorization = options.authenticationScheme
+ ? `${options.authenticationScheme} ${token}`
+ : token;
+ }
+ return config;
+ },
+
+ /**
+ * @description: 响应拦截器处理
+ */
+ responseInterceptors: (res: AxiosResponse<any>) => {
+ return res;
+ },
+
+ /**
+ * @description: 响应错误处理
+ */
+ responseInterceptorsCatch: (error: any) => {
+ const { t } = useI18n();
+ const errorLogStore = useErrorLogStoreWithOut();
+ errorLogStore.addAjaxErrorInfo(error);
+ const { response, code, message, config } = error || {};
+ const errorMessageMode = config?.requestOptions?.errorMessageMode || 'none';
+ const msg: string = response?.data?.error?.message ?? '';
+ const err: string = error?.toString?.() ?? '';
+ let errMessage = '';
+
+ try {
+ if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
+ errMessage = t('sys.api.apiTimeoutMessage');
+ }
+ if (err?.includes('Network Error')) {
+ errMessage = t('sys.api.networkExceptionMsg');
+ }
+
+ if (errMessage) {
+ if (errorMessageMode === 'modal') {
+ createErrorModal({ title: t('sys.api.errorTip'), content: errMessage });
+ } else if (errorMessageMode === 'message') {
+ createMessage.error(errMessage);
+ }
+ return Promise.reject(error);
+ }
+ } catch (error) {
+ throw new Error(error);
+ }
+
+ checkStatus(error?.response?.status, msg, errorMessageMode);
+ return Promise.reject(error);
+ },
+};
+
项目接口默认为 Json 参数格式,即 headers: { 'Content-Type': ContentTypeEnum.JSON }
,
如果需要更改为 form-data
格式,更改 headers 的 'Content-Type
为 ContentTypeEnum.FORM_URLENCODED
即可
当项目中需要用到多个接口地址时, 可以在 src/utils/http/axios/index.ts 导出多个 axios 实例
// 目前只导出一个默认实例,接口地址对应的是环境变量中的 VITE_GLOB_API_URL 接口地址
+export const defHttp = createAxios();
+
+// 需要有其他接口地址的可以在后面添加
+
+// other api url
+export const otherHttp = createAxios({
+ requestOptions: {
+ apiUrl: 'xxx',
+ },
+});
+
如果不需要 url 上面默认携带的时间戳参数 ?_t=xxx
const axios = new VAxios({
+ requestOptions: {
+ // 是否加入时间戳
+ joinTime: false,
+ },
+});
+
Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发进程所阻塞。
本项目使用 vite-plugin-mock 来进行 mock 数据处理。项目内 mock 服务分本地和线上。
本地 mock 采用 Node.js 中间件进行参数拦截(不采用 mock.js 的原因是本地开发看不到请求参数和响应结果)。
如果你想添加 mock 数据,只要在根目录下找到 mock 文件,添加对应的接口,对其进行拦截和模拟数据。
在 mock 文件夹内新建文件
TIP
文件新增后会自动更新,不需要手动重启,可以在代码控制台查看日志信息 mock 文件夹内会自动注册,排除以_开头的文件夹及文件
例:
import { MockMethod } from 'vite-plugin-mock';
+import { resultPageSuccess } from '../_util';
+
+const demoList = (() => {
+ const result: any[] = [];
+ for (let index = 0; index < 60; index++) {
+ result.push({
+ id: `${index}`,
+ beginTime: '@datetime',
+ endTime: '@datetime',
+ address: '@city()',
+ name: '@cname()',
+ 'no|100000-10000000': 100000,
+ 'status|1': ['正常', '启用', '停用'],
+ });
+ }
+ return result;
+})();
+
+export default [
+ {
+ url: '/api/table/getDemoList',
+ timeout: 1000,
+ method: 'get',
+ response: ({ query }) => {
+ const { page = 1, pageSize = 20 } = query;
+ return resultPageSuccess(page, pageSize, demoList);
+ },
+ },
+] as MockMethod[];
+
TIP
mock 的值可以直接使用 mockjs 的语法。
{
+ url: string; // mock 接口地址
+ method?: MethodType; // 请求方式
+ timeout?: number; // 延时时间
+ statusCode: number; // 响应状态码
+ response: ((opt: { // 响应结果
+ body: any;
+ query: any;
+ }) => any) | object;
+}
+
GET 接口: ({ query }) => { }
POST 接口: ({ body }) => { }
可在 代码 中查看
TIP
util 只作为服务处理结果数据使用。可以不用,如需使用可自行封装,需要将对应的字段改为接口的返回结构
在 src/api
下面,如果接口匹配到 mock,则会优先使用 mock 进行响应
import { defHttp } from '/@/utils/http/axios';
+import { LoginParams, LoginResultModel } from './model/userModel';
+
+enum Api {
+ Login = '/login',
+}
+
+/**
+ * @description: user login api
+ */
+export function loginApi(params: LoginParams) {
+ return defHttp.request<LoginResultModel>(
+ {
+ url: Api.Login,
+ method: 'POST',
+ params,
+ },
+ {
+ errorMessageMode: 'modal',
+ }
+ );
+}
+// 会匹配到上方的
+export default [
+ {
+ url: '/api/login',
+ timeout: 1000,
+ method: 'POST',
+ response: ({ body }) => {
+ return resultPageSuccess({});
+ },
+ },
+] as MockMethod[];
+
当后台接口已经开发完成,只需要将相应的 mock 函数去掉即可。
以上方接口为例,假如后台接口 login 已经开发完成,则只需要删除/注释掉下方代码即可
export default [
+ {
+ url: '/api/login',
+ timeout: 1000,
+ method: 'POST',
+ response: ({ body }) => {
+ return resultPageSuccess({});
+ },
+ },
+] as MockMethod[];
+
由于该项目是一个展示类项目,线上也是用 mock 数据,所以在打包后同时也集成了 mock。通常项目线上一般为正式接口。
项目线上 mock 采用的是 mockjs 进行 mock 数据模拟。
注意
线上开启 mock 只适用于一些简单的示例网站及预览网站。一定不要在正式的生产环境开启!!!
VITE_USE_MOCK
的值为 trueVITE_USE_MOCK = true;
+
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer';
+
+const modules = import.meta.globEager('./**/*.ts');
+
+const mockModules: any[] = [];
+Object.keys(modules).forEach((key) => {
+ if (key.includes('/_')) {
+ return;
+ }
+ mockModules.push(...modules[key].default);
+});
+
+export function setupProdMockServer() {
+ createProdMockServer(mockModules);
+}
+
import { viteMockServe } from 'vite-plugin-mock';
+
+export function configMockPlugin(isBuild: boolean) {
+ return viteMockServe({
+ injectCode: `
+ import { setupProdMockServer } from '../mock/_createProductionServer';
+
+ setupProdMockServer();
+ `,
+ });
+}
+
为什么通过插件注入代码而不是直接在 main.ts 内插入
在插件内通过 injectCode
插入代码,方便控制 mockjs 是否被打包到最终代码内。如果在 main.ts 内判断,如果关闭了 mock 功能,mockjs 也会打包到构建文件内,这样会增加打包体积。
到这里线上 mock 就配置完成了。线上与本地差异不大,比较大的区别是线上在控制台内看不到接口请求日志。
项目路由配置存放于 src/router/routes 下面。 src/router/routes/modules用于存放路由模块,在该目录下的文件会自动注册。
在 src/router/routes/modules 内的 .ts
文件会被视为一个路由模块。
一个路由模块包含以下结构
import type { AppRouteModule } from '/@/router/types';
+
+import { LAYOUT } from '/@/router/constant';
+import { t } from '/@/hooks/web/useI18n';
+
+const dashboard: AppRouteModule = {
+ path: '/dashboard',
+ name: 'Dashboard',
+ component: LAYOUT,
+ redirect: '/dashboard/analysis',
+ meta: {
+ icon: 'ion:grid-outline',
+ title: t('routes.dashboard.dashboard'),
+ },
+ children: [
+ {
+ path: 'analysis',
+ name: 'Analysis',
+ component: () => import('/@/views/dashboard/analysis/index.vue'),
+ meta: {
+ affix: true,
+ title: t('routes.dashboard.analysis'),
+ },
+ },
+ {
+ path: 'workbench',
+ name: 'Workbench',
+ component: () => import('/@/views/dashboard/workbench/index.vue'),
+ meta: {
+ title: t('routes.dashboard.workbench'),
+ },
+ },
+ ],
+};
+export default dashboard;
+
注意事项
name
不能重复/
,其余子路由都不要以/
开头示例
import type { AppRouteModule } from '/@/router/types';
+import { getParentLayout, LAYOUT } from '/@/router/constant';
+import { t } from '/@/hooks/web/useI18n';
+const permission: AppRouteModule = {
+ path: '/level',
+ name: 'Level',
+ component: LAYOUT,
+ redirect: '/level/menu1/menu1-1/menu1-1-1',
+ meta: {
+ icon: 'ion:menu-outline',
+ title: t('routes.demo.level.level'),
+ },
+
+ children: [
+ {
+ path: 'tabs/:id',
+ name: 'TabsParams',
+ component: getParentLayout('TabsParams'),
+ meta: {
+ carryParam: true,
+ hidePathForChildren: true, // 本级path将会在子级菜单中合成完整path时会忽略这一层级
+ },
+ children: [
+ path: 'tabs/id1', // 其上级有标记hidePathForChildren,所以本级在生成菜单时最终的path为 /level/tabs/id1
+ name: 'TabsParams',
+ component: getParentLayout('TabsParams'),
+ meta: {
+ carryParam: true,
+ ignoreRoute: true, // 本路由仅用于菜单生成,不会在实际的路由表中出现
+ },
+ ]
+ },
+ {
+ path: 'menu1',
+ name: 'Menu1Demo',
+ component: getParentLayout('Menu1Demo'),
+ meta: {
+ title: 'Menu1',
+ },
+ redirect: '/level/menu1/menu1-1/menu1-1-1',
+ children: [
+ {
+ path: 'menu1-1',
+ name: 'Menu11Demo',
+ component: getParentLayout('Menu11Demo'),
+ meta: {
+ title: 'Menu1-1',
+ },
+ redirect: '/level/menu1/menu1-1/menu1-1-1',
+ children: [
+ {
+ path: 'menu1-1-1',
+ name: 'Menu111Demo',
+ component: () => import('/@/views/demo/level/Menu111.vue'),
+ meta: {
+ title: 'Menu111',
+ },
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
+
+export default permission;
+
export interface RouteMeta {
+ // 路由title 一般必填
+ title: string;
+ // 动态路由可打开Tab页数
+ dynamicLevel?: number;
+ // 动态路由的实际Path, 即去除路由的动态部分;
+ realPath?: string;
+ // 是否忽略权限,只在权限模式为Role的时候有效
+ ignoreAuth?: boolean;
+ // 可以访问的角色,只在权限模式为Role的时候有效
+ roles?: RoleEnum[];
+ // 是否忽略KeepAlive缓存
+ ignoreKeepAlive?: boolean;
+ // 是否固定标签
+ affix?: boolean;
+ // 图标,也是菜单图标
+ icon?: string;
+ // 内嵌iframe的地址
+ frameSrc?: string;
+ // 指定该路由切换的动画名
+ transitionName?: string;
+ // 隐藏该路由在面包屑上面的显示
+ hideBreadcrumb?: boolean;
+ // 如果该路由会携带参数,且需要在tab页上面显示。则需要设置为true
+ carryParam?: boolean;
+ // 隐藏所有子菜单
+ hideChildrenInMenu?: boolean;
+ // 当前激活的菜单。用于配置详情页时左侧激活的菜单路径
+ currentActiveMenu?: string;
+ // 当前路由不再标签页显示
+ hideTab?: boolean;
+ // 当前路由不再菜单显示
+ hideMenu?: boolean;
+ // 菜单排序,只对第一级有效
+ orderNo?: number;
+ // 忽略路由。用于在ROUTE_MAPPING以及BACK权限模式下,生成对应的菜单而忽略路由。2.5.3以上版本有效
+ ignoreRoute?: boolean;
+ // 是否在子级菜单的完整path中忽略本级path。2.5.3以上版本有效
+ hidePathForChildren?: boolean;
+}
+
只需要将 frameSrc
设置为需要跳转的地址即可
const IFrame = () => import('/@/views/sys/iframe/FrameBlank.vue');
+{
+ path: 'doc',
+ name: 'Doc',
+ component: IFrame,
+ meta: {
+ frameSrc: 'https://vvbin.cn/doc-next/',
+ title: t('routes.demo.iframe.doc'),
+ },
+},
+
只需要将 path
设置为需要跳转的HTTP 地址即可
{
+ path: 'https://vvbin.cn/doc-next/',
+ name: 'DocExternal',
+ component: IFrame,
+ meta: {
+ title: t('routes.demo.iframe.docExternal'),
+ },
+}
+
若需要开启该功能,需要在动态路由的meta
中设置如下两个参数:
dynamicLevel
最大能打开的Tab标签页数realPath
动态路由实际路径(考虑到动态路由有时候可能存在N层的情况, 例:/:id/:subId/:...
), 为了减少计算开销, 使用配置方式事先规定好路由的实际路径(注意: 该参数若不设置,将无法使用该功能){
+ path: 'detail/:id',
+ name: 'TabDetail',
+ component: () => import('/@/views/demo/feat/tabs/TabDetail.vue'),
+ meta: {
+ currentActiveMenu: '/feat/tabs',
+ title: t('routes.demo.feat.tabDetail'),
+ hideMenu: true,
+ dynamicLevel: 3,
+ realPath: '/feat/tabs/detail',
+ },
+}
+
这里的 icon
配置,会同步到 菜单(icon 的值可以查看此处)。
示例,新增 test.ts 文件
import type { AppRouteModule } from '/@/router/types';
+import { LAYOUT } from '/@/router/constant';
+import { t } from '/@/hooks/web/useI18n';
+
+const dashboard: AppRouteModule = {
+ path: '/about',
+ name: 'About',
+ component: LAYOUT,
+ redirect: '/about/index',
+ meta: {
+ icon: 'simple-icons:about-dot-me',
+ title: t('routes.dashboard.about'),
+ },
+ children: [
+ {
+ path: 'index',
+ name: 'AboutPage',
+ component: () => import('/@/views/sys/about/index.vue'),
+ meta: {
+ title: t('routes.dashboard.about'),
+ icon: 'simple-icons:about-dot-me',
+ },
+ },
+ ],
+};
+
+export default dashboard;
+
此时路由已添加完成,不需要手动引入,放在src/router/routes/modules 内的文件会自动被加载。
访问 ip:端口/about/index 出现对应组件内容即代表成功
项目中采用的是重定向方式
import { useRedo } from '/@/hooks/web/usePage';
+import { defineComponent } from 'vue';
+export default defineComponent({
+ setup() {
+ const redo = useRedo();
+ // 执行刷新
+ redo();
+ return {};
+ },
+});
+
src/views/sys/redirect/index.vue
import { defineComponent, unref } from 'vue';
+import { useRouter } from 'vue-router';
+export default defineComponent({
+ name: 'Redirect',
+ setup() {
+ const { currentRoute, replace } = useRouter();
+ const { params, query } = unref(currentRoute);
+ const { path } = params;
+ const _path = Array.isArray(path) ? path.join('/') : path;
+ replace({
+ path: '/' + _path,
+ query,
+ });
+ return {};
+ },
+});
+
页面跳转建议采用项目提供的 useGo
import { useGo } from '/@/hooks/web/usePage';
+import { defineComponent } from 'vue';
+export default defineComponent({
+ setup() {
+ const go = useGo();
+
+ // 执行刷新
+ go();
+ go(PageEnum.Home);
+ return {};
+ },
+});
+
标签页使用的是 keep-alive
和 router-view
实现,实现切换 tab 后还能保存切换之前的状态。
开启缓存有 3 个条件
openKeepAlive
设置为 true
name
,且不能重复name
,与路由设置的 name
保持一致 {
+ ...,
+ // name
+ name: 'Login',
+ // 对应组件组件的name
+ component: () => import('/@/views/sys/login/index.vue'),
+ ...
+ },
+
+ // /@/views/sys/login/index.vue
+ export default defineComponent({
+ // 需要和路由的name一致
+ name:"Login"
+ });
+
注意
keep-alive 生效的前提是:需要将路由的 name
属性及对应的页面的 name
设置成一样。因为:
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
可在 router.meta 下配置
可以将 ignoreKeepAlive
配置成 true
即可关闭缓存。
export interface RouteMeta {
+ // 是否忽略KeepAlive缓存
+ ignoreKeepAlive?: boolean;
+}
+
首页路由指的是应用程序中的默认路由,当不输入其他任何路由时,会自动重定向到该路由下,并且该路由在Tab上是固定的,即使设置affix: false
也不允许关闭
例:首页路由配置的是/dashboard/analysis
,那么当直接访问 http://localhost:3100/
会自动跳转到http://localhost:3100/#/dashboard/analysis
上(用户已登录的情况下)
可以将pageEnum.ts
中的BASE_HOME
更改为需要你想设置的首页即可
export enum PageEnum {
+ // basic home path
+ // 更改此处即可
+ BASE_HOME = '/dashboard',
+}
+
+
用于修改项目的配色、布局、缓存、多语言、组件默认配置
项目的环境变量配置位于项目根目录下的 .env、.env.development、.env.production
具体可以参考 Vite 文档
.env # 在所有的环境中被载入
+.env.local # 在所有的环境中被载入,但会被 git 忽略
+.env.[mode] # 只在指定的模式中被载入
+.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略
+
+
温馨提醒
VITE_
开头的变量会被嵌入到客户端侧的包中,你可以在项目代码中这样访问它们:console.log(import.meta.env.VITE_PROT);
+
VITE_GLOB_*
开头的的变量,在打包的时候,会被加入_app.config.js配置文件当中.所有环境适用
# 端口号
+VITE_PORT=3100
+# 网站标题
+VITE_GLOB_APP_TITLE=vben admin
+# 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符
+VITE_GLOB_APP_SHORT_NAME=vben_admin
+
开发环境适用
# 是否开启mock数据,关闭时需要自行对接后台接口
+VITE_USE_MOCK=true
+# 资源公共路径,需要以 /开头和结尾
+VITE_PUBLIC_PATH=/
+# 是否删除Console.log
+VITE_DROP_CONSOLE=false
+# 本地开发代理,可以解决跨域及多地址代理
+# 如果接口地址匹配到,则会转发到http://localhost:3000,防止本地出现跨域问题
+# 可以有多个,注意多个不能换行,否则代理将会失效
+VITE_PROXY=[["/api","http://localhost:3000"],["api1","http://localhost:3001"],["/upload","http://localhost:3001/upload"]]
+
+::: tip
+v3.0.0开始,作者重构了vite.config.ts,新版本不再支持VITE_PROXY环境变量。
+:::
+
+# 接口地址
+# 如果没有跨域问题,直接在这里配置即可
+VITE_GLOB_API_URL=/api
+# 文件上传接口 可选
+VITE_GLOB_UPLOAD_URL=/upload
+# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换
+VITE_GLOB_API_URL_PREFIX=
+
注意
这里配置的 VITE_PROXY
以及 VITE_GLOB_API_URL
, /api 需要是唯一的,不要和接口有的名字冲突
如果你的接口是 http://localhost:3000/api
之类的,请考虑将 VITE_GLOB_API_URL=/xxxx
换成别的名字
生产环境适用
# 是否开启mock
+VITE_USE_MOCK=true
+# 接口地址 可以由nginx做转发或者直接写实际地址
+VITE_GLOB_API_URL=/api
+# 文件上传地址 可以由nginx做转发或者直接写实际地址
+VITE_GLOB_UPLOAD_URL=/upload
+# 接口地址前缀,有些系统所有接口地址都有前缀,可以在这里统一加,方便切换
+VITE_GLOB_API_URL_PREFIX=
+# 是否删除Console.log
+VITE_DROP_CONSOLE=true
+# 资源公共路径,需要以 / 开头和结尾
+VITE_PUBLIC_PATH=/
+# 打包是否输出gz|br文件
+# 可选: gzip | brotli | none
+# 也可以有多个, 例如 ‘gzip’|'brotli',这样会同时生成 .gz和.br文件
+VITE_BUILD_COMPRESS = 'gzip'
+# 打包是否压缩图片
+VITE_USE_IMAGEMIN = false
+# 打包是否开启pwa功能
+VITE_USE_PWA = false
+# 是否兼容旧版浏览器。开启后打包时间会慢一倍左右。会多打出旧浏览器兼容包,且会根据浏览器兼容性自动使用相应的版本
+VITE_LEGACY = false
+
当执行yarn build
构建项目之后,会自动生成 _app.config.js
文件并插入 index.html
。
注意: 开发环境不会生成
// _app.config.js
+// 变量名命名规则 __PRODUCTION__xxx_CONF__ xxx:为.env配置的VITE_GLOB_APP_SHORT_NAME
+window.__PRODUCTION__VUE_VBEN_ADMIN__CONF__ = {
+ VITE_GLOB_APP_TITLE: 'vben admin',
+ VITE_GLOB_APP_SHORT_NAME: 'vue_vben_admin',
+ VITE_GLOB_API_URL: '/app',
+ VITE_GLOB_API_URL_PREFIX: '/',
+ VITE_GLOB_UPLOAD_URL: '/upload',
+};
+
_app.config.js
用于项目在打包后,需要动态修改配置的需求,如接口地址。不用重新进行打包,可在打包后修改 /dist/_app.config.js
内的变量,刷新即可更新代码内的局部变量。
想要获取 _app.config.js
内的变量,可以使用 src/hooks/setting/index.ts 提供的函数来进行获取
首先在 .env
或者对应的开发环境配置文件内,新增需要可动态配置的变量,需要以 VITE_GLOB_
开头
VITE_GLOB_
开头的变量会自动加入环境变量,通过在 types/config.d.ts
内修改 GlobEnvConfig
和 GlobConfig
两个环境变量的值来定义新添加的类型
useGlobSetting 函数中添加刚新增的返回值即可
const {
+ VITE_GLOB_APP_TITLE,
+ VITE_GLOB_API_URL,
+ VITE_GLOB_APP_SHORT_NAME,
+ VITE_GLOB_API_URL_PREFIX,
+ VITE_GLOB_UPLOAD_URL,
+} = ENV;
+
+export const useGlobSetting = (): SettingWrap => {
+ // Take global configuration
+ const glob: Readonly<GlobConfig> = {
+ title: VITE_GLOB_APP_TITLE,
+ apiUrl: VITE_GLOB_API_URL,
+ shortName: VITE_GLOB_APP_SHORT_NAME,
+ urlPrefix: VITE_GLOB_API_URL_PREFIX,
+ uploadUrl: VITE_GLOB_UPLOAD_URL
+ };
+ return glob as Readonly<GlobConfig>;
+};
+
+
WARNING
项目配置文件用于配置项目内展示的内容、布局、文本等效果,存于localStorage
中。如果更改了项目配置,需要手动清空 localStorage
缓存,刷新重新登录后方可生效。
src/settings/projectSetting.ts
// ! 改动后需要清空浏览器缓存
+const setting: ProjectConfig = {
+ // 是否显示SettingButton
+ showSettingButton: true,
+
+ // 是否显示主题切换按钮
+ showDarkModeToggle: true,
+
+ // 设置按钮位置 可选项
+ // SettingButtonPositionEnum.AUTO: 自动选择
+ // SettingButtonPositionEnum.HEADER: 位于头部
+ // SettingButtonPositionEnum.FIXED: 固定在右侧
+ settingButtonPosition: SettingButtonPositionEnum.AUTO,
+
+ // 权限模式,默认前端角色权限模式
+ // ROUTE_MAPPING: 前端模式(菜单由路由生成,默认)
+ // ROLE:前端模式(菜单路由分开)
+ permissionMode: PermissionModeEnum.ROUTE_MAPPING,
+ // 权限缓存存放位置。默认存放于localStorage
+ permissionCacheType: CacheTypeEnum.LOCAL,
+ // 会话超时处理方案
+ // SessionTimeoutProcessingEnum.ROUTE_JUMP: 路由跳转到登录页
+ // SessionTimeoutProcessingEnum.PAGE_COVERAGE: 生成登录弹窗,覆盖当前页面
+ sessionTimeoutProcessing: SessionTimeoutProcessingEnum.ROUTE_JUMP,
+ // 项目主题色
+ themeColor: primaryColor,
+ // 网站灰色模式,用于可能悼念的日期开启
+ grayMode: false,
+ // 色弱模式
+ colorWeak: false,
+ // 是否取消菜单,顶部,多标签页显示, 用于可能内嵌在别的系统内
+ fullContent: false,
+ // 主题内容宽度
+ contentMode: ContentEnum.FULL,
+ // 是否显示logo
+ showLogo: true,
+ // 是否显示底部信息 copyright
+ showFooter: true,
+ // 头部配置
+ headerSetting: {
+ // 背景色
+ bgColor: '#ffffff',
+ // 固定头部
+ fixed: true,
+ // 是否显示顶部
+ show: true,
+ // 主题
+ theme: MenuThemeEnum.LIGHT,
+ // 开启锁屏功能
+ useLockPage: true,
+ // 显示全屏按钮
+ showFullScreen: true,
+ // 显示文档按钮
+ showDoc: true,
+ // 显示消息中心按钮
+ showNotice: true,
+ // 显示菜单搜索按钮
+ showSearch: true,
+ },
+ // 菜单配置
+ menuSetting: {
+ // 背景色
+ bgColor: '#273352',
+ // 是否固定住菜单
+ fixed: true,
+ // 菜单折叠
+ collapsed: false,
+ // 折叠菜单时候是否显示菜单名
+ collapsedShowTitle: false,
+ // 是否可拖拽
+ canDrag: true,
+ // 是否显示
+ show: true,
+ // 菜单宽度
+ menuWidth: 180,
+ // 菜单模式
+ mode: MenuModeEnum.INLINE,
+ // 菜单类型
+ type: MenuTypeEnum.SIDEBAR,
+ // 菜单主题
+ theme: MenuThemeEnum.DARK,
+ // 分割菜单
+ split: false,
+ // 顶部菜单布局
+ topMenuAlign: 'start',
+ // 折叠触发器的位置
+ trigger: TriggerEnum.HEADER,
+ // 手风琴模式,只展示一个菜单
+ accordion: true,
+ // 在路由切换的时候关闭左侧混合菜单展开菜单
+ closeMixSidebarOnChange: false,
+ // 左侧混合菜单模块切换触发方式
+ mixSideTrigger: MixSidebarTriggerEnum.CLICK,
+ // 是否固定左侧混合菜单
+ mixSideFixed: false,
+ },
+ // 多标签
+ multiTabsSetting: {
+ // 刷新后是否保留已经打开的标签页
+ cache: false,
+ // 开启
+ show: true,
+ // 开启快速操作
+ showQuick: true,
+ // 是否可以拖拽
+ canDrag: true,
+ // 是否显示刷新按钮
+ showRedo: true,
+ // 是否显示折叠按钮
+ showFold: true,
+ },
+
+ // 动画配置
+ transitionSetting: {
+ // 是否开启切换动画
+ enable: true,
+ // 动画名
+ basicTransition: RouterTransitionEnum.FADE_SIDE,
+ // 是否打开页面切换loading
+ openPageLoading: true,
+ // 是否打开页面切换顶部进度条
+ openNProgress: false,
+ },
+
+ // 是否开启KeepAlive缓存 开发时候最好关闭,不然每次都需要清除缓存
+ openKeepAlive: true,
+ // 自动锁屏时间,为0不锁屏。 单位分钟 默认1个小时
+ lockTime: 0,
+ // 显示面包屑
+ showBreadCrumb: true,
+ // 显示面包屑图标
+ showBreadCrumbIcon: false,
+ // 是否使用全局错误捕获
+ useErrorHandle: false,
+ // 是否开启回到顶部
+ useOpenBackTop: true,
+ // 是否可以嵌入iframe页面
+ canEmbedIFramePage: true,
+ // 切换界面的时候是否删除未关闭的message及notify
+ closeMessageOnSwitch: true,
+ // 切换界面的时候是否取消已经发送但是未响应的http请求。
+ // 如果开启,想对单独接口覆盖。可以在单独接口设置
+ removeAllHttpPending: true,
+};
+
用于配置缓存内容加密信息,对缓存到浏览器的信息进行 AES 加密
在 /@/settings/encryptionSetting.ts 内可以配置 localStorage
及 sessionStorage
缓存信息
前提: 使用项目自带的缓存工具类 /@/utils/cache 来进行缓存操作
import { isDevMode } from '/@/utils/env';
+
+// 缓存默认过期时间
+export const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7;
+
+// 开启缓存加密后,加密密钥。采用aes加密
+export const cacheCipher = {
+ key: '_11111000001111@',
+ iv: '@11111000001111_',
+};
+
+// 是否加密缓存,默认生产环境加密
+export const enableStorageEncryption = !isDevMode();
+
用于配置多语言信息
在 src/settings/localeSetting.ts 内配置
export const LOCALE: { [key: string]: LocaleType } = {
+ ZH_CN: 'zh_CN',
+ EN_US: 'en',
+};
+
+export const localeSetting: LocaleSetting = {
+ // 是否显示语言选择器
+ showPicker: true,
+ // 当前语言
+ locale: LOCALE.ZH_CN,
+ // 默认语言
+ fallback: LOCALE.ZH_CN,
+ // 允许的语言
+ availableLocales: [LOCALE.ZH_CN, LOCALE.EN_US],
+};
+
+// 语言列表
+export const localeList: DropMenu[] = [
+ {
+ text: '简体中文',
+ event: LOCALE.ZH_CN,
+ },
+ {
+ text: 'English',
+ event: LOCALE.EN_US,
+ },
+];
+
默认全局主题色配置位于 build/config/glob/themeConfig.ts 内
只需要修改 primaryColor 为您需要的配色,然后重新执行 yarn serve
即可
/**
+ * less global variable
+ */
+export const primaryColor = '#0960bd';
+
用于修改项目内组件 class 的统一前缀
export const prefixCls = 'vben';
+
@namespace: vben;
+
在 css 内
<style lang="less" scoped>
+ /* namespace已经全局注入,不需要额外再引入 */
+ @prefix-cls: ~'@{namespace}-app-logo';
+
+ .@{prefix-cls} {
+ width: 100%;
+ }
+</style>
+
在 vue/ts 内
import { useDesign } from '/@/hooks/web/useDesign';
+
+const { prefixCls } = useDesign('app-logo');
+
+// prefixCls => vben-app-logo
+
用于预设一些颜色数组
在 src/settings/designSetting.ts 内配置
// app主题色预设
+export const APP_PRESET_COLOR_LIST: string[] = [
+ '#0960bd',
+ '#0084f4',
+ '#009688',
+ '#536dfe',
+ '#ff5c93',
+ '#ee4f12',
+ '#0096c7',
+ '#9c27b0',
+ '#ff9800',
+];
+
+// 顶部背景色预设
+export const HEADER_PRESET_BG_COLOR_LIST: string[] = [
+ '#ffffff',
+ '#009688',
+ '#5172DC',
+ '#1E9FFF',
+ '#018ffb',
+ '#409eff',
+ '#4e73df',
+ '#e74c3c',
+ '#24292e',
+ '#394664',
+ '#001529',
+ '#383f45',
+];
+
+// 左侧菜单背景色预设
+export const SIDE_BAR_BG_COLOR_LIST: string[] = [
+ '#001529',
+ '#273352',
+ '#ffffff',
+ '#191b24',
+ '#191a23',
+ '#304156',
+ '#001628',
+ '#28333E',
+ '#344058',
+ '#383f45',
+];
+
在 src/settings/componentSetting.ts 内配置
// 用于配置某些组件的常规配置,而无需修改组件
+import type { SorterResult } from '../components/Table';
+
+export default {
+ // 表格配置
+ table: {
+ // 表格接口请求通用配置,可在组件prop覆盖
+ // 支持 xxx.xxx.xxx格式
+ fetchSetting: {
+ // 传给后台的当前页字段
+ pageField: 'page',
+ // 传给后台的每页显示多少条的字段
+ sizeField: 'pageSize',
+ // 接口返回表格数据的字段
+ listField: 'items',
+ // 接口返回表格总数的字段
+ totalField: 'total',
+ },
+ // 可选的分页选项
+ pageSizeOptions: ['10', '50', '80', '100'],
+ //默认每页显示多少条
+ defaultPageSize: 10,
+ // 默认排序方法
+ defaultSortFn: (sortInfo: SorterResult) => {
+ const { field, order } = sortInfo;
+ return {
+ // 排序字段
+ field,
+ // 排序方式 asc/desc
+ order,
+ };
+ },
+ // 自定义过滤方法
+ defaultFilterFn: (data: Partial<Recordable<string[]>>) => {
+ return data;
+ },
+ },
+ // 滚动组件配置
+ scrollbar: {
+ // 是否使用原生滚动样式
+ // 开启后,菜单,弹窗,抽屉会使用原生滚动条组件
+ native: false,
+ },
+};
+
该分类主要说明一些地方为什么这样做,以及原因是什么
/@/
是 vite
内配置的别名
/@/settings
等同于 src/settings
为什么是/@/
因为项目是从 vite1.0
过渡过来的,vite1.0
只能以 /
开头,所以有一部分从 webpack
用户转过来的可能不习惯。
在 main.ts 内可以看到,本地开发会全量引入 antd.less,vite-plugin-style-import 在本地是没有作用的。
这样做的原因主要是加快本地开发刷新速度。如果在本地开发中也按需按需引入,则在浏览器控制台内可以看到,平均一个页面大概增加了 100 次 http 请求。如果全量引入,只增加了一个请求,所以为了减少请求数量,才这样种。
// src/main.ts
+if (import.meta.env.DEV) {
+ import('ant-design-vue/dist/antd.less');
+}
+
+// build/vite/plugin/styleImport
+import styleImport from 'vite-plugin-style-import';
+export function configStyleImportPlugin(isBuild: boolean) {
+ if (!isBuild) return [];
+ const styleImportPlugin = styleImport({
+ libs: [
+ {
+ libraryName: 'ant-design-vue',
+ esModule: true,
+ resolveStyle: (name) => {
+ return `ant-design-vue/es/${name}/style/index`;
+ },
+ },
+ ],
+ });
+ return styleImportPlugin;
+}
+
在 src/utils/dataUtil
内,使用的是 moment,其次在页面中对时间的操作也是使用 dateUtil,而不是直接 import moment from 'moment'
。
这样做主要是方便后续切换到 dayjs
,因为 api 一样,所以在后续切换中,只需更改 dataUtil 内的 import 即可,而不用全部更改。
TIP
列举了一些常见的问题。有问题可以先来这里寻找,如果没有可以在 issue 提。
遇到问题,可以先从以下几个方面查找
vben-admin 的项目配置默认是缓存在 localStorage
内,所以版本更新后可能有些配置没改变。
解决方式是每次更新代码的时候修改 package.json
内的 version
版本号. 因为 localStorage 的 key 是根据版本号来的。所以更新后版本不同前面的配置会失效。重新登录即可
VUE_VBEN_ADMIN__DEVELOPMENT__2.0.3__COMMON__LOCAL__KEY__
key 的组成是 [项目名]+[开发环境]+[版本号]+[key]
当修改 .env
等环境文件及 vite.config.ts
文件时,vite 会自动重启服务。
自动重启有几率出现问题,请重新运行项目即可解决.
如果将 build.minify 设置为 'esbuild',且不能启用 LEGACY,否则打包将会报错,两者选其一即可打包。
在控制台看到以下警告的原因是 ant-design-vue
会检测是否使用了 babel-plugin-import
来判断是否进行了组件库的按需引入。
但是项目使用的是 vite 的插件 vite-plugin-style-import 来进行按需引入。在 vite 内没必要使用 babel 在转换一次代码了。
所以想关闭这个警告,得等 ant-design-vue 提供可以关闭该警告的配置。
You are using a whole package of antd, please use https://www.npmjs.com/package/babel-plugin-import to reduce app bundle size. Not support Vite !!!
+
菜单必须和路由匹配才会显示在界面上,所以得确保菜单和对应的路由存在即可显示.
由于 vite 在本地没有转换代码,且代码中用到了可选链等比较新的语法。所以本地开发需要使用版本较高的浏览器(Chrome 85+
)进行开发
这是由于开启了路由切换动画,且对应的页面组件存在多个根节点导致的,在页面最外层添加<div></div>
即可
错误示例
<template>
+ <!-- 注释也算一个节点 -->
+ <h1>text h1</h1>
+ <h2>text h2</h2>
+</template>
+
正确示例
<template>
+ <div>
+ <h1>text h1</h1>
+ <h2>text h2</h2>
+ </div>
+</template>
+
提示
目前在 vite+vue3.0.5 版本中,如果组件命名携带关键字,则可能会导致内存溢出。例如 ImportExcel
excel 导入组件。
目前发现这个原因可能有以下,可以从以下原因来排查,如果还有别的可能,可以提交 pr 来告诉我
import { getCurrentInstance } from 'vue';
+getCurrentInstance().ctx.xxxx;
+
目前在 safari 上面本地开发运行样式会有问题,还未找到原因,有知道的也可以告诉我。
如果出现依赖安装报错,启动报错等。先检查电脑环境有没有安装齐全。
12.0.0
不支持 13
, 推荐 14 版本。yarn.lock
和 node_modules
,然后重新运行 yarn install
.npmrc
文件,内容如下# .npmrc
+registry = https://registry.npm.taobao.org
+
然后重新执行yarn run reinstall
等待安装完成即可
如果你使用了该项目进行项目开发。开发之中想同步最新的代码。你可以设置多个源的方式
git clone https://github.com/vbenjs/vben-admin-thin-next.git
+
# up 为源名称,可以随意设置
+# gitUrl为开源最新代码
+git remote add up gitUrl;
+
# 提交代码到自己公司
+# main为分支名 需要自行根据情况修改
+git push up main
+
+# 同步公司的代码
+# main为分支名 需要自行根据情况修改
+git pull up main
+
git pull origin main
+
TIP
同步代码的时候会出现冲突。只需要把冲突解决即可
首先,完整版由于引用了比较多的库文件,所以打包会比较大。可以使用精简版来进行开发
其次建议开启 gzip,使用之后体积会只有原先 1/3 左右。
gzip 可以由服务器直接开启。如果是这样,前端不需要构建 .gz
格式的文件
如果前端构建了 .gz
文件,以 nginx 为例,nginx 需要开启 gzip_static: on
这个选项。
brotli
,比 gzip 更好的压缩。两者可以共存注意
gzip_static: 这个模块需要 nginx 另外安装,默认的 nginx 没有安装这个模块。
开启 brotli
也需要 nginx 另外安装模块
如果出现类似以下错误,请检查项目全路径(包含所有父级路径)不能出现中文、日文、韩文。否则将会出现路径访问 404 导致以下问题
[vite] Failed to resolve module import "ant-design-vue/dist/antd.css-vben-adminode_modulesant-design-vuedistantd.css". (imported by /@/setup/ant-design-vue/index.ts)
+
很多人问为什么不用dayjs
。在项目依赖中可以看到,它是 Ant-Design-Vue 内部自带的。
目前还没有基于 Vite 的 dayjs 替换 momentjs 方案,webpack 已经有了。等以后出现了在进行替换。
如果看到控制台有如下警告,且页面能正常打开 可以忽略该警告。
后续 vue-router
可能会提供配置项来关闭警告
2.6.1 及以上版本已移除此警告
[Vue Router warn]: No match found for location with path "xxxx"
+
当出现以下错误信息时,请检查你的 nodejs 版本号是否符合要求
TypeError: str.matchAll is not a function
+at Object.extractor (vue-vben-admin-main\node_modules@purge-icons\core\dist\index.js:146:27)
+at Extract (vue-vben-admin-main\node_modules@purge-icons\core\dist\index.js:173:54)
+
+
当页面出现以下报错,是因为 /xxx 对应的路由组件内部出现了错误。
Uncaught (in promise) Error: Couldn't resolve component "default" at "/xxx"
+
+
可以尝试从以下几点排查
// 正确的
+import { cloneDeep } from 'lodash-es';
+
+// 报错
+import _ from 'lodash-es';
+
这样就不会是使用的取值忘记 xxx.value 来进行数据获取
参考跨域问题
proxy 代理不成功,没有代理到实际地址?
代理只是服务请求代理,这个地址是不会变的。 原理可以简单的理解为,在本地启了一个服务,你先请求了本地的服务,本地的服务转发了你的请求到实际服务器。所以你在浏览器上看到的请求地址还是 http://localhost:8000/xxx
。以服务端是否收到请求为准。
跟组件库相关的问题可以查看常见问题
菜单数据的值被存放在 store/modules/permission
store 中, 你可以在这里进行修改
你可以在 store/modules/permission
下, 修改 routeFilter
方法来进行更灵活的菜单路由权限控制
const routeFilter = (route: AppRouteRecordRaw) => {
+ const { meta } = route;
+ // 抽出角色
+ const { roles } = meta || {};
+
+ // 添加你的自定义逻辑来过滤路由和菜单
+ if (xxx) {
+ return false;
+ }
+
+ if (!roles) return true;
+ // 进行角色权限判断
+ return roleList.some((role) => roles.includes(role));
+ };
+
+
对接vben的项目地址,非官方项目,vben用户开源,开源协议请自行查看
在项目 /test/server
内有简单的 Node.js 测试后台接口服务,用 Koa2 实现
+cd ./test/server
+
+# 安装依赖
+yarn
+
+# 运行服务
+yarn start
+
+
服务运行成功之后,就可以访问测试上传接口及 websocket 接口服务