vue-element-plus-admin-doc
如何本地开发
# 克隆本仓库
+$ git clone https://github.com/kailong321200875/vue-element-plus-admin-doc.git
+
+# 安装依赖
+$ yarn
+
+# 启动开发服务器
+$ npm run 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..a4aea535 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +element-plus-admin-doc.cn diff --git a/README.html b/README.html new file mode 100644 index 00000000..207908e3 --- /dev/null +++ b/README.html @@ -0,0 +1,39 @@ + + +
+ + +# 克隆本仓库
+$ git clone https://github.com/kailong321200875/vue-element-plus-admin-doc.git
+
+# 安装依赖
+$ yarn
+
+# 启动开发服务器
+$ npm run dev
+
# 克隆本仓库\n$ git clone https://github.com/kailong321200875/vue-element-plus-admin-doc.git\n\n# 安装依赖\n$ yarn\n\n# 启动开发服务器\n$ npm run dev\n
展示多个头像集合
Avatars 组件位于 src/components/Avatars 内
<script lang="ts" setup>\nimport { Avatars } from '@/components/Avatars'\n\nconst data = ref<AvatarItem[]>([\n {\n name: 'Lily',\n url: 'https://avatars.githubusercontent.com/u/3459374?v=4'\n },\n {\n name: 'Amanda',\n url: 'https://avatars.githubusercontent.com/u/3459375?v=4'\n },\n {\n name: 'Daisy',\n url: 'https://avatars.githubusercontent.com/u/3459376?v=4'\n },\n {\n name: 'Olivia',\n url: 'https://avatars.githubusercontent.com/u/3459377?v=4'\n },\n {\n name: 'Tina',\n url: 'https://avatars.githubusercontent.com/u/3459378?v=4'\n },\n {\n name: 'Kitty',\n url: 'https://avatars.githubusercontent.com/u/3459323?v=4'\n },\n {\n name: 'Helen',\n url: 'https://avatars.githubusercontent.com/u/3459324?v=4'\n },\n {\n name: 'Sophia',\n url: 'https://avatars.githubusercontent.com/u/3459325?v=4'\n },\n {\n name: 'Wendy',\n url: 'https://avatars.githubusercontent.com/u/3459326?v=4'\n }\n])\n</script>\n\n<template>\n <Avatars :data="data" />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
size | 头像尺寸 | ComponentSize、number | - | - |
max | 最大展示个数 | number | - | 5 |
data | 头像数据,详见 | AvatarItem[] | - | - |
showTooltip | 是否展示名称tip | boolean | - | true |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 头像图片地址 | string | - | - |
name | 名称,非必填 | string | - | - |
二次封装 ElButton
,支持修改主题色
BaseButton 组件位于 src/components/Button 内
BaseButton 已经全局引入,无需在手动引入
<template>\n <BaseButton type="primary"> Add </BaseButton>\n</template>\n\n
支持 ElButton
的所有属性
1.2.4
新增
用于展示详情,自带返回按钮。
ContentDetailWrap 组件位于 src/components/ContentDetailWrap 内
<script setup lang="ts">\nimport { ContentDetailWrap } from '@/components/ContentDetailWrap'\n</script>\n\n<template>\n <ContentDetailWrap title="详情" @back="push('/example/example-page')">\n Details\n </ContentDetailWrap>\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
title | 标题 | string | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
back | 返回事件 | - |
插槽名 | 说明 | 子标签 |
---|---|---|
- | 默认展示内容 | - |
title | 自定义标题内容 | - |
right | 自定义右侧内容 | - |
基于 vue-count-to
改造
CountTo 组件位于 src/components/CountTo 内
更复杂点的例子,请在线预览
<script setup lang="ts">\nimport { CountTo } from '@/components/CountTo'\n</script>\n\n<template>\n <CountTo :start-val="0" :end-val="35225" />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
startVal | 初始值 | number | - | 0 |
endVal | 最后展示的值 | number | - | 2021 |
duration | 动画时间 | number | - | 3000 |
autoplay | 是否自动播放 | boolean | - | true |
decimals | 小位数 | number | - | 0 |
decimal | 小位数分割符号 | string | - | . |
separator | 分割符号 | string | - | , |
prefix | 前缀 | string | - | - |
suffix | 后缀 | string | - | - |
useEasing | 过渡动画 | boolean | - | true |
easingFn | 自定义动画效果 | (t: number, b: number, c: number, d: number) => number | - | - |
注意
从 v2.5.3之后,Descriptions 组件不再基于 element-plus
的 Descriptions
进行二次封装,所以可能有的属性无法使用,具体可以自行修改或者改造,或者可以提issue。
对 element-plus
的 Descriptions
组件进行封装。
Descriptions 组件位于 src/components/Descriptions 内
注意
推荐使用 tsx
来使用 Descriptions
组件
更复杂点的例子,请在线预览
<script setup lang="tsx">\nimport { Descriptions, DescriptionsSchema } from '@/components/Descriptions'\nimport { reactive } from 'vue'\n\nconst data = reactive({\n username: 'chenkl',\n nickName: '梦似花落。',\n age: 26,\n phone: '13655971xxxx',\n email: '502431556@qq.com',\n addr: '这是一个很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的地址',\n sex: '男',\n certy: '3505831994xxxxxxxx'\n})\n\nconst schema = reactive<DescriptionsSchema[]>([\n {\n field: 'username',\n label: 'username'\n },\n {\n field: 'nickName',\n label: 'nickName'\n },\n {\n field: 'phone',\n label: 'phone'\n },\n {\n field: 'email',\n label: 'email'\n },\n {\n field: 'addr',\n label: 'addr',\n span: 24\n }\n])\n</script>\n\n<template>\n <Descriptions\n title="descriptions"\n message="message"\n :data="data"\n :schema="schema"\n />\n</template>\n\n
除以下参数外,还支持 element-plus
的 Descriptions
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
title | 标题 | string | - | - |
message | 提示 | string | - | - |
collapse | 是否显示展开按钮 | boolean | - | true |
schema | 布局结构数据,详见 | DescriptionsSchema[] | - | [] |
data | 展示的数据 | Recordable | - | {} |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
span | 栅格占比 | number | - | - |
field | 字段名,唯一值,需要与 data 中的属性名对应 | string | - | - |
label | 列表标题 | string | - | - |
width | 列表宽度 | string /number | - | - |
minWidth | 列表最小宽度 | string /number | - | - |
align | 内容对齐方式 | string | left/center/right | left |
labelAlign | 标题对齐方式 | string | left/center/right | left |
className | 自定义内容标签类名 | string | - | - |
labelClassName | 自定义标题标签类名 | string | - | - |
slots | 插槽对象 | object | - | - |
对 element-plus
的 Dialog
组件进行封装。
Dialog 组件位于 src/components/Dialog 内
<script setup lang="ts">\nimport { Dialog } from '@/components/Dialog'\nimport { ElButton } from 'element-plus'\nimport { ref } from 'vue'\n\nconst dialogVisible = ref(false)\n</script>\n\n<template>\n <ElButton type="primary" @click="dialogVisible = !dialogVisible">\n open\n </ElButton>\n <Dialog v-model="dialogVisible" title="dialog">\n <div v-for="v in 10000" :key="v">{{ v }}</div>\n <template #footer>\n <el-button @click="dialogVisible = false">close</el-button>\n </template>\n </Dialog>\n</template>\n\n
除以下参数外,还支持 element-plus
的 Dialog
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
modelValue | 是否显示弹窗,支持v-model | boolean | - | false |
fullscreen | 是否显示全屏按钮 | boolean | - | true |
title | 弹窗标题 | string | - | Dialog |
maxHeight | 弹窗内容最大高度 | string /number | - | 500px |
插槽名 | 说明 | 子标签 |
---|---|---|
- | 弹窗内容 | - |
title | 弹窗标题内容 | - |
footer | 弹窗底部内容 | - |
对 echarts
进行封装,自适应窗口大小。
Echart 组件位于 src/components/Echart 内
只需传入对应的 options
和 height
即可展示图表。
<template>\n <Echart :options="pieOptions" :height="300" />\n</template>\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
options | echart 对应的配置项,详见 | EChartsOption | - | [] |
width | 图表宽度 | string /number | - | - |
height | 图表高度 | string /number | - | 500 |
基于 wangeditor 封装。
目前项目中的 editor
只是做了简单的封装,需要开发者根据实际情况,自行配置 editorConfig
属性,如,上传图片功能。
可自行阅读 wangeditor文档
Editor 组件位于 src/components/Editor 内
<script setup lang="ts">\nimport { Editor } from '@/components/Editor'\nimport { ref} from 'vue'\n\nconst defaultHtml = ref('<p>hello <strong>world</strong></p>')\n\nconst change = (html: string) => {\n console.log(html)\n}\n</script>\n\n<template>\n <Editor v-model="defaultHtml" ref="editorRef" @change="change" />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
editorId | 富文本组件唯一值,必填项 | string | - | wangeEditor-1 |
height | 高度 | string /number | - | 500px |
editorConfig | wangeditor 组件的所有配置项 | IEditorConfig | - | - |
modelValue | 内容双向绑定,支持v-model | string | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
change | 内容改变时,返回 editor 实例 | editor: IDomEditor |
方法名 | 说明 | 回调参数 |
---|---|---|
getEditorRef | 获取 editor 实例 | () => Promise<IDomEditor> |
用于各种占位图组件,如 404
、403
、500
等错误页面。
Error 组件位于 src/components/Error 内
<script setup lang="ts">\nimport { Error } from '@/components/Error'\n</script>\n\n<template>\n <Error />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
type | 占位图类型 | string | - | 404 |
方法名 | 说明 | 回调参数 |
---|---|---|
errorClick | 点击按钮后的回调 | - |
目前只提供了 404
、403
、500
三种类型,如果不满足实际需求,可自行扩展。
只需在 src/components/Error/src/Error.vue 文件的 errorMap
对象扩展对应类型即可。
为整个项目提供页脚信息,自动适应,内容高度不够时,会一直保持在最底部,内容超出则跟随在内容后面。
Footer 组件位于 src/components/Footer 内,如果需要修改页脚信息,可在组件内自定义修改。
<script setup lang="ts">\nimport { Footer } from '@/components/Footer'\n</script>\n\n<template>\n <Footer />\n</template>\n\n
对 element-plus
的 Form
组件进行封装,支持 element-plus
的所有表单组件,并额外扩展了一些功能。
Form 组件位于 src/components/Form 内
注意
推荐使用 tsx 来使用 Form
组件。
目前支持的表单组件,你可以在 在线预览 中看到。
<script setup lang="ts">\nimport { Form, FormSchema } from '@/components/Form'\nimport { reactive } from 'vue'\n\nconst schema = reactive<FormSchema[]>([\n {\n field: 'field1',\n label: 'input',\n component: 'Input'\n }\n])\n</script>\n\n<template>\n <Form :schema="schema" />\n</template>\n\n
对于复杂的场景,可以配合 useForm
来使用。
如果想看更复杂点的例子,请在线预览
<script setup lang="tsx">\nimport { Form, FormSchema } from '@/components/Form'\nimport { reactive } from 'vue'\nimport { useForm } from '@/hooks/web/useForm'\n\nconst schema = reactive<FormSchema[]>([\n {\n field: 'field1',\n label: 'input',\n component: 'Input'\n }\n])\n\nconst { formRegister } = useForm()\n</script>\n\n<template>\n <Form :schema="schema" @register="formRegister" />\n</template>\n\n
const { formRegister, formMethods } = useForm()\n
register
formRegister
用于注册 useForm
,如果需要使用 useForm
提供的 api
,必须将 formRegister
传入组件的 onRegister
formMethods
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getFormData | 用于获取表单数据 | <T = Recordable>() => Promise<T> |
getComponentExpose | 用于获取表单组件实例,如 ElInput 实例 | (field: string) => any |
getFormItemExpose | 用于获取 formItem 组件实例 | (field: string) => Promise<ComponentRef<typeof ElFormItem>> |
getElFormExpose | 用于获取 elForm 组件实例 | (field: string) => Promise<ComponentRef<typeof ElForm>> |
getFormExpose | 用于获取二次封装的 Form 组件实例 | () => Promise<ComponentRef<typeof Form>> |
除以下参数外,还支持 element-plus
的 Form
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
schema | 生成 Form 的布局结构数组,详见 | FormSchema | - | [] |
isCol | 是否需要栅格布局 | boolean | - | true |
model | 表单数据对象 | Recordable | - | {} |
autoSetPlaceholder | 是否自动设置 placeholder | boolean | - | true |
isCustom | 是否自定义内容 | boolean | - | false |
labelWidth | 表单 label 宽度 | string /number | - | auto |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
field | 唯一值,必填项 | string | - | - |
label | 标题 | string | - | - |
colProps | element-plus 的 col 组件属性 | ColProps | - | - |
componentProps | 表单组件子属性,详见 | any | - | - |
formItemProps | element-plus 的 form-item 组件属性,详见 | FormItemProps | - | - |
component | 需要渲染的表单子组件 | ComponentName | - | - |
value | 表单子组件初始值 | any | - | - |
hidden | 表单子组件是否隐藏 | boolean | - | - |
remove | 表单子组件是否隐藏,如果为true,会连同值一同删除,类似v-if | boolean | - | - |
optionApi | 加载 options 方法 | () => Promise<any> | - | - |
componentProps的类型有: InputComponentProps
AutocompleteComponentProps
InputNumberComponentProps
SelectComponentProps
SelectV2ComponentProps
CascaderComponentProps
SwitchComponentProps
RateComponentProps
ColorPickerComponentProps
TransferComponentProps
RadioGroupComponentProps
RadioButtonComponentProps
DividerComponentProps
DatePickerComponentProps
DateTimePickerComponentProps
TimePickerComponentProps
InputPasswordComponentProps
TreeSelectComponentProps
UploadComponentProps
any
基本上每个表单组件都有 slots
的插槽对象,用于自定义插槽,如 InputComponentProps :
slots?: {\n prefix?: (...args: any[]) => JSX.Element | null\n suffix?: (...args: any[]) => JSX.Element | null\n prepend?: (...args: any[]) => JSX.Element | null\n append?: (...args: any[]) => JSX.Element | null\n}\n
如果需要监听组件事件,如 change 事件,每个 ComponentProps
基本上都有 on
对象用来接收事件,如 InputComponentProps :
on?: {\n blur?: (event: FocusEvent) => void\n focus?: (event: FocusEvent) => void\n change?: (value: string | number) => void\n clear?: () => void\n input?: (value: string | number) => void\n}\n\n
除了以下属性,还支持 element-plus
中的 FormItem
的所有属性
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
slots | FormItem的插槽 | Object | - | - |
style | 子表单项的样式 | CSSProperties | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getComponentExpose | 用于获取表单子组件的实例,如 ElInput 实例 | (field: string) => any |
getFormItemExpose | 用于获取 FormItem 组件的实例 | () => Promise<typeof FormItem> |
当项目中内置的表单组件不满足需求时,可以自行添加组件进去。
ComponentName
添加你组件名称。componentMap
对象中添加键值对即可。componentProps
Highlight 组件位于 src/components/Highlight 内
组件只能接收纯文本。
<script setup lang="ts">\nimport { Highlight } from '@/components/Highlight'\n</script>\n\n<template>\n <Highlight :keys="['十年前', '现在']">\n 种一棵树最好的时间是十年前,其次就是现在。\n </Highlight>\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
tag | 包裹标签 | string | - | span |
keys | 高亮的关键字 | string[] | - | [] |
color | 高亮的颜色 | string | - | var(--el-color-primary) |
方法名 | 说明 | 回调参数 |
---|---|---|
click | 关键字点击事件 | key: string |
用于同意协议选项
IAgree 组件位于 src/components/IAgree 内
<template>\n <IAgree\n :link="[\n {\n text: '《隐私政策》',\n url: 'https://www.baidu.com'\n }\n ]"\n text="我同意《隐私政策》"\n />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
text | 文案 | string | - | - |
link | 需要跳转的高亮数据,详见 | LinkItem[] | - | - |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 跳转地址,非必填 | string | - | - |
text | 高亮文案 | string | - | - |
onClick | 点击高亮文案执行的方法,非必填 | () => void | - | - |
用于快速选择 Iconify 图标。
IconPicker 组件位于 src/components/IconPicker 内
TIP
目前只集成了 Ant Design Icons 、Element Plus、TDesign Icons 三个开源项目图标
<script lang="ts" setup>\nimport { IconPicker } from '@/components/IconPicker'\n\nconst currentIcon = ref('tdesign:book-open')\n</script>\n\n<template>\n <IconPicker v-model="currentIcon" />\n</template>\n\n
可以执行 pnpm run icon
然后选择你想要的图标集
之后,在 IconPicker.vue 导入,并添加到 icons
中即可。
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
modelValue | 选中项绑定值,支持v-model | string | - | - |
用于项目内组件的展示,基本支持所有图标库(支持按需加载,只打包所用到的图标),支持使用本地 svg 和 Iconify 图标。
Icon 组件位于 src/components/Icon 内
TIP
在 Iconify 上,你可以查询到你想要的所有图标并使用,不管是不是 element-plus
的图标库。
如果以svg-icon:
开头,则会在本地中找到该 svg
图标,否则,会加载 Iconify
图标。
<template>\n <!-- 加载本地 svg -->\n <Icon icon="svg-icon:peoples" />\n\n <!-- 加载 Iconify -->\n <Icon icon="ep:aim" />\n</template>\n\n
如果需要在其他组件中如 ElButton
传入 icon
属性,可以使用 useIcon
<script setup lang="ts">\nimport { useIcon } from '@/hooks/web/useIcon'\nimport { ElButton } from 'element-plus'\n\nconst icon = useIcon({ icon: 'svg-icon:save' })\n</script>\n\n<template>\n <ElButton :icon="icon"> button </ElButton>\n</template>\n
const icon = useIcon(props)\n
props
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
icon | 图标名 | string | - | - |
color | 图标颜色 | string | - | - |
size | 图标大小 | number | - | 16 |
hoverColor | hover颜色 | string | - | - |
将 element-plus
的 ImageViewer
组件函数化,通过函数方便创建组件。
ImageViewer 组件位于 src/components/ImageViewer 内
<script setup lang="ts">\nimport { createImageViewer } from '@/components/ImageViewer'\nimport { ElButton } from 'element-plus'\n\nconst open = () => {\n createImageViewer({\n urlList: [\n 'https://img1.baidu.com/it/u=657828739,1486746195&fm=26&fmt=auto&gp=0.jpg',\n 'https://img0.baidu.com/it/u=3114228356,677481409&fm=26&fmt=auto&gp=0.jpg',\n 'https://img1.baidu.com/it/u=508846955,3814747122&fm=26&fmt=auto&gp=0.jpg',\n 'https://img1.baidu.com/it/u=3536647690,3616605490&fm=26&fmt=auto&gp=0.jpg',\n 'https://img1.baidu.com/it/u=4087287201,1148061266&fm=26&fmt=auto&gp=0.jpg',\n 'https://img2.baidu.com/it/u=3429163260,2974496379&fm=26&fmt=auto&gp=0.jpg'\n ]\n })\n}\n</script>\n\n<template>\n <ElButton type="primary" @click="open">预览</ElButton>\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
urlList | 图片列表 | string[] | - | - |
zIndex | 层级 | number | - | - |
initialIndex | 默认展示第几张 | number | - | 1 |
infinite | 是否可以循环切换 | boolean | - | true |
hideOnClickModal | 点击蒙版是否关闭 | boolean | - | false |
appendToBody | 是否添加到 body 中 | boolean | - | false |
show | 是否显示图片预览 | boolean | - | false |
基于 Highlight
组件封装。
Infotip 组件位于 src/components/Infotip 内
<script setup lang="ts">\nimport { Infotip } from '@/components/Infotip'\n</script>\n\n<template>\n <Infotip\n title="推荐使用Iconify组件"\n :schema="[\n {\n label: 'Iconify组件基本包含所有的图标,你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。',\n keys: ['Iconify']\n },\n {\n label: '访问地址',\n keys: ['访问地址']\n }\n ]"\n />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
title | 标题 | string | - | - |
schema | 展示的数据内容 | string[] /TipSchema[] | - | [] |
showIndex | 显示序号 | boolean | - | true |
highlightColor | 高亮颜色 | string | - | var(--el-color-primary) |
方法名 | 说明 | 回调参数 |
---|---|---|
click | 关键字点击事件 | key: string |
对 element-plus
的 Input
组件进行封装。
InputPassword 组件位于 src/components/InputPassword 内
<script setup lang="ts">\nimport { InputPassword } from '@/components/InputPassword'\nimport { ref } from 'vue'\n\nconst password = ref('')\n</script>\n\n<template>\n <InputPassword v-model="password" strength />\n</template>\n\n
除以下参数外,还支持 element-plus
的 Input
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
strength | 是否显示强度校验 | boolean | - | false |
modelValue | 选中项绑定值,支持v-model | string | - | - |
基于 vue-json-pretty 封装。
可自行阅读 vue-json-pretty文档
JsonEditor 组件位于 src/components/JsonEditor 内
<script setup lang="ts">\n<script setup lang="ts">\nimport { ContentWrap } from '@/components/ContentWrap'\nimport { JsonEditor } from '@/components/JsonEditor'\nimport { useI18n } from '@/hooks/web/useI18n'\nimport { ref, watch } from 'vue'\n\nconst { t } = useI18n()\n\nconst defaultData = ref({\n title: '标题',\n content: '内容'\n})\n\nwatch(\n () => defaultData.value,\n (val) => {\n console.log(val)\n },\n {\n deep: true\n }\n)\n\nsetTimeout(() => {\n defaultData.value = {\n title: '异步标题',\n content: '异步内容'\n }\n}, 4000)\n</script>\n\n<template>\n <ContentWrap :title="t('richText.jsonEditor')" :message="t('richText.jsonEditorDes')">\n <JsonEditor v-model="defaultData" />\n </ContentWrap>\n</template>\n\n
用于颗粒级别的按钮权限组件
Permission 组件位于 src/components/Permission 内
由于项目中的颗粒级别的权限,是放在路由表中,所以会判断在当前路由 meta.permission
是否包含传入的权限值,有的话则展示。
如果权限实现不一致的话,可以自行改造下。
<template>\n <Permission permission="add">\n <ElButton type="primary"> Add </ElButton>\n </Permission>\n</template>\n\n
权限控制目前还提供了指令的使用方式,并且已经全局注册,所以可以在任意组件中使用 v-hasPermi
<ElButton v-hasPermi="'add'" type="primary"> Add </ElButton>\n\n
除了以上两种,还可以使用函数的形式进行控制
import { hasPermi } from '@/components/Permission'\n\n
<ElButton v-if="hasPermi('add')" type="primary"> Add </ElButton>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
permission | 权限值 | string | - | - |
基于 qrcode
封装。
Qrcode 组件位于 src/components/Qrcode 内
更复杂点的例子,请在线预览
<script setup lang="ts">\nimport { Qrcode } from '@/components/Qrcode'\n</script>\n\n<template>\n <Qrcode text="vue-element-plus-admin" />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
tag | 以什么标签生成二维码 | string | canvas/img | canvas |
text | 二维码内容 | string /Array | - | - |
options | qrcode.js 配置项 | QRCodeRenderersOptions | - | {} |
width | 二维码宽度 | number | - | 200 |
logo | 二维码 logo | QrcodeLogo /string | - | - |
disabled | 二维码是否过期 | boolean | - | false |
disabledText | 二维码过期提示内容 | string | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
done | 生成二维码后的回调 | - |
click | 二维码点击事件 | - |
disabled-click | 二维码过期后点击事件 | - |
基于 Form
组件封装,支持收缩展开。
Search 组件位于 src/components/Search 内
注意
推荐使用 tsx
来使用 Search
组件
更复杂例子,请在线预览
<script setup lang="ts">\nimport { Search } from '@/components/Search'\nimport { FormSchema } from '@/components/Form'\nimport { reactive } from 'vue'\n\nconst schema = reactive<FormSchema[]>([\n {\n field: 'field1',\n label: 'input',\n component: 'Input'\n }\n])\n</script>\n\n<template>\n <Search :schema="schema" />\n</template>\n\n
对于复杂的场景,可以配合 useSearch
来使用。
<script setup lang="ts">\nimport { ContentWrap } from '@/components/ContentWrap'\nimport { useI18n } from '@/hooks/web/useI18n'\nimport { Search } from '@/components/Search'\nimport { reactive, ref, unref } from 'vue'\nimport { ElButton } from 'element-plus'\nimport { getDictOneApi } from '@/api/common'\nimport { FormSchema } from '@/components/Form'\nimport { useSearch } from '@/hooks/web/useSearch'\n\nconst { t } = useI18n()\n\nconst { searchRegister, searchMethods } = useSearch()\nconst { setSchema, setProps, setValues } = searchMethods\n\nconst schema = reactive<FormSchema[]>([\n {\n field: 'field1',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field2',\n label: t('formDemo.select'),\n component: 'Select',\n componentProps: {\n options: [\n {\n label: 'option1',\n value: '1'\n },\n {\n label: 'option2',\n value: '2'\n }\n ],\n on: {\n change: (value: string) => {\n console.log(value)\n }\n }\n }\n },\n {\n field: 'field3',\n label: t('formDemo.radio'),\n component: 'RadioGroup',\n componentProps: {\n options: [\n {\n label: 'option-1',\n value: '1'\n },\n {\n label: 'option-2',\n value: '2'\n }\n ]\n }\n },\n {\n field: 'field5',\n component: 'DatePicker',\n label: t('formDemo.datePicker'),\n componentProps: {\n type: 'date'\n }\n },\n {\n field: 'field6',\n component: 'TimeSelect',\n label: t('formDemo.timeSelect')\n },\n {\n field: 'field8',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field9',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field10',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field11',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field12',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field13',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field14',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field15',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field16',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field17',\n label: t('formDemo.input'),\n component: 'Input'\n },\n {\n field: 'field18',\n label: t('formDemo.input'),\n component: 'Input'\n }\n])\n\nconst isGrid = ref(false)\n\nconst changeGrid = (grid: boolean) => {\n setProps({\n isCol: grid\n })\n // isGrid.value = grid\n}\n\nconst layout = ref('inline')\n\nconst changeLayout = () => {\n layout.value = unref(layout) === 'inline' ? 'bottom' : 'inline'\n}\n\nconst buttonPosition = ref('left')\n\nconst changePosition = (position: string) => {\n layout.value = 'bottom'\n buttonPosition.value = position\n}\n\nconst getDictOne = async () => {\n const res = await getDictOneApi()\n if (res) {\n setSchema([\n {\n field: 'field2',\n path: 'componentProps.options',\n value: res.data\n }\n ])\n }\n}\n\nconst handleSearch = (data: any) => {\n console.log(data)\n}\n\nconst delRadio = () => {\n setSchema([\n {\n field: 'field3',\n path: 'remove',\n value: true\n }\n ])\n}\n\nconst restoreRadio = () => {\n setSchema([\n {\n field: 'field3',\n path: 'remove',\n value: false\n }\n ])\n}\n\nconst setValue = () => {\n setValues({\n field1: 'Joy'\n })\n}\n\nconst searchLoading = ref(false)\nconst changeSearchLoading = () => {\n searchLoading.value = true\n setTimeout(() => {\n searchLoading.value = false\n }, 2000)\n}\n\nconst resetLoading = ref(false)\nconst changeResetLoading = () => {\n resetLoading.value = true\n setTimeout(() => {\n resetLoading.value = false\n }, 2000)\n}\n</script>\n\n<template>\n <ContentWrap\n :title="`${t('searchDemo.search')} ${t('searchDemo.operate')}`"\n style="margin-bottom: 20px"\n >\n <ElButton @click="changeGrid(true)">{{ t('searchDemo.grid') }}</ElButton>\n <ElButton @click="changeGrid(false)">\n {{ t('searchDemo.restore') }} {{ t('searchDemo.grid') }}\n </ElButton>\n\n <ElButton @click="changeLayout">\n {{ t('searchDemo.button') }} {{ t('searchDemo.position') }}\n </ElButton>\n\n <ElButton @click="changePosition('left')">\n {{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.left') }}\n </ElButton>\n <ElButton @click="changePosition('center')">\n {{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.center') }}\n </ElButton>\n <ElButton @click="changePosition('right')">\n {{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.right') }}\n </ElButton>\n <ElButton @click="getDictOne">\n {{ t('formDemo.select') }} {{ t('searchDemo.dynamicOptions') }}\n </ElButton>\n <ElButton @click="delRadio">{{ t('searchDemo.deleteRadio') }}</ElButton>\n <ElButton @click="restoreRadio">{{ t('searchDemo.restoreRadio') }}</ElButton>\n <ElButton @click="setValue">{{ t('formDemo.setValue') }}</ElButton>\n\n <ElButton @click="changeSearchLoading">\n {{ t('searchDemo.search') }} {{ t('searchDemo.loading') }}\n </ElButton>\n <ElButton @click="changeResetLoading">\n {{ t('searchDemo.reset') }} {{ t('searchDemo.loading') }}\n </ElButton>\n </ContentWrap>\n\n <ContentWrap :title="t('searchDemo.search')" :message="t('searchDemo.searchDes')">\n <Search\n :schema="schema"\n :is-col="isGrid"\n :layout="layout"\n :button-position="buttonPosition"\n :search-loading="searchLoading"\n :reset-loading="resetLoading"\n show-expand\n expand-field="field6"\n @search="handleSearch"\n @reset="handleSearch"\n @register="searchRegister"\n />\n </ContentWrap>\n</template>\n\n
const { searchRegister, searchMethods } = useSearch()\n
register
searchRegister
用于注册 useSearch
,如果需要使用 useSearch
提供的 api
,必须将 searchRegister
传入组件的 onRegister
formMethods
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getFormData | 用于获取表单数据 | <T = Recordable>() => Promise<T> |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
schema | 生成 Search 的布局结构数组,详见 | FormSchema | - | [] |
isCol | 是否需要栅格布局 | boolean | - | true |
labelWidth | 表单 label 宽度 | string /number | - | auto |
layout | 操作按钮风格位置 | string | inline/bottom | inline |
buttonPosition | 底部操作按钮的对齐方式 | string | left/center/right | center |
showSearch | 是否显示查询按钮 | boolean | - | true |
showReset | 是否显示重置按钮 | boolean | - | true |
expand | 是否显示伸缩按钮 | boolean | - | false |
expandField | 伸缩的界限字段 | string | - | - |
inline | 是否是行内 | boolean | - | true |
removeNoValueItem | 是否自动去除空值 | boolean | - | true |
model | 初始化数据 | object | - | - |
searchLoading | 查询按钮加载状态 | boolean | - | false |
resetLoading | 重置按钮加载状态 | boolean | - | false |
方法名 | 说明 | 回调参数 |
---|---|---|
search | 查询后的回调 | data: Recordable |
reset | 重置后的回调 | data: Recordable |
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getElFormExpose | 用于获取 Form 组件的实例 | () => Promise<typeof ElForm> |
1.2.4
新增
Sticky 组件位于 src/components/Sticky 内
<script setup lang="ts">\nimport { Sticky } from '@/components/Sticky'\n</script>\n\n<template>\n <Sticky :offset="90">\n <div style="padding: 10px; background-color: lightblue"> Sticky 距离顶部90px </div>\n </Sticky>\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
offset | 距离顶部或者底部的距离 | number | - | 0 |
zIndex | 设置元素的堆叠顺序 | number | - | 999 |
className | 设置指定的class | string /number | - | - |
position | 定位方式,默认为(top),表示距离顶部位置,可以设置为top或者bottom | string | top/bottom | top |
对 element-plus
的 Table
组件进行封装,只需传入 columns
与 data
参数,即可渲染出响应的表格出来。
Table 组件位于 src/components/Table 内
注意
推荐使用 tsx 来使用 Table
组件。
<script setup lang="ts">\nimport { reactive } from 'vue'\nimport { Table, TableColumn } from '@/components/Table'\n\nconst columns = reactive<TableColumn[]>([\n {\n field: 'title',\n label: 'title'\n },\n {\n field: 'author',\n label: 'author'\n }\n])\n\nconst data = reactive([\n {\n title: 'title1',\n author: 'author1'\n },\n {\n title: 'title2',\n author: 'author2'\n },\n {\n title: 'title3',\n author: 'author3'\n }\n])\n</script>\n\n<template>\n <Table :columns="columns" :data="data" />\n</template>\n\n
推荐配合 useTable
来使用
复杂点的例子,请在线预览
<script setup lang="tsx">\nimport { ContentWrap } from '@/components/ContentWrap'\nimport { useI18n } from '@/hooks/web/useI18n'\nimport { Table, TableColumn, TableSlotDefault } from '@/components/Table'\nimport { getTreeTableListApi } from '@/api/table'\nimport { reactive, unref } from 'vue'\nimport { ElTag, ElButton } from 'element-plus'\nimport { useTable } from '@/hooks/web/useTable'\n\nconst { tableRegister, tableState } = useTable({\n fetchDataApi: async () => {\n const { currentPage, pageSize } = tableState\n const res = await getTreeTableListApi({\n pageIndex: unref(currentPage),\n pageSize: unref(pageSize)\n })\n return {\n list: res.data.list,\n total: res.data.total\n }\n }\n})\nconst { loading, dataList, total, currentPage, pageSize } = tableState\n\nconst { t } = useI18n()\n\nconst columns = reactive<TableColumn[]>([\n {\n field: 'selection',\n type: 'selection'\n },\n {\n field: 'index',\n label: t('tableDemo.index'),\n type: 'index'\n },\n {\n field: 'content',\n label: t('tableDemo.header'),\n children: [\n {\n field: 'title',\n label: t('tableDemo.title')\n },\n {\n field: 'author',\n label: t('tableDemo.author')\n },\n {\n field: 'display_time',\n label: t('tableDemo.displayTime')\n },\n {\n field: 'importance',\n label: t('tableDemo.importance'),\n formatter: (_: Recordable, __: TableColumn, cellValue: number) => {\n return (\n <ElTag type={cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'}>\n {cellValue === 1\n ? t('tableDemo.important')\n : cellValue === 2\n ? t('tableDemo.good')\n : t('tableDemo.commonly')}\n </ElTag>\n )\n }\n },\n {\n field: 'pageviews',\n label: t('tableDemo.pageviews')\n }\n ]\n },\n {\n field: 'action',\n label: t('tableDemo.action'),\n slots: {\n default: (data) => {\n return (\n <ElButton type="primary" onClick={() => actionFn(data)}>\n {t('tableDemo.action')}\n </ElButton>\n )\n }\n }\n }\n])\n\nconst actionFn = (data: TableSlotDefault) => {\n console.log(data)\n}\n</script>\n\n<template>\n <ContentWrap :title="`${t('router.treeTable')} ${t('tableDemo.example')}`">\n <Table\n v-model:pageSize="pageSize"\n v-model:currentPage="currentPage"\n :columns="columns"\n :data="dataList"\n row-key="id"\n :loading="loading"\n sortable\n :pagination="{\n total: total\n }"\n @register="tableRegister"\n />\n </ContentWrap>\n</template>\n\n</script>\n\n<template>\n <Table\n v-model:pageSize="tableObject.pageSize"\n v-model:currentPage="tableObject.currentPage"\n :data="tableObject.tableList"\n :loading="tableObject.loading"\n :pagination="{\n total: tableObject.total\n }"\n @register="register"\n />\n</template>\n\n
const { tableRegister, tableState, tableMethods } = useTable(props: UseTableConfig)\n
props
在使用 useTable
的时候,需要传入 fetchDataApi
,为了保证可定制,需要自行在 fetchDataApi
中完成请求逻辑,之后返回结果 { list: Array, total?: number },后续分页,就可以自动请求数据。
如果需要删除,同样需要传入 fetchDelApi
,返回一个 Boolean
来判断是否删除完成,后续 useTable
将自行刷新表格。
tableRegister
tableRegister
用于注册 useTable
,如果需要使用 useTable
提供的 api
,必须将 tableRegister
传入组件的 onRegister
tableState
表格状态
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
pageSize | 每页显示多少条 | number | - | 10 |
currentPage | 当前页 | number | - | 1 |
total | 总条数 | number | - | - |
dataList | 表格数据 | any[] | - | [] |
loading | 表格是否加载中 | boolean | - | false |
tableMethods
方法名 | 说明 | 回调参数 |
---|---|---|
setProps | 用于表格组件属性 | (props: Recordable) => void |
getList | 获取表格数据 | () => Promise<void> |
setColumn | 设置表头结构 | (columnProps: TableSetProps[]) => void |
addColumn | 新增表头结构 | (tableColumn: TableColumn, index?: number) => void |
delColumn | 删除表头结构 | (field: string) => void |
getElTableExpose | 获取 ElTable 实例 | () => Promise<typeof ElTable> |
refresh | 刷新表格 | () => void |
delList | 删除数据 | (idsLength: number) => Promise<void> |
除以下参数外,还支持 element-plus
的 Table
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
pageSize | 每页显示多少条,支持 v-model 双向绑定 | number | - | 10 |
currentPage | 当前页,支持 v-model 双向绑定 | number | - | 1 |
selection | 是否多选 | boolean | - | true |
showOverflowTooltip | 是否所有的超出隐藏,优先级低于 schema 中的 showOverflowTooltip | boolean | - | true |
columns | 表头结构,详见 | TableColumn[] | - | [] |
expand | 是否显示展开行 | boolean | - | false |
pagination | 是否展示分页,详见 | Pagination /undefined | - | - |
reserveSelection | 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key) | boolean | - | false |
loading | 加载状态 | boolean | - | false |
reserveIndex | 是否叠加索引 | boolean | - | false |
align | 内容对齐方式 | string | left /center /right | left |
headerAlign | 表头对齐方式 | string | left /center /right | left |
data | 表格数据 | Recordable[] | - | [] |
showAction | 是否显示表格操作 | boolean | - | false |
imagePreview | 需要展示图片的字段 | string[] | - | - |
videoPreview | 需要展示视频的字段 | string[] | - | - |
customContent | 是否自定义内容 | boolean | - | false |
cardBodyStyle | 卡片内容样式 | CSSProperties | - | - |
cardBodyClass | 卡片内容类名 | string | - | - |
cardWrapStyle | 卡片容器样式 | CSSProperties | - | - |
cardWrapClass | 卡片容器类名 | string | - | - |
除了以下属性,还支持 element-plus
的 TableColumn
属性。
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
field | 唯一值,如需展示正确的数据,需要与 data 中的属性名对应 | string | - | - |
label | 表头名称 | string | - | - |
hidden | 是否隐藏 | boolean | - | - |
slots | 插槽对象 | object | - | - |
children | 子项,用于多级表头 | TableColumn[] | - | - |
支持 element-plus
的 Pagination
所有属性,详见
方法名 | 说明 | 回调参数 |
---|---|---|
setProps | 用于设置表格属性 | (props: Recordable) => void |
setColumn | 用于修改表头结构 | (columnProps: TableSetPropsType[]) => void |
addColumn | 新增表头结构 | (tableColumn: TableColumn, index?: number) => void |
delColumn | 删除表头结构 | (field: string) => void |
基于 xgplayer
二次封装的视频播放器
VideoPlayer 组件位于 src/components/VideoPlayer 内
<script lang="ts" setup>\nimport { VideoPlayer } from '@/components/VideoPlayer'\n</script>\n\n<template>\n <VideoPlayer\n url="//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4"\n poster="//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg"\n />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 视频的地址 | string | - | - |
poster | 视频的封面 | string | - | - |
将 VideoPlayer
组件函数化,通过函数方便创建组件。
VideoViewer 组件位于 src/components/VideoViewer 内
<script setup lang="ts">\nimport { createVideoViewer } from '@/components/VideoPlayer'\n\nconst open = () => {\n createVideoViewer({\n url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4',\n poster: '//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg'\n })\n}\n</script>\n\n<template>\n <ElButton type="primary" @click="open">预览</ElButton>\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 视频的地址 | string | - | - |
poster | 视频的封面 | string | - | - |
瀑布流组件
Waterfall 组件位于 src/components/Waterfall 内
TIP
data 数据必须带有高度字段,用于确保计算出正确的位置
<script lang="ts" setup>\nimport { Waterfall } from '@/components/Waterfall'\nimport Mock from 'mockjs'\nimport { ref, unref } from 'vue'\nimport { toAnyString } from '@/utils'\n\nconst data = ref<any>([])\n\nconst getList = () => {\n const list: any = []\n for (let i = 0; i < 20; i++) {\n // 随机 100, 500 之间的整数\n const height = Mock.Random.integer(100, 500)\n const width = Mock.Random.integer(100, 500)\n list.push(\n Mock.mock({\n width,\n height,\n id: toAnyString(),\n image_uri: Mock.Random.image(`${width}x${height}`)\n })\n )\n }\n data.value = [...unref(data), ...list]\n if (unref(data).length >= 60) {\n end.value = true\n }\n}\ngetList()\n\nconst loading = ref(false)\n\nconst end = ref(false)\n\nconst loadMore = () => {\n loading.value = true\n setTimeout(() => {\n getList()\n loading.value = false\n }, 1000)\n}\n</script>\n\n<template>\n <Waterfall\n :data="data"\n :loading="loading"\n :end="end"\n :props="{\n src: 'image_uri',\n height: 'height'\n }"\n @load-more="loadMore"\n />\n</template>\n\n
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
data | 要展示的数据 | Array | - | - |
reset | 窗口变化是否重新布局 | boolean | true/false | true |
width | 每个项的宽度 | number | - | 200 |
gap | 每个项的间距 | number | - | 20 |
loadingText | 加载中文字 | string | - | 加载中... |
loading | 是否加载中 | boolean | - | false |
end | 是否加载结束 | boolean | - | false |
endText | 是否加载结束文字 | string | - | 没有更多了 |
props | 字段别名 | object | - | { src: 'src', height: 'height' } |
方法名 | 说明 | 回调参数 |
---|---|---|
loadMore | 加载更多事件 | - |
为了方便开发者快速生成 组件
和 视图
文件,本项目提供了 plop
为开发者生成统一的文件模版。
运行
npm run p\n
选择 component
之后,输入组件名,如 newCom
,既可在 src/components
目录下创建对应的组件。
组件名开头如果是小写,会自动转换为大写。
运行
npm run p\n
选择 view
之后,输入路径,默认为 views
,接着输入模块名,如 newView
,既可在 src/${views}
目录下创建对应的视图文件。
如果需要扩展额外的视图模版,可以在根目录 plopfile.js
文件中,添加初始模版,然后到根目录 plop
文件夹中,添加对应的模块代码。具体可以参考 component
下的代码。
更多的 plop
配置,则可以查阅 文档
默认第一次进入系统,会检测浏览器默认的主题
如果需要切换 明亮
或者 暗黑
,可以执行 appStore.setIsDark(val)
进行主题的切换。
具体例子可以查看登录页的右上角主题切换。
',6);t.render=function(r,d,t,o,i,l){return e(),a("div",null,[h])};export default t;export{d as __pageData}; diff --git a/assets/dep_dark.md.5137d629.lean.js b/assets/dep_dark.md.5137d629.lean.js new file mode 100644 index 00000000..12441028 --- /dev/null +++ b/assets/dep_dark.md.5137d629.lean.js @@ -0,0 +1 @@ +import{o as e,c as a,a as r}from"./app.7e863e47.js";const d='{"title":"黑暗主题","description":"","frontmatter":{},"headers":[{"level":2,"title":"介绍","slug":"介绍"},{"level":2,"title":"切换主题","slug":"切换主题"}],"relativePath":"dep/dark.md","lastUpdated":1718353615647}',t={},h=r('',6);t.render=function(r,d,t,o,i,l){return e(),a("div",null,[h])};export default t;export{d as __pageData}; diff --git a/assets/dep_i18n.md.ea190651.js b/assets/dep_i18n.md.ea190651.js new file mode 100644 index 00000000..2080a488 --- /dev/null +++ b/assets/dep_i18n.md.ea190651.js @@ -0,0 +1 @@ +import{o as n,c as a,b as s,d as t}from"./app.7e863e47.js";const e='{"title":"国际化","description":"","frontmatter":{},"headers":[{"level":2,"title":"I18n-ally 插件","slug":"i18n-ally-插件"},{"level":2,"title":"配置默认语言","slug":"配置默认语言"},{"level":2,"title":"语言文件","slug":"语言文件"},{"level":2,"title":"语言导入逻辑说明","slug":"语言导入逻辑说明"},{"level":2,"title":"使用","slug":"使用"},{"level":2,"title":"切换语言","slug":"切换语言"},{"level":2,"title":"新增","slug":"新增"},{"level":3,"title":"语言文件","slug":"语言文件-1"},{"level":3,"title":"新增语言","slug":"新增语言"},{"level":2,"title":"远程读取语言数据","slug":"远程读取语言数据"},{"level":3,"title":"useLocale","slug":"uselocale"}],"relativePath":"dep/i18n.md","lastUpdated":1718353615651}',o={},l=s("h1",{id:"国际化"},[s("a",{class:"header-anchor",href:"#国际化","aria-hidden":"true"},"#"),t(" 国际化")],-1),c=s("p",null,[t("如果你使用的 vscode 开发工具,则推荐安装 "),s("a",{href:"https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally",target:"_blank",rel:"noopener noreferrer"},"I18n-ally"),t(" 这个插件")],-1),p=s("h2",{id:"i18n-ally-插件"},[s("a",{class:"header-anchor",href:"#i18n-ally-插件","aria-hidden":"true"},"#"),t(" I18n-ally 插件")],-1),u=s("p",null,"安装了该插件后,你的代码内可以实时看到对应的语言内容",-1),r=s("p",null,[s("img",{src:"/images/i18n.png",alt:""})],-1),i=s("h2",{id:"配置默认语言"},[s("a",{class:"header-anchor",href:"#配置默认语言","aria-hidden":"true"},"#"),t(" 配置默认语言")],-1),k=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/config/locale.ts",target:"_blank",rel:"noopener noreferrer"},"src/config/locale.ts"),t(" 内配置 "),s("code",null,"currentLocale"),t(" 为其他语言。")],-1),d=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"import"),t(),s("span",{class:"token punctuation"},"{"),t(" useCache "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'@/hooks/web/useCache'"),t("\n"),s("span",{class:"token keyword"},"import"),t(" zhCn "),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'element-plus/lib/locale/lang/zh-cn'"),t("\n"),s("span",{class:"token keyword"},"import"),t(" en "),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'element-plus/lib/locale/lang/en'"),t("\n\n"),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token punctuation"},"{"),t(" wsCache "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useCache"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n\n"),s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"const"),t(" elLocaleMap "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token operator"},":"),t(" zhCn"),s("span",{class:"token punctuation"},","),t("\n en"),s("span",{class:"token operator"},":"),t(" en\n"),s("span",{class:"token punctuation"},"}"),t("\n"),s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"interface"),t(),s("span",{class:"token class-name"},"LocaleState"),t(),s("span",{class:"token punctuation"},"{"),t("\n currentLocale"),s("span",{class:"token operator"},":"),t(" LocaleDropdownType\n localeMap"),s("span",{class:"token operator"},":"),t(" LocaleDropdownType"),s("span",{class:"token punctuation"},"["),s("span",{class:"token punctuation"},"]"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n\n"),s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"const"),t(" localeModules"),s("span",{class:"token operator"},":"),t(" LocaleState "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token punctuation"},"{"),t("\n currentLocale"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(" wsCache"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"get"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'lang'"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"||"),t(),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token punctuation"},","),t("\n elLocale"),s("span",{class:"token operator"},":"),t(" elLocaleMap"),s("span",{class:"token punctuation"},"["),t("wsCache"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"get"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'lang'"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"||"),t(),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token punctuation"},"]"),t("\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},","),t("\n "),s("span",{class:"token comment"},"// 多语言"),t("\n localeMap"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token punctuation"},"["),t("\n "),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token punctuation"},","),t("\n name"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'简体中文'"),t("\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},","),t("\n "),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'en'"),s("span",{class:"token punctuation"},","),t("\n name"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'English'"),t("\n "),s("span",{class:"token punctuation"},"}"),t("\n "),s("span",{class:"token punctuation"},"]"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n\n")])])],-1),g=s("h2",{id:"语言文件"},[s("a",{class:"header-anchor",href:"#语言文件","aria-hidden":"true"},"#"),t(" 语言文件")],-1),h=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/src/locales",target:"_blank",rel:"noopener noreferrer"},"src/locales"),t(" 可以配置具体的语言,目前项目中的语言都是没有拆分的,全部放一起,后续会考虑拆分出来,比较好维护。")],-1),m=s("h2",{id:"语言导入逻辑说明"},[s("a",{class:"header-anchor",href:"#语言导入逻辑说明","aria-hidden":"true"},"#"),t(" 语言导入逻辑说明")],-1),f=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/plugins/vueI18n/index.ts",target:"_blank",rel:"noopener noreferrer"},"src/plugins/vueI18n/index.ts"),t(" 内可以看到")],-1),y=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"const"),t(" defaultLocal "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"await"),t(),s("span",{class:"token keyword"},"import"),s("span",{class:"token punctuation"},"("),s("span",{class:"token template-string"},[s("span",{class:"token template-punctuation string"},"`"),s("span",{class:"token string"},"../../locales/"),s("span",{class:"token interpolation"},[s("span",{class:"token interpolation-punctuation punctuation"},"${"),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token interpolation-punctuation punctuation"},"}")]),s("span",{class:"token string"},".ts"),s("span",{class:"token template-punctuation string"},"`")]),s("span",{class:"token punctuation"},")"),t("\n")])])],-1),w=s("p",null,[t("这会导入 "),s("code",null,"src/locales"),t(" 文件语言包。")],-1),b=s("h2",{id:"使用"},[s("a",{class:"header-anchor",href:"#使用","aria-hidden":"true"},"#"),t(" 使用")],-1),L=s("p",null,[t("引入项目自带的 "),s("code",null,"useI18n"),t(),s("strong",null,"注意不要引入 vue-i18n 的 useI18n")],-1),v=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"import"),t(),s("span",{class:"token punctuation"},"{"),t(" useI18n "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'/@/hooks/web/useI18n'"),t("\n\n"),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token punctuation"},"{"),t(" t "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useI18n"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n\n"),s("span",{class:"token keyword"},"const"),t(" title "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"t"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'common.menu'"),s("span",{class:"token punctuation"},")"),t("\n")])])],-1),I=s("h2",{id:"切换语言"},[s("a",{class:"header-anchor",href:"#切换语言","aria-hidden":"true"},"#"),t(" 切换语言")],-1),C=s("p",null,[t("切换语言需要使用 "),s("a",{href:"https://github.com/anncwb/vue-vben-admin/tree/main/src/locales/useLocale.ts",target:"_blank",rel:"noopener noreferrer"},"src/locales/useLocale.ts")],-1),M=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"import"),t(),s("span",{class:"token punctuation"},"{"),t(" useLocale "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'@/hooks/web/useLocale'"),t("\n"),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token punctuation"},"{"),t(" changeLocale "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useLocale"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n\n"),s("span",{class:"token function"},"changeLocale"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'en'"),s("span",{class:"token punctuation"},")"),t("\n")])])],-1),_=s("h2",{id:"新增"},[s("a",{class:"header-anchor",href:"#新增","aria-hidden":"true"},"#"),t(" 新增")],-1),x=s("h3",{id:"语言文件-1"},[s("a",{class:"header-anchor",href:"#语言文件-1","aria-hidden":"true"},"#"),t(" 语言文件")],-1),z=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/src/locales",target:"_blank",rel:"noopener noreferrer"},"src/locales"),t(" 增加对应语言的文件即可")],-1),S=s("h3",{id:"新增语言"},[s("a",{class:"header-anchor",href:"#新增语言","aria-hidden":"true"},"#"),t(" 新增语言")],-1),N=s("p",null,[t("目前项目自带的语言只有 "),s("code",null,"zh_CN"),t(" 和 "),s("code",null,"en"),t(" 两种")],-1),T=s("p",null,"如果需要新增,按以下操作即可",-1),O=s("ol",null,[s("li",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/src/locales",target:"_blank",rel:"noopener noreferrer"},"src/locales"),t(" 下语言文件")]),s("li",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/types/global.d.ts",target:"_blank",rel:"noopener noreferrer"},"types/global.d.ts"),t(" 给 "),s("code",null,"LocaleType"),t(" 添加对应的类型")]),s("li",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/config/locale.ts",target:"_blank",rel:"noopener noreferrer"},"src/config/locale.ts"),t(),s("code",null,"localeMap"),t(" 中添加对应语言")])],-1),W=s("h2",{id:"远程读取语言数据"},[s("a",{class:"header-anchor",href:"#远程读取语言数据","aria-hidden":"true"},"#"),t(" 远程读取语言数据")],-1),P=s("p",null,[t("目前项目会在 "),s("code",null,"src/main.ts"),t(" 内等待 "),s("code",null,"setupI18n"),t(" 这个函数执行完之后才会渲染界面,所以只需在 setupI18n 内的 "),s("code",null,"createI18nOptions"),t(" 发送 ajax 请求,将对应的数据设置到 i18n 实例上即可。")],-1),$=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"const"),t(" createI18nOptions "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"async"),t(),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token builtin"},"Promise"),s("span",{class:"token operator"},"<"),t("I18nOptions"),s("span",{class:"token operator"},">"),t(),s("span",{class:"token operator"},"=>"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token keyword"},"const"),t(" localeStore "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useLocaleStoreWithOut"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n "),s("span",{class:"token keyword"},"const"),t(" locale "),s("span",{class:"token operator"},"="),t(" localeStore"),s("span",{class:"token punctuation"},"."),t("getCurrentLocale\n "),s("span",{class:"token keyword"},"const"),t(" localeMap "),s("span",{class:"token operator"},"="),t(" localeStore"),s("span",{class:"token punctuation"},"."),t("getLocaleMap\n "),s("span",{class:"token comment"},"// 这里改为远程请求即可。"),t("\n "),s("span",{class:"token keyword"},"const"),t(" defaultLocal "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"await"),t(),s("span",{class:"token keyword"},"import"),s("span",{class:"token punctuation"},"("),s("span",{class:"token template-string"},[s("span",{class:"token template-punctuation string"},"`"),s("span",{class:"token string"},"../../locales/"),s("span",{class:"token interpolation"},[s("span",{class:"token interpolation-punctuation punctuation"},"${"),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token interpolation-punctuation punctuation"},"}")]),s("span",{class:"token string"},".ts"),s("span",{class:"token template-punctuation string"},"`")]),s("span",{class:"token punctuation"},")"),t("\n "),s("span",{class:"token keyword"},"const"),t(" message "),s("span",{class:"token operator"},"="),t(" defaultLocal"),s("span",{class:"token punctuation"},"."),t("default "),s("span",{class:"token operator"},"??"),t(),s("span",{class:"token punctuation"},"{"),s("span",{class:"token punctuation"},"}"),t("\n\n "),s("span",{class:"token function"},"setHtmlPageLang"),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},")"),t("\n\n localeStore"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"setCurrentLocale"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(" locale"),s("span",{class:"token punctuation"},"."),t("lang\n "),s("span",{class:"token comment"},"// elLocale: elLocal"),t("\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},")"),t("\n\n "),s("span",{class:"token keyword"},"return"),t(),s("span",{class:"token punctuation"},"{"),t("\n legacy"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"false"),s("span",{class:"token punctuation"},","),t("\n locale"),s("span",{class:"token operator"},":"),t(" locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},","),t("\n fallbackLocale"),s("span",{class:"token operator"},":"),t(" locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},","),t("\n messages"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token punctuation"},"["),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},"]"),s("span",{class:"token operator"},":"),t(" message\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},","),t("\n availableLocales"),s("span",{class:"token operator"},":"),t(" localeMap"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"map"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},"("),t("v"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"=>"),t(" v"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},")"),s("span",{class:"token punctuation"},","),t("\n sync"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"true"),s("span",{class:"token punctuation"},","),t("\n silentTranslationWarn"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"true"),s("span",{class:"token punctuation"},","),t("\n missingWarn"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"false"),s("span",{class:"token punctuation"},","),t("\n silentFallbackWarn"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"true"),t("\n "),s("span",{class:"token punctuation"},"}"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n")])])],-1),j=s("h3",{id:"uselocale"},[s("a",{class:"header-anchor",href:"#uselocale","aria-hidden":"true"},"#"),t(" useLocale")],-1),D=s("p",null,[t("代码: "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/hooks/web/useLocale.ts",target:"_blank",rel:"noopener noreferrer"},"src/hooks/web/useLocale/")],-1),A=s("p",null,[t("当手动切换语言的时候会触发 "),s("code",null,"useLocale"),t(" 函数,useLocale 也是异步函数,只需等待接口返回响应的数据后,再进行设置即可")],-1),E=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token function-variable function"},"useLocale"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"=>"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token comment"},"// Switching the language will change the locale of useI18n"),t("\n "),s("span",{class:"token comment"},"// And submit to configuration modification"),t("\n "),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token function-variable function"},"changeLocale"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"async"),t(),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token operator"},":"),t(" LocaleType"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"=>"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token keyword"},"const"),t(" globalI18n "),s("span",{class:"token operator"},"="),t(" i18n"),s("span",{class:"token punctuation"},"."),t("global\n \n "),s("span",{class:"token comment"},"// 改为远程获取"),t("\n "),s("span",{class:"token keyword"},"const"),t(" langModule "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"await"),t(),s("span",{class:"token keyword"},"import"),s("span",{class:"token punctuation"},"("),s("span",{class:"token template-string"},[s("span",{class:"token template-punctuation string"},"`"),s("span",{class:"token string"},"../../locales/"),s("span",{class:"token interpolation"},[s("span",{class:"token interpolation-punctuation punctuation"},"${"),t("locale"),s("span",{class:"token interpolation-punctuation punctuation"},"}")]),s("span",{class:"token string"},".ts"),s("span",{class:"token template-punctuation string"},"`")]),s("span",{class:"token punctuation"},")"),t("\n\n globalI18n"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"setLocaleMessage"),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token punctuation"},","),t(" langModule"),s("span",{class:"token punctuation"},"."),t("default"),s("span",{class:"token punctuation"},")"),t("\n\n "),s("span",{class:"token function"},"setI18nLanguage"),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token punctuation"},")"),t("\n "),s("span",{class:"token punctuation"},"}"),t("\n\n "),s("span",{class:"token keyword"},"return"),t(),s("span",{class:"token punctuation"},"{"),t("\n changeLocale\n "),s("span",{class:"token punctuation"},"}"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n")])])],-1);o.render=function(s,t,e,o,F,H){return n(),a("div",null,[l,c,p,u,r,i,k,d,g,h,m,f,y,w,b,L,v,I,C,M,_,x,z,S,N,T,O,W,P,$,j,D,A,E])};export default o;export{e as __pageData}; diff --git a/assets/dep_i18n.md.ea190651.lean.js b/assets/dep_i18n.md.ea190651.lean.js new file mode 100644 index 00000000..2080a488 --- /dev/null +++ b/assets/dep_i18n.md.ea190651.lean.js @@ -0,0 +1 @@ +import{o as n,c as a,b as s,d as t}from"./app.7e863e47.js";const e='{"title":"国际化","description":"","frontmatter":{},"headers":[{"level":2,"title":"I18n-ally 插件","slug":"i18n-ally-插件"},{"level":2,"title":"配置默认语言","slug":"配置默认语言"},{"level":2,"title":"语言文件","slug":"语言文件"},{"level":2,"title":"语言导入逻辑说明","slug":"语言导入逻辑说明"},{"level":2,"title":"使用","slug":"使用"},{"level":2,"title":"切换语言","slug":"切换语言"},{"level":2,"title":"新增","slug":"新增"},{"level":3,"title":"语言文件","slug":"语言文件-1"},{"level":3,"title":"新增语言","slug":"新增语言"},{"level":2,"title":"远程读取语言数据","slug":"远程读取语言数据"},{"level":3,"title":"useLocale","slug":"uselocale"}],"relativePath":"dep/i18n.md","lastUpdated":1718353615651}',o={},l=s("h1",{id:"国际化"},[s("a",{class:"header-anchor",href:"#国际化","aria-hidden":"true"},"#"),t(" 国际化")],-1),c=s("p",null,[t("如果你使用的 vscode 开发工具,则推荐安装 "),s("a",{href:"https://marketplace.visualstudio.com/items?itemName=Lokalise.i18n-ally",target:"_blank",rel:"noopener noreferrer"},"I18n-ally"),t(" 这个插件")],-1),p=s("h2",{id:"i18n-ally-插件"},[s("a",{class:"header-anchor",href:"#i18n-ally-插件","aria-hidden":"true"},"#"),t(" I18n-ally 插件")],-1),u=s("p",null,"安装了该插件后,你的代码内可以实时看到对应的语言内容",-1),r=s("p",null,[s("img",{src:"/images/i18n.png",alt:""})],-1),i=s("h2",{id:"配置默认语言"},[s("a",{class:"header-anchor",href:"#配置默认语言","aria-hidden":"true"},"#"),t(" 配置默认语言")],-1),k=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/config/locale.ts",target:"_blank",rel:"noopener noreferrer"},"src/config/locale.ts"),t(" 内配置 "),s("code",null,"currentLocale"),t(" 为其他语言。")],-1),d=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"import"),t(),s("span",{class:"token punctuation"},"{"),t(" useCache "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'@/hooks/web/useCache'"),t("\n"),s("span",{class:"token keyword"},"import"),t(" zhCn "),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'element-plus/lib/locale/lang/zh-cn'"),t("\n"),s("span",{class:"token keyword"},"import"),t(" en "),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'element-plus/lib/locale/lang/en'"),t("\n\n"),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token punctuation"},"{"),t(" wsCache "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useCache"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n\n"),s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"const"),t(" elLocaleMap "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token operator"},":"),t(" zhCn"),s("span",{class:"token punctuation"},","),t("\n en"),s("span",{class:"token operator"},":"),t(" en\n"),s("span",{class:"token punctuation"},"}"),t("\n"),s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"interface"),t(),s("span",{class:"token class-name"},"LocaleState"),t(),s("span",{class:"token punctuation"},"{"),t("\n currentLocale"),s("span",{class:"token operator"},":"),t(" LocaleDropdownType\n localeMap"),s("span",{class:"token operator"},":"),t(" LocaleDropdownType"),s("span",{class:"token punctuation"},"["),s("span",{class:"token punctuation"},"]"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n\n"),s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"const"),t(" localeModules"),s("span",{class:"token operator"},":"),t(" LocaleState "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token punctuation"},"{"),t("\n currentLocale"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(" wsCache"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"get"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'lang'"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"||"),t(),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token punctuation"},","),t("\n elLocale"),s("span",{class:"token operator"},":"),t(" elLocaleMap"),s("span",{class:"token punctuation"},"["),t("wsCache"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"get"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'lang'"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"||"),t(),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token punctuation"},"]"),t("\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},","),t("\n "),s("span",{class:"token comment"},"// 多语言"),t("\n localeMap"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token punctuation"},"["),t("\n "),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'zh-CN'"),s("span",{class:"token punctuation"},","),t("\n name"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'简体中文'"),t("\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},","),t("\n "),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'en'"),s("span",{class:"token punctuation"},","),t("\n name"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token string"},"'English'"),t("\n "),s("span",{class:"token punctuation"},"}"),t("\n "),s("span",{class:"token punctuation"},"]"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n\n")])])],-1),g=s("h2",{id:"语言文件"},[s("a",{class:"header-anchor",href:"#语言文件","aria-hidden":"true"},"#"),t(" 语言文件")],-1),h=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/src/locales",target:"_blank",rel:"noopener noreferrer"},"src/locales"),t(" 可以配置具体的语言,目前项目中的语言都是没有拆分的,全部放一起,后续会考虑拆分出来,比较好维护。")],-1),m=s("h2",{id:"语言导入逻辑说明"},[s("a",{class:"header-anchor",href:"#语言导入逻辑说明","aria-hidden":"true"},"#"),t(" 语言导入逻辑说明")],-1),f=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/plugins/vueI18n/index.ts",target:"_blank",rel:"noopener noreferrer"},"src/plugins/vueI18n/index.ts"),t(" 内可以看到")],-1),y=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"const"),t(" defaultLocal "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"await"),t(),s("span",{class:"token keyword"},"import"),s("span",{class:"token punctuation"},"("),s("span",{class:"token template-string"},[s("span",{class:"token template-punctuation string"},"`"),s("span",{class:"token string"},"../../locales/"),s("span",{class:"token interpolation"},[s("span",{class:"token interpolation-punctuation punctuation"},"${"),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token interpolation-punctuation punctuation"},"}")]),s("span",{class:"token string"},".ts"),s("span",{class:"token template-punctuation string"},"`")]),s("span",{class:"token punctuation"},")"),t("\n")])])],-1),w=s("p",null,[t("这会导入 "),s("code",null,"src/locales"),t(" 文件语言包。")],-1),b=s("h2",{id:"使用"},[s("a",{class:"header-anchor",href:"#使用","aria-hidden":"true"},"#"),t(" 使用")],-1),L=s("p",null,[t("引入项目自带的 "),s("code",null,"useI18n"),t(),s("strong",null,"注意不要引入 vue-i18n 的 useI18n")],-1),v=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"import"),t(),s("span",{class:"token punctuation"},"{"),t(" useI18n "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'/@/hooks/web/useI18n'"),t("\n\n"),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token punctuation"},"{"),t(" t "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useI18n"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n\n"),s("span",{class:"token keyword"},"const"),t(" title "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"t"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'common.menu'"),s("span",{class:"token punctuation"},")"),t("\n")])])],-1),I=s("h2",{id:"切换语言"},[s("a",{class:"header-anchor",href:"#切换语言","aria-hidden":"true"},"#"),t(" 切换语言")],-1),C=s("p",null,[t("切换语言需要使用 "),s("a",{href:"https://github.com/anncwb/vue-vben-admin/tree/main/src/locales/useLocale.ts",target:"_blank",rel:"noopener noreferrer"},"src/locales/useLocale.ts")],-1),M=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"import"),t(),s("span",{class:"token punctuation"},"{"),t(" useLocale "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token keyword"},"from"),t(),s("span",{class:"token string"},"'@/hooks/web/useLocale'"),t("\n"),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token punctuation"},"{"),t(" changeLocale "),s("span",{class:"token punctuation"},"}"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useLocale"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n\n"),s("span",{class:"token function"},"changeLocale"),s("span",{class:"token punctuation"},"("),s("span",{class:"token string"},"'en'"),s("span",{class:"token punctuation"},")"),t("\n")])])],-1),_=s("h2",{id:"新增"},[s("a",{class:"header-anchor",href:"#新增","aria-hidden":"true"},"#"),t(" 新增")],-1),x=s("h3",{id:"语言文件-1"},[s("a",{class:"header-anchor",href:"#语言文件-1","aria-hidden":"true"},"#"),t(" 语言文件")],-1),z=s("p",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/src/locales",target:"_blank",rel:"noopener noreferrer"},"src/locales"),t(" 增加对应语言的文件即可")],-1),S=s("h3",{id:"新增语言"},[s("a",{class:"header-anchor",href:"#新增语言","aria-hidden":"true"},"#"),t(" 新增语言")],-1),N=s("p",null,[t("目前项目自带的语言只有 "),s("code",null,"zh_CN"),t(" 和 "),s("code",null,"en"),t(" 两种")],-1),T=s("p",null,"如果需要新增,按以下操作即可",-1),O=s("ol",null,[s("li",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/src/locales",target:"_blank",rel:"noopener noreferrer"},"src/locales"),t(" 下语言文件")]),s("li",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/tree/master/types/global.d.ts",target:"_blank",rel:"noopener noreferrer"},"types/global.d.ts"),t(" 给 "),s("code",null,"LocaleType"),t(" 添加对应的类型")]),s("li",null,[t("在 "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/config/locale.ts",target:"_blank",rel:"noopener noreferrer"},"src/config/locale.ts"),t(),s("code",null,"localeMap"),t(" 中添加对应语言")])],-1),W=s("h2",{id:"远程读取语言数据"},[s("a",{class:"header-anchor",href:"#远程读取语言数据","aria-hidden":"true"},"#"),t(" 远程读取语言数据")],-1),P=s("p",null,[t("目前项目会在 "),s("code",null,"src/main.ts"),t(" 内等待 "),s("code",null,"setupI18n"),t(" 这个函数执行完之后才会渲染界面,所以只需在 setupI18n 内的 "),s("code",null,"createI18nOptions"),t(" 发送 ajax 请求,将对应的数据设置到 i18n 实例上即可。")],-1),$=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"const"),t(" createI18nOptions "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"async"),t(),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token builtin"},"Promise"),s("span",{class:"token operator"},"<"),t("I18nOptions"),s("span",{class:"token operator"},">"),t(),s("span",{class:"token operator"},"=>"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token keyword"},"const"),t(" localeStore "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token function"},"useLocaleStoreWithOut"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t("\n "),s("span",{class:"token keyword"},"const"),t(" locale "),s("span",{class:"token operator"},"="),t(" localeStore"),s("span",{class:"token punctuation"},"."),t("getCurrentLocale\n "),s("span",{class:"token keyword"},"const"),t(" localeMap "),s("span",{class:"token operator"},"="),t(" localeStore"),s("span",{class:"token punctuation"},"."),t("getLocaleMap\n "),s("span",{class:"token comment"},"// 这里改为远程请求即可。"),t("\n "),s("span",{class:"token keyword"},"const"),t(" defaultLocal "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"await"),t(),s("span",{class:"token keyword"},"import"),s("span",{class:"token punctuation"},"("),s("span",{class:"token template-string"},[s("span",{class:"token template-punctuation string"},"`"),s("span",{class:"token string"},"../../locales/"),s("span",{class:"token interpolation"},[s("span",{class:"token interpolation-punctuation punctuation"},"${"),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token interpolation-punctuation punctuation"},"}")]),s("span",{class:"token string"},".ts"),s("span",{class:"token template-punctuation string"},"`")]),s("span",{class:"token punctuation"},")"),t("\n "),s("span",{class:"token keyword"},"const"),t(" message "),s("span",{class:"token operator"},"="),t(" defaultLocal"),s("span",{class:"token punctuation"},"."),t("default "),s("span",{class:"token operator"},"??"),t(),s("span",{class:"token punctuation"},"{"),s("span",{class:"token punctuation"},"}"),t("\n\n "),s("span",{class:"token function"},"setHtmlPageLang"),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},")"),t("\n\n localeStore"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"setCurrentLocale"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},"{"),t("\n lang"),s("span",{class:"token operator"},":"),t(" locale"),s("span",{class:"token punctuation"},"."),t("lang\n "),s("span",{class:"token comment"},"// elLocale: elLocal"),t("\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},")"),t("\n\n "),s("span",{class:"token keyword"},"return"),t(),s("span",{class:"token punctuation"},"{"),t("\n legacy"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"false"),s("span",{class:"token punctuation"},","),t("\n locale"),s("span",{class:"token operator"},":"),t(" locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},","),t("\n fallbackLocale"),s("span",{class:"token operator"},":"),t(" locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},","),t("\n messages"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token punctuation"},"["),t("locale"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},"]"),s("span",{class:"token operator"},":"),t(" message\n "),s("span",{class:"token punctuation"},"}"),s("span",{class:"token punctuation"},","),t("\n availableLocales"),s("span",{class:"token operator"},":"),t(" localeMap"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"map"),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},"("),t("v"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"=>"),t(" v"),s("span",{class:"token punctuation"},"."),t("lang"),s("span",{class:"token punctuation"},")"),s("span",{class:"token punctuation"},","),t("\n sync"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"true"),s("span",{class:"token punctuation"},","),t("\n silentTranslationWarn"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"true"),s("span",{class:"token punctuation"},","),t("\n missingWarn"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"false"),s("span",{class:"token punctuation"},","),t("\n silentFallbackWarn"),s("span",{class:"token operator"},":"),t(),s("span",{class:"token boolean"},"true"),t("\n "),s("span",{class:"token punctuation"},"}"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n")])])],-1),j=s("h3",{id:"uselocale"},[s("a",{class:"header-anchor",href:"#uselocale","aria-hidden":"true"},"#"),t(" useLocale")],-1),D=s("p",null,[t("代码: "),s("a",{href:"https://github.com/kailong321200875/vue-element-plus-admin/blob/master/src/hooks/web/useLocale.ts",target:"_blank",rel:"noopener noreferrer"},"src/hooks/web/useLocale/")],-1),A=s("p",null,[t("当手动切换语言的时候会触发 "),s("code",null,"useLocale"),t(" 函数,useLocale 也是异步函数,只需等待接口返回响应的数据后,再进行设置即可")],-1),E=s("div",{class:"language-ts"},[s("pre",null,[s("code",null,[s("span",{class:"token keyword"},"export"),t(),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token function-variable function"},"useLocale"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token punctuation"},"("),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"=>"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token comment"},"// Switching the language will change the locale of useI18n"),t("\n "),s("span",{class:"token comment"},"// And submit to configuration modification"),t("\n "),s("span",{class:"token keyword"},"const"),t(),s("span",{class:"token function-variable function"},"changeLocale"),t(),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"async"),t(),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token operator"},":"),t(" LocaleType"),s("span",{class:"token punctuation"},")"),t(),s("span",{class:"token operator"},"=>"),t(),s("span",{class:"token punctuation"},"{"),t("\n "),s("span",{class:"token keyword"},"const"),t(" globalI18n "),s("span",{class:"token operator"},"="),t(" i18n"),s("span",{class:"token punctuation"},"."),t("global\n \n "),s("span",{class:"token comment"},"// 改为远程获取"),t("\n "),s("span",{class:"token keyword"},"const"),t(" langModule "),s("span",{class:"token operator"},"="),t(),s("span",{class:"token keyword"},"await"),t(),s("span",{class:"token keyword"},"import"),s("span",{class:"token punctuation"},"("),s("span",{class:"token template-string"},[s("span",{class:"token template-punctuation string"},"`"),s("span",{class:"token string"},"../../locales/"),s("span",{class:"token interpolation"},[s("span",{class:"token interpolation-punctuation punctuation"},"${"),t("locale"),s("span",{class:"token interpolation-punctuation punctuation"},"}")]),s("span",{class:"token string"},".ts"),s("span",{class:"token template-punctuation string"},"`")]),s("span",{class:"token punctuation"},")"),t("\n\n globalI18n"),s("span",{class:"token punctuation"},"."),s("span",{class:"token function"},"setLocaleMessage"),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token punctuation"},","),t(" langModule"),s("span",{class:"token punctuation"},"."),t("default"),s("span",{class:"token punctuation"},")"),t("\n\n "),s("span",{class:"token function"},"setI18nLanguage"),s("span",{class:"token punctuation"},"("),t("locale"),s("span",{class:"token punctuation"},")"),t("\n "),s("span",{class:"token punctuation"},"}"),t("\n\n "),s("span",{class:"token keyword"},"return"),t(),s("span",{class:"token punctuation"},"{"),t("\n changeLocale\n "),s("span",{class:"token punctuation"},"}"),t("\n"),s("span",{class:"token punctuation"},"}"),t("\n")])])],-1);o.render=function(s,t,e,o,F,H){return n(),a("div",null,[l,c,p,u,r,i,k,d,g,h,m,f,y,w,b,L,v,I,C,M,_,x,z,S,N,T,O,W,P,$,j,D,A,E])};export default o;export{e as __pageData}; diff --git a/assets/dep_lint.md.13a9d801.js b/assets/dep_lint.md.13a9d801.js new file mode 100644 index 00000000..b9182194 --- /dev/null +++ b/assets/dep_lint.md.13a9d801.js @@ -0,0 +1 @@ +import{o as s,c as n,a as t}from"./app.7e863e47.js";const e='{"title":"Lint","description":"","frontmatter":{},"headers":[{"level":2,"title":"介绍","slug":"介绍"},{"level":2,"title":"ESLint","slug":"eslint"},{"level":3,"title":"手动校验代码","slug":"手动校验代码"},{"level":3,"title":"配置项","slug":"配置项"},{"level":2,"title":"CommitLint","slug":"commitlint"},{"level":3,"title":"配置","slug":"配置"},{"level":3,"title":"Git 提交规范","slug":"git-提交规范"},{"level":3,"title":"如何关闭","slug":"如何关闭"},{"level":3,"title":"示例","slug":"示例"},{"level":2,"title":"Stylelint","slug":"stylelint"},{"level":3,"title":"配置","slug":"配置-1"},{"level":3,"title":"编辑器配合","slug":"编辑器配合"},{"level":2,"title":"Prettier","slug":"prettier"},{"level":3,"title":"配置","slug":"配置-2"},{"level":3,"title":"编辑器配合","slug":"编辑器配合-1"},{"level":2,"title":"Git Hook","slug":"git-hook"},{"level":3,"title":"husky","slug":"husky"},{"level":3,"title":"如何跳过某一个检查","slug":"如何跳过某一个检查"},{"level":3,"title":"lint-staged","slug":"lint-staged"}],"relativePath":"dep/lint.md","lastUpdated":1718353615651}',a={},i=t('使用 lint 的好处
具备基本工程素养的同学都会注重编码规范,而代码风格检查(Code Linting,简称 Lint)是保障代码规范一致性的重要手段。
遵循相应的代码规范有以下好处
项目内集成了以下几种代码校验方式
注意
lint 不是必须的,但是很有必要,一个项目做大了以后或者参与人员过多后,就会出现各种风格迥异的代码,对后续的维护造成了一定的麻烦。
ESLint 是一个代码规范和错误检查工具,可以根据自己的团队设置符合自己团队的规范
# 执行下面代码.能修复的会自动修复,不能修复的需要手动修改\npnpm run lint:eslint\n
项目的 eslint 配置位于根目录下 .eslintrc.js 内,可以根据团队自行修改代码规范
在一个团队中,每个人的 git 的 commit 信息都不一样,五花八门,没有一个机制很难保证规范化,如何才能规范化呢?可能你想到的是 git 的 hook 机制,去写 shell 脚本去实现。这当然可以,其实 JavaScript 有一个很好的工具可以实现这个模板,它就是 commitlint(用于校验 git 提交信息规范)。
commit-lint 的配置位于项目根目录下 commitlint.config.js
feat
新功能fix
修补 bugdocs
文档style
格式、样式(不影响代码运行的变动)refactor
重构(即不是新增功能,也不是修改 BUG 的代码)perf
优化相关,比如提升性能、体验test
添加测试build
编译相关的修改,对项目构建或者依赖的改动ci
持续集成修改chore
构建过程或辅助工具的变动revert
回滚到上一个版本workflow
工作流改进mod
不确定分类的修改wip
开发中types
类型在 .husky/commit-msg
内注释以下代码即可
# npx --no-install commitlint --edit "$1"\n
\ngit commit -m 'feat: add new component'\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
# 加上 --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
如果你想进入技术交流群讨论,请扫码入群或者添加我为好友邀请入群
项目中集成了 2 种权限处理方式:
目前项目中提供了测试的帐号:
admin/admin
实现原理: 在前端固定写死路由的权限,指定路由有哪些权限可以查看。只初始化通用的路由,需要权限才能访问的路由没有被加入路由表内。在登陆后或者其他方式获取对应的路由 keys 后,遍历路由表去匹配 keys,过滤生成可以访问的路由表,再通过 router.addRoutes
添加到路由实例,实现权限的过滤。
缺点: 权限相对不自由,因为路由表的控制在前端,不管是要排序还是修改,都需要前端去修改,服务端只提供有权限的路由 keys
实现原理: 是通过接口动态生成路由表,且遵循一定的数据结构返回。前端根据需要处理该数据为可识别的结构,再通过 router.addRoutes
添加到路由实例,实现权限的动态生成。
优点: 所有的菜单控制都是通过服务端的接口返回,前端只负责渲染,后期维护成本降低,优先推荐此方式。
generateRoutes()
进行更改。接收的 type
参数,目前只是针对于本项目的模拟情况,如果不需要或者不适用,可自行改动。
generateRoutes(\n type: 'server' | 'frontEnd' | 'static',\n routers?: AppCustomRouteRecordRaw[] | string[]\n): Promise<unknown> {\n return new Promise<void>((resolve) => {\n let routerMap: AppRouteRecordRaw[] = []\n if (type === 'server') {\n // 模拟后端过滤菜单\n routerMap = generateRoutesByServer(routers as AppCustomRouteRecordRaw[])\n } else if (type === 'frontEnd') {\n // 模拟前端过滤菜单\n routerMap = generateRoutesByFrontEnd(cloneDeep(asyncRouterMap), routers as string[])\n } else {\n // 直接读取静态路由表\n routerMap = cloneDeep(asyncRouterMap)\n }\n // 动态路由,404一定要放到最后面\n this.addRouters = routerMap.concat([\n {\n path: '/:path(.*)*',\n redirect: '/404',\n name: '404Page',\n meta: {\n hidden: true,\n breadcrumb: false\n }\n }\n ])\n // 渲染菜单的所有路由\n this.routers = cloneDeep(constantRouterMap).concat(routerMap)\n resolve()\n })\n}\n
generateRoutesByFrontEnd ()
进行更改。目前本项目的前端权限控制,是根据 path
是否相同来进行过滤演示的,如果不符合需求,需要手动更改以下判断逻辑。// 前端控制路由生成\nexport const generateRoutesByFrontEnd = (\n routes: AppRouteRecordRaw[],\n keys: string[],\n basePath = '/'\n): AppRouteRecordRaw[] => {\n const res: AppRouteRecordRaw[] = [];\n\n for (const route of routes) {\n const meta = route.meta as RouteMeta;\n // skip some route\n if (meta.hidden && !meta.showMainRoute) {\n continue;\n }\n\n let data: Nullable<AppRouteRecordRaw> = null;\n\n let onlyOneChild: Nullable<string> = null;\n if (route.children && route.children.length === 1 && !meta.alwaysShow) {\n onlyOneChild = (\n isUrl(route.children[0].path)\n ? route.children[0].path\n : pathResolve(pathResolve(basePath, route.path), route.children[0].path)\n ) as string;\n }\n\n // 开发者可以根据实际情况进行扩展\n for (const item of keys) {\n // 通过路径去匹配\n if (isUrl(item) && (onlyOneChild === item || route.path === item)) {\n data = Object.assign({}, route);\n } else {\n const routePath = pathResolve(basePath, onlyOneChild || route.path);\n if (routePath === item || meta.followRoute === item) {\n data = Object.assign({}, route);\n }\n }\n }\n\n // recursive child routes\n if (route.children && data) {\n data.children = generateRoutesByFrontEnd (route.children, keys, pathResolve(basePath, data.path));\n }\n if (data) {\n res.push(data as AppRouteRecordRaw);\n }\n }\n return res;\n};\n
generateRoutesByServer ()
进行更改。// 后端控制路由生成\nexport const generateRoutesByServer = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {\n const res: AppRouteRecordRaw[] = [];\n\n for (const route of routes) {\n const data: AppRouteRecordRaw = {\n path: route.path,\n name: route.name,\n redirect: route.redirect,\n meta: route.meta,\n };\n if (route.component) {\n const comModule =\n modules[`../${route.component}.vue`] || modules[`../${route.component}.tsx`];\n const component = route.component as string;\n if (!comModule && !component.includes('#')) {\n console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`);\n } else {\n // 动态加载路由文件,可根据实际情况进行自定义逻辑\n data.component =\n component === '#' ? Layout : component.includes('##') ? getParentLayout() : comModule;\n }\n }\n // recursive child routes\n if (route.children) {\n data.children = generateRoutesByServer (route.children);\n }\n res.push(data as AppRouteRecordRaw);\n }\n return res;\n};\n
getRole()
进行更改。需要开发者自行根据需求进行代码变更。
// 获取角色信息\nconst getRole = async () => {\n const formData = await getFormData<UserType>()\n const params = {\n roleName: formData.username\n }\n const res =\n appStore.getDynamicRouter && appStore.getServerDynamicRouter\n ? await getAdminRoleApi(params)\n : await getTestRoleApi(params)\n if (res) {\n const routers = res.data || []\n setStorage('roleRouters', routers)\n appStore.getDynamicRouter && appStore.getServerDynamicRouter\n ? await permissionStore.generateRoutes('server', routers).catch(() => {})\n : await permissionStore.generateRoutes('frontEnd', routers).catch(() => {})\n\n permissionStore.getAddRouters.forEach((route) => {\n addRoute(route as RouteRecordRaw) // 动态添加可访问路由表\n })\n permissionStore.setIsAddRouters(true)\n push({ path: redirect.value || permissionStore.addRouters[0].path })\n }\n};\n
// 开发者可根据实际情况进行修改\nconst roleRouters = getStorage('roleRouters') || []\n\n// 是否使用动态路由\nif (appStore.getDynamicRouter) {\n appStore.serverDynamicRouter\n ? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[])\n : await permissionStore.generateRoutes('frontEnd', roleRouters as string[])\n } else {\n await permissionStore.generateRoutes('static')\n}\n\npermissionStore.getAddRouters.forEach((route) => {\n router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表\n})\nconst redirectPath = from.query.redirect || to.path\nconst redirect = decodeURIComponent(redirectPath as string)\nconst nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }\npermissionStore.setIsAddRouters(true)\nnext(nextData)\n
有时候,我们并不需要动态路由,那么可以在 src/config/app.ts
中把 dynamicRouter
设置为 false
,这样我们取得都是项目中的静态路由表了。
内部逻辑已经处理了静态路由的部分,所以可以无需关心其他。
',30);p.render=function(a,t,p,e,c,u){return n(),s("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/guide_auth.md.3188bb51.lean.js b/assets/guide_auth.md.3188bb51.lean.js new file mode 100644 index 00000000..407909f3 --- /dev/null +++ b/assets/guide_auth.md.3188bb51.lean.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.7e863e47.js";const t='{"title":"权限","description":"","frontmatter":{},"headers":[{"level":2,"title":"前端控制权限","slug":"前端控制权限"},{"level":2,"title":"后台动态获取","slug":"后台动态获取"},{"level":2,"title":"实现","slug":"实现"},{"level":3,"title":"前端控制实现","slug":"前端控制实现"},{"level":3,"title":"后台动态获取","slug":"后台动态获取-1"},{"level":3,"title":"公用部分修改","slug":"公用部分修改"},{"level":2,"title":"静态路由(无权限)","slug":"静态路由(无权限)"}],"relativePath":"guide/auth.md","lastUpdated":1718353615651}',p={},o=a('',30);p.render=function(a,t,p,e,c,u){return n(),s("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/guide_component.md.e230104c.js b/assets/guide_component.md.e230104c.js new file mode 100644 index 00000000..923cdb65 --- /dev/null +++ b/assets/guide_component.md.e230104c.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.7e863e47.js";const t='{"title":"组件注册","description":"","frontmatter":{},"headers":[{"level":2,"title":"按需引入","slug":"按需引入"},{"level":3,"title":"tsx 文件注册","slug":"tsx-文件注册"},{"level":2,"title":"全局注册","slug":"全局注册"}],"relativePath":"guide/component.md","lastUpdated":1718353615651}',p={},e=a('项目目前的组件注册机制是按需注册,是在需要用到的页面才引入。
<script setup lang="ts">\nimport { ElBacktop } from 'element-plus'\nimport { useDesign } from '@/hooks/web/useDesign'\n\nconst { getPrefixCls, variables } = useDesign()\n\nconst prefixCls = getPrefixCls('backtop')\n</script>\n\n<template>\n <ElBacktop\n :class="`${prefixCls}-backtop`"\n :target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"\n />\n</template>\n\n
tsx 文件内不能使用全局注册组件,需要手动引入组件使用。
如果觉得按需引入太麻烦,可以进行全局注册,在src/components/index.ts,添加需要注册的组件。
目前只有 Icon
组件进行了全局注册。
import type { App } from 'vue'\nimport { Icon } from './Icon'\n\nexport const setupGlobCom = (app: App<Element>): void => {\n app.component('Icon', Icon)\n}\n\n
如果 element-plus
的组件需要全局注册,在 src/plugins/elementPlus/index.ts 添加需要注册的组件。
目前 element-plus
中只有 ElLoading
与 ElScrollbar
进行了全局注册。
import type { App } from 'vue'\n\n// 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题\nimport { ElLoading, ElScrollbar } from 'element-plus'\n\nconst plugins = [ElLoading]\n\nconst components = [ElScrollbar]\n\nexport const setupElementPlus = (app: App) => {\n plugins.forEach((plugin) => {\n app.use(plugin)\n })\n\n components.forEach((component) => {\n app.component(component.name, component)\n })\n}\n\n
前言
由于是展示项目,所以打包后相对较大,如果项目中没有用到的插件,可以删除对应的文件或者路由,不引用即可,没有引用就不会打包。
项目开发完成之后,执行以下命令进行构建
构建打包成功之后,会在根目录生成 dist-* 文件夹,里面就是构建打包好的文件。
发布之前可以在本地进行预览
不能直接打开构建后的 html 文件
使用项目自定的命令进行预览(推荐)
# 先打包在进行预览\n\n# 预览开发环境\npnpm run serve:dev\n\n# 预览测试环境\npnpm run serve:test\n\n# 预览生产环境\npnpm run serve:pro\n
注意
项目默认是在生产环境开启 Mock,这样做非常不好,只是为了演示环境有数据,不建议在生产环境使用 Mock,而应该使用真实的后台接口。
简单的部署只需要将最终生成的静态文件,dist-* 文件夹的静态文件发布到你的 cdn 或者静态服务器即可。
部署时可能会发现资源路径不对,只需要修改对应的.env.xxx
文件即可。
# 根据自己路径来配置更改\nVITE_BASE_PATH = /dist-dev/\n
主要介绍如何在项目中使用和规划样式文件。
默认使用 less
作为预处理语言,建议在使用前或者遇到疑问时学习一下 Less 的相关特性。
项目中使用的通用样式,都存放于 src/style/ 下面。
.\n├── index.less # 入口\n├── theme.less # 主题相关\n├── var.css # css变量\n└── variables.module.less # less变量\n\n
全局注入
variables.module.less 这个文件会被全局注入到所有文件,所以在页面内可以直接使用变量而不需要手动引入。
var.css 则是注入到根元素,所以在每个地方也都能用到。
项目中使用了 unocss,具体参见文件使用说明。
可能没有用到人会觉得用起来很不习惯,但就个人而言,用起来还是挺香的。减少了很多不必要的麻烦
语法如下:
<div class="relative w-full h-full px-4"></div>\n
提示
列举了一些常见的问题。有问题可以先来这里寻找,看是否有相关解答,没有的话可以上 issue 中提问或者搜索
因为项目中有的 Store 默认开始了持久化,所以不管你修没修改默认值,都会优先默认取缓存中的值,所以如果修改完默认值之后,还请手动清除下浏览器的 localStorage
,默认值就会生效了。
本地运行之后,会出现路由警告
[Vue Router warn]: No match found for location with path "/authorization/menu"\n
这个无需关心,是vue-router的问题,项目打包上线后是不会有次警告,所以该问题可以忽略。
请自行去百度下 vite 快是怎么个快法,本地运行启动,都是按需加载,一次性加载了几十个资源,当然会比较慢,有了缓存之后非首次就会实现秒开了。
目前项目中已经对于启动时间进行了优化,本地默认加载了全部的 element-plus
的样式文件,会多多少少减少请求资源数量。
启动快慢还是得根据当前文件引用的资源数量来决定。
这是因为你在该路由中使用了第三方模块,这个模块是没有预加载的,所以需要重新去加载这个模块,然后就会出现 page reload
,极大的影响了开发体验,所以可以在 vite.config.ts
中去配置预加载列表:optimizeDeps.include
,这样在服务启动的使用,会先把这些模块给预先加载打包。
pnpm-lock
和 node_modules
,然后重新运行 pnpm i
由于完整版引入了许多第三方模块,所以打包体积会比较大,可以自行删除不需要的第三方模块,或者使用精简版(mini分支)来进行开发。
合理的进行拆包,目前项目中对一些比较大的第三方模块进行了拆包处理。
菜单是根据路由配置来生成,请先看下已有的路由配置是否可以满足你的需要,如果不满足,可以自行去定制化。可以查看路由相关文档
在使用组件的时候,遇到问题,可以先看下对应的在线例子,看是否有对应的代码,基本上覆盖了95%
的使用方式,或者查看对应的组件文档。
项目中大部分使用了 tsx
,所以原先 template
的一些代码规范就不适用了,如 v-if
得使用 {判断条件 ? 成立 : 不成立}
来进行显示隐藏,可以查阅下相关文档。
并且请确保如果要使用 tsx
语法, script
是否声明了 lang="tsx"
如果是在项目中直接添加静态路由,需要确保 appStore 中的 dynamicRouter
和 serverDynamicRouter
为 false
,并且手动清除下浏览器的 localStorage
这是 Volar
插件的问题,一般重启下编辑器即可生效。
设置 VITE_USE_ONLINE_ICON=false ,可能在有的版本设置之后会无效,是因为有BUG,可以复制最新版本的 uno.config.ts
和 Icon.vue
的最新代码。
本文将快速的帮助你从头运行并启动项目。
为什么使用 Pnpm,而不是用其他包管理器,大家可以搜索一下,这里就不做过多的阐述了。
注意
14.x
以上,这里推荐 16.x
及以上。如果你使用的 IDE 是vscode的话,可以安装以下工具来提高开发效率及代码格式化:
注意
注意存放代码的目录及所有父级目录不能存在中文、韩文、日文以及空格,否则安装依赖后启动会出错。
# clone 代码\ngit clone https://github.com/kailong321200875/vue-element-plus-admin.git\n\n
git clone https://gitee.com/kailong110120130/vue-element-plus-admin.git\n
如果您电脑未安装Node.js,请安装它,推荐 18.x
及以上
验证
# 验证 npm 是否安装成功\nnpm -v\n\n# 验证 node 是否安装成功\nnode -v\n
如果你需要同时存在多个 node
版本,可以使用 Nvm 或者其他工具进行 Node.js 进行版本管理。
推荐使用 Pnpm进行依赖安装(若其他包管理器安装不了需要自行处理)。
如果未安装 Pnpm
,可以用下面命令来进行全局安装
# 全局安装 pnpm\nnpm i -g pnpm\n\n# 验证\npnpm -v\n
在项目根目录下,打开命令窗口执行,耐心等待安装完成即可
# 安装依赖\npnpm i\n
安装依赖时 husky 安装失败
请查看你的源码是否从 Github 或者 Gitee 直接下载的,直接下载是没有 .git
文件夹的,而 husky
需要依赖 git
才能安装。此时需使用 git init
初始化项目,再尝试重新安装即可。
当依赖安装完成后,执行以下命令即可启动项目:
pnpm run dev\n
"scripts": {\n # 安装依赖\n "i": "pnpm install",\n # 本地开发环境运行\n "dev": "vite --mode base",\n # typeScript 检测\n "ts:check": "vue-tsc --noEmit",\n # 打包生产环境\n "build:pro": "vite build --mode pro",\n # 打包开发环境\n "build:dev": "npm run ts:check && vite build --mode dev",\n # 打包测试环境\n "build:test": "npm run ts:check && vite build --mode test",\n # 本地预览 已打包的生产环境项目包\n "serve:pro": "vite preview --mode pro",\n # 本地预览 已打包的开发环境项目包\n "serve:dev": "vite preview --mode dev",\n # 本地预览 已打包的测试环境项目包\n "serve:test": "vite preview --mode test",\n # 检测可更新依赖\n "npm:check": "npx npm-check-updates",\n # 删除 node_modules\n "clean": "npx rimraf node_modules",\n # 删除 缓存\n "clean:cache": "npx rimraf node_modules/.cache",\n # eslint 检测\n "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",\n # eslint 格式化\n "lint:format": "prettier --write --loglevel warn \\"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\\"",\n # stylelint 格式化\n "lint:style": "stylelint --fix \\"**/*.{vue,less,postcss,css,scss}\\" --cache --cache-location node_modules/.cache/stylelint/",\n "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",\n "lint:pretty": "pretty-quick --staged",\n "postinstall": "husky install",\n # 快速生成统一规范的模块\n "p": "plop"\n},\n
注意
vue-element-plus-admin 是一个基于 element-plus 免费开源的中后台模版。使用了最新的 Vue3,Vite,Typescript等主流技术开发,开箱即用的中后台前端解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。
vue-element-plus-admin 的定位是后台集成方案,因为集成了很多你可能用不到的功能,会造成不少的代码冗余。如果你的项目不关注这方面的问题,也可以直接基于它进行二次开发。
',5),i=n('如需要基础模版,请切换到 mini 分支,mini 只简单集成了一些如:布局、动态菜单等常用布局功能,更适合开发者进行二次开发。
本项目需要一定前端基础知识,请确保掌握 Vue 的基础知识,以便能处理一些常见的问题。
为了能快速上手本项目,请先大致浏览一遍文档及在线示例。
建议在开发前先学一下以下内容,提前了解和学习这些知识,会对项目理解非常有帮助:
.\n├── .github # github workflows 相关\n├── .husky # husky 配置\n├── .vscode # vscode 配置\n├── mock # 自定义 mock 数据及配置\n├── public # 静态资源\n├── src # 项目代码\n│ ├── api # api接口管理\n| |── axios # axios配置\n│ ├── assets # 静态资源\n│ ├── components # 公用组件\n│ ├── constants # 存放常量\n│ ├── hooks # 常用hooks\n│ ├── layout # 布局组件\n│ ├── locales # 语言文件\n│ ├── plugins # 外部插件\n│ ├── router # 路由配置\n│ ├── store # 状态管理\n│ ├── styles # 全局样式\n│ ├── utils # 全局工具类\n│ ├── views # 路由页面\n│ ├── App.vue # 入口vue文件\n│ ├── main.ts # 主入口文件\n│ └── permission.ts # 路由拦截\n├── types # 全局类型\n├── .env.base # 本地开发环境 环境变量配置\n├── .env.dev # 打包到开发环境 环境变量配置\n├── .env.gitee # 针对 gitee 的环境变量 可忽略\n├── .env.pro # 打包到生产环境 环境变量配置\n├── .env.test # 打包到测试环境 环境变量配置\n├── .eslintignore # eslint 跳过检测配置\n├── .eslintrc.js # eslint 配置\n├── .gitignore # git 跳过配置\n├── .prettierignore # prettier 跳过检测配置\n├── .stylelintignore # stylelint 跳过检测配置\n├── .versionrc 自动生成版本号及更新记录配置\n├── CHANGELOG.md # 更新记录\n├── commitlint.config.js # git commit 提交规范配置\n├── index.html # 入口页面\n├── package.json\n├── .postcssrc.js # postcss 配置\n├── prettier.config.js # prettier 配置\n├── README.md # 英文 README\n├── README.zh-CN.md # 中文 README\n├── stylelint.config.js # stylelint 配置\n├── tsconfig.json # typescript 配置\n├── vite.config.ts # vite 配置\n└── uno.config.ts # unocss 配置\n
本地开发推荐使用Chrome 最新版
浏览器。
由于 Vue 3 不再支持 IE11,本项目也不支持 IE。
IE | Edge | Firefox | Chrome | Safari |
---|---|---|---|---|
not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
如果前端应用和后端接口服务器没有运行在同一个主机上,你需要在开发环境下将接口请求代理到接口服务器。
如果是同一个主机,可以直接请求具体的接口地址。
在 vite.config.ts
配置文件中,提供了 server 的 proxy 功能,用于代理 API 请求。
server: {\n proxy: {\n "/api":{\n target: 'http://localhost:3000',\n changeOrigin: true,\n ws: true,\n rewrite: (path) => path.replace(new RegExp(`^/api`), ''),\n }\n },\n},\n
配置接口前缀,可以在对应的 env
文件中,修改 VITE_API_BASE_PATH
的值
注意
该配置只能作用于 本地开发环境。
从浏览器控制台的 Network 看,请求是 http://localhost:3000/api/xxx
,这是因为 proxy 配置不会改变本地请求的 url。
在本项目中,所有的接口数据都是使用 Mock
模拟
接口统一存放于 src/api/ 下面管理
以获取列表接口为例:
在 src/api/ 内新建模块文件,其中参数与返回值最好定义一下类型,方便校验。虽然麻烦,但是后续维护字段很方便。
提示
类型定义文件可以抽取出去统一管理,具体参考项目
import request from '@/axios'\nimport type { TableData } from './types'\n\nexport const getTableListApi = (params: any) => {\n return request.get({ url: '/example/list', params })\n}\n\nexport const getTreeTableListApi = (params: any) => {\n return request.get({ url: '/example/treeList', params })\n}\n\nexport const saveTableApi = (data: Partial<TableData>): Promise<IResponse> => {\n return request.post({ url: '/example/save', data })\n}\n\nexport const getTableDetApi = (id: string): Promise<IResponse<TableData>> => {\n return request.get({ url: '/example/detail', params: { id } })\n}\n\nexport const delTableListApi = (ids: string[] | number[]): Promise<IResponse> => {\n return request.post({ url: '/example/delete', data: { ids } })\n}\n\n
axios 请求封装存放于 src/axios 中。
axios 全局配置放在 src/constants 中。
注意
更改之后,将影响所有的请求。
/**\n * 请求成功状态码\n */\nexport const SUCCESS_CODE = 0\n\n/**\n * 请求contentType\n */\nexport const CONTENT_TYPE = 'application/json'\n\n/**\n * 请求超时时间\n */\nexport const REQUEST_TIMEOUT = 60000\n
Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发进程所阻塞。
本项目使用 vite-mock-plugin 来进行 mock 数据处理。项目内 mock 服务分本地和线上。
本地 mock 采用 Node.js 中间件进行参数拦截(不采用 mock.js 的原因是本地开发看不到请求参数和响应结果)。
如果你想添加 mock 数据,只要在根目录下找到 mock 文件,添加对应的接口,对其进行拦截和模拟数据。
在 mock 文件夹内新建文件
TIP
文件新增后会自动更新,不需要手动重启,可以在代码控制台查看日志信息 mock 文件夹内会自动注册
TIP
mock 的值可以直接使用 mock.js 的语法。
可以在对应的 env
文件中设置 VITE_USE_MOCK
为 false
,如果想要更彻底一点,可以在vite.config.ts中删除 viteMockServe
对应的代码。
由于该项目是一个展示类项目,线上也是用 mock 数据,所以在打包后同时也集成了 mock。通常项目线上一般为正式接口。
项目线上 mock 采用的是 mock.js 进行 mock 数据模拟。
',37);p.render=function(a,t,p,e,c,l){return s(),n("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/guide_mock.md.23e13f26.lean.js b/assets/guide_mock.md.23e13f26.lean.js new file mode 100644 index 00000000..13452730 --- /dev/null +++ b/assets/guide_mock.md.23e13f26.lean.js @@ -0,0 +1 @@ +import{o as s,c as n,a}from"./app.7e863e47.js";const t='{"title":"数据mock&联调","description":"","frontmatter":{},"headers":[{"level":2,"title":"开发环境","slug":"开发环境"},{"level":3,"title":"跨域设置","slug":"跨域设置"},{"level":2,"title":"接口请求","slug":"接口请求"},{"level":2,"title":"axios 配置","slug":"axios-配置"},{"level":3,"title":"全局 axios 配置说明","slug":"全局-axios-配置说明"},{"level":2,"title":"Mock 服务","slug":"mock-服务"},{"level":3,"title":"本地 Mock","slug":"本地-mock"},{"level":3,"title":"线上 mock","slug":"线上-mock"}],"relativePath":"guide/mock.md","lastUpdated":1718353615651}',p={},o=a('',37);p.render=function(a,t,p,e,c,l){return s(),n("div",null,[o])};export default p;export{t as __pageData}; diff --git a/assets/guide_router.md.cc1fa32a.js b/assets/guide_router.md.cc1fa32a.js new file mode 100644 index 00000000..5c7ea5aa --- /dev/null +++ b/assets/guide_router.md.cc1fa32a.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.7e863e47.js";const t='{"title":"路由","description":"","frontmatter":{},"headers":[{"level":2,"title":"配置","slug":"配置"},{"level":3,"title":"如何添加新配置","slug":"如何添加新配置"},{"level":3,"title":"多级路由","slug":"多级路由"},{"level":3,"title":"外链","slug":"外链"},{"level":2,"title":"图标","slug":"图标"},{"level":2,"title":"多标签页","slug":"多标签页"},{"level":3,"title":"如何开启页面缓存","slug":"如何开启页面缓存"},{"level":3,"title":"如何让某个页面不缓存","slug":"如何让某个页面不缓存"},{"level":2,"title":"默认跳转地址","slug":"默认跳转地址"}],"relativePath":"guide/router.md","lastUpdated":1718353615651}',p={},o=a('项目路由配置存放于 src/router/index.ts 中。
为了方便阅读和查找,目前项目中并没有去对路由进行拆分,而是统一写在了一起,如果需要拆分,可自行更改。
因为路由是生成菜单关键,所以本项目中对路由提供了以下配置,方便开发者进行定制。
/**\n* redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击\n* name:'router-name' 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题\n* meta : {\n hidden: true 当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false)\n\n alwaysShow: true 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式,\n 只有一个时,会将那个子路由当做根路由显示在侧边栏,\n 若你想不管路由下面的 children 声明的个数都显示你的根路由,\n 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,\n 一直显示根路由(默认 false)\n\n title: 'title' 设置该路由在侧边栏和面包屑中展示的名字\n\n icon: 'svg-name' 设置该路由的图标\n\n noCache: true 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)\n\n breadcrumb: false 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)\n\n affix: true 如果设置为true,则会一直固定在tag项中(默认 false)\n\n noTagsView: true 如果设置为true,则不会出现在tag中(默认 false)\n\n activeMenu: '/dashboard' 显示高亮的路由路径\n\n canTo: true 设置为true即使hidden为true,也依然可以进行路由跳转(默认 false)\n\n permission: ['edit','add', 'delete'] 设置该路由的权限\n }\n**/\n
如果本项目中的路由配置项,满足不了你当前的开发工作,可以自行添加新的属性。
注意
所有的路由项配置,都必须放在 meta
中。
在 types/router.d.ts 中添加对应的类型,之后就可以在路由中添加你需要的配置项了。
declare module 'vue-router' {\n interface RouteMeta extends Record<string | number | symbol, unknown> {\n hidden?: boolean\n alwaysShow?: boolean\n title?: string\n icon?: string\n noCache?: boolean\n breadcrumb?: boolean\n affix?: boolean\n activeMenu?: string\n noTagsView?: boolean\n canTo?: boolean\n permission?: string[]\n\n // 添加新的配置类型\n ...\n }\n}\n\n
注意事项
name
不能重复/
,其余子路由都不要以/
开头示例
{\n path: '/level',\n component: Layout,\n redirect: '/level/menu1/menu1-1/menu1-1-1',\n name: 'Level',\n meta: {\n title: t('router.level'),\n icon: 'carbon:skill-level-advanced'\n },\n children: [\n {\n path: 'menu1',\n name: 'Menu1',\n component: getParentLayout(),\n redirect: '/level/menu1/menu1-1/menu1-1-1',\n meta: {\n title: t('router.menu1')\n },\n children: [\n {\n path: 'menu1-1',\n name: 'Menu11',\n component: getParentLayout(),\n redirect: '/level/menu1/menu1-1/menu1-1-1',\n meta: {\n title: t('router.menu11'),\n alwaysShow: true\n },\n children: [\n {\n path: 'menu1-1-1',\n name: 'Menu111',\n component: () => import('@/views/Level/Menu111.vue'),\n meta: {\n title: t('router.menu111')\n }\n }\n ]\n },\n {\n path: 'menu1-2',\n name: 'Menu12',\n component: () => import('@/views/Level/Menu12.vue'),\n meta: {\n title: t('router.menu12')\n }\n }\n ]\n },\n {\n path: 'menu2',\n name: 'Menu2Demo',\n component: () => import('@/views/Level/Menu2.vue'),\n meta: {\n title: t('router.menu2')\n }\n }\n ]\n}\n\n
只需要将 path
设置为需要跳转的HTTP 地址即可。
{\n path: '/external-link',\n component: Layout,\n meta: {\n name: 'ExternalLink'\n },\n children: [\n {\n path: 'https://github.com/kailong321200875/vue-element-plus-admin-doc',\n meta: { name: 'Link', title: '文档' }\n }\n ]\n}\n
这里的 icon
配置,会同步到 菜单(icon 的值可以查看此处)。
标签页使用的是 keep-alive
和 router-view
实现,实现切换 tab
后还能保存切换之前的状态。
开启缓存有 2 个条件
name
,且不能重复name
,与路由设置的 name
保持一致{\n path: 'menu2',\n name: 'Menu2',\n component: () => import('@/views/Level/Menu2.vue'),\n meta: {\n title: t('router.menu2')\n }\n}\n\n// /@/views/Level/Menu2.vue\n<script setup lang="ts">\ndefineOptions({\n name: 'Menu2'\n})\n</script>\n\n
注意
keep-alive 生效的前提是:需要将路由的 name
属性及对应的页面的 name
设置成一样。因为:
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
可以将 noCache
配置成 true
即可关闭缓存或者组件不添加 name
属性。
{\n path: 'workplace',\n component: () => import('@/views/Dashboard/Workplace.vue'),\n name: 'Workplace',\n meta: {\n title: t('router.workplace'),\n noCache: true\n }\n}\n
目前项目中,登录进来,默认是进入到当前第一个能找到的路由页面。
后续会考虑弄成一个配置项出来。
',33);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.cc1fa32a.lean.js b/assets/guide_router.md.cc1fa32a.lean.js new file mode 100644 index 00000000..b43331bf --- /dev/null +++ b/assets/guide_router.md.cc1fa32a.lean.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.7e863e47.js";const t='{"title":"路由","description":"","frontmatter":{},"headers":[{"level":2,"title":"配置","slug":"配置"},{"level":3,"title":"如何添加新配置","slug":"如何添加新配置"},{"level":3,"title":"多级路由","slug":"多级路由"},{"level":3,"title":"外链","slug":"外链"},{"level":2,"title":"图标","slug":"图标"},{"level":2,"title":"多标签页","slug":"多标签页"},{"level":3,"title":"如何开启页面缓存","slug":"如何开启页面缓存"},{"level":3,"title":"如何让某个页面不缓存","slug":"如何让某个页面不缓存"},{"level":2,"title":"默认跳转地址","slug":"默认跳转地址"}],"relativePath":"guide/router.md","lastUpdated":1718353615651}',p={},o=a('',33);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_settings.md.f6473247.js b/assets/guide_settings.md.f6473247.js new file mode 100644 index 00000000..c742c9e9 --- /dev/null +++ b/assets/guide_settings.md.f6473247.js @@ -0,0 +1 @@ +import{o as n,c as s,a}from"./app.7e863e47.js";const e='{"title":"项目配置项","description":"","frontmatter":{},"headers":[{"level":2,"title":"环境变量配置","slug":"环境变量配置"},{"level":3,"title":"配置项说明","slug":"配置项说明"},{"level":3,"title":".env.base","slug":"env-base"},{"level":3,"title":".env.dev","slug":"env-dev"},{"level":3,"title":".env.test","slug":"env-test"},{"level":3,"title":".env.pro","slug":"env-pro"},{"level":2,"title":"项目及主题配置","slug":"项目及主题配置"},{"level":3,"title":"配置文件路径","slug":"配置文件路径"},{"level":3,"title":"说明","slug":"说明"},{"level":3,"title":"如何添加新属性","slug":"如何添加新属性"},{"level":2,"title":"多语言配置","slug":"多语言配置"},{"level":2,"title":"样式配置","slug":"样式配置"},{"level":3,"title":"css 前缀设置","slug":"css-前缀设置"},{"level":3,"title":"前缀使用","slug":"前缀使用"}],"relativePath":"guide/settings.md","lastUpdated":1718353615651}',p={},t=a('本文将介绍一些常用的项目配置,方便开发者可以根据需求进行定制化改造。
项目的环境变量配置位于项目根目录下的,这里主要配置四个个环境变量,分别为:
在开发调试的时候,会读取 .env.base
里面的数据。其他环境亦是如此,根据打包命令的不同,来读取不同的环境变量。
也许你会疑惑,为什么会有多个环境变量?
以 生产环境
为例,当我们执行 pnpm run build:pro
时,输出的包是用于线上环境的,所以代码都应该是压缩,我们需要删除掉代码中的 console.log
和 degubber
,保证打包后代码的整洁度和不可见性。而其他环境,所以应该保留 console.log
和 degubber
用于调试,这样才能快速定位到问题所在。
所以环境变量的作用就是为了,在不同环境下有不同的表现。
提示
VITE_
开头的变量会被嵌入到项目中,你可以项目代码中这样访问它们:console.log(import.meta.env.VITE_APP_TITLE)\n
本地开发环境适用
# 环境\nNODE_ENV = development\n\n# 接口前缀\nVITE_API_BASEPATH = base\n\n# 打包路径\nVITE_BASE_PATH = /\n\n# 标题\nVITE_APP_TITLE = ElementAdmin\n
开发环境适用
# 环境\nNODE_ENV = production\n\n# 接口前缀\nVITE_API_BASEPATH = dev\n\n# 打包路径\nVITE_BASE_PATH = /dist-dev/\n\n# 是否删除debugger\nVITE_DROP_DEBUGGER = false\n\n# 是否删除console.log\nVITE_DROP_CONSOLE = false\n\n# 是否sourcemap\nVITE_SOURCEMAP = true\n\n# 输出路径\nVITE_OUT_DIR = dist-dev\n\n# 标题\nVITE_APP_TITLE = ElementAdmin\n\n
测试环境适用
# 环境\nNODE_ENV = production\n\n# 接口前缀\nVITE_API_BASEPATH = test\n\n# 打包路径\nVITE_BASE_PATH = /dist-test/\n\n# 是否删除debugger\nVITE_DROP_DEBUGGER = false\n\n# 是否删除console.log\nVITE_DROP_CONSOLE = false\n\n# 是否sourcemap\nVITE_SOURCEMAP = true\n\n# 输出路径\nVITE_OUT_DIR = dist-test\n\n
生产环境适用
# 环境\nNODE_ENV = production\n\n# 接口前缀\nVITE_API_BASEPATH = pro\n\n# 打包路径\nVITE_BASE_PATH = /\n\n# 是否删除debugger\nVITE_DROP_DEBUGGER = true\n\n# 是否删除console.log\nVITE_DROP_CONSOLE = true\n\n# 是否sourcemap\nVITE_SOURCEMAP = false\n\n# 输出路径\nVITE_OUT_DIR = dist-pro\n\n# 标题\nVITE_APP_TITLE = ElementAdmin\n\n
提示
项目配置文件用于配置项目内展示的内容、布局、主题色等效果。
修改完之后,会添加到全局的状态管理中,方便其他地方使用。
export const appModules: AppState = {\n sizeMap: ['default', 'large', 'small'],\n mobile: false, // 是否是移动端\n title: import.meta.env.VITE_APP_TITLE as string, // 标题\n pageLoading: false, // 路由跳转loading\n\n breadcrumb: true, // 面包屑\n breadcrumbIcon: true, // 面包屑图标\n collapse: false, // 折叠菜单\n hamburger: true, // 折叠图标\n screenfull: true, // 全屏图标\n size: true, // 尺寸图标\n locale: true, // 多语言图标\n tagsView: true, // 标签页\n logo: true, // logo\n fixedHeader: true, // 固定toolheader\n footer: true, // 显示页脚\n greyMode: false, // 是否开始灰色模式,用于特殊悼念日\n\n layout: wsCache.get('layout') || 'classic', // layout布局\n isDark: wsCache.get('isDark') || false, // 是否是暗黑模式\n currentSize: wsCache.get('default') || 'default', // 组件尺寸\n theme: wsCache.get('theme') || {\n // 主题色\n elColorPrimary: '#409eff',\n // 左侧菜单边框颜色\n leftMenuBorderColor: 'inherit',\n // 左侧菜单背景颜色\n leftMenuBgColor: '#001529',\n // 左侧菜单浅色背景颜色\n leftMenuBgLightColor: '#0f2438',\n // 左侧菜单选中背景颜色\n leftMenuBgActiveColor: 'var(--el-color-primary)',\n // 左侧菜单收起选中背景颜色\n leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',\n // 左侧菜单字体颜色\n leftMenuTextColor: '#bfcbd9',\n // 左侧菜单选中字体颜色\n leftMenuTextActiveColor: '#fff',\n // logo字体颜色\n logoTitleTextColor: '#fff',\n // logo边框颜色\n logoBorderColor: 'inherit',\n // 头部背景颜色\n topHeaderBgColor: '#fff',\n // 头部字体颜色\n topHeaderTextColor: 'inherit',\n // 头部悬停颜色\n topHeaderHoverColor: '#f6f6f6',\n // 头部边框颜色\n topToolBorderColor: '#eee'\n }\n}\n
如果想要添加新的全局配置属性,需要在 src/store/modules/app.ts 中 AppState
添加对应的类型,并在 appModules
对象中,赋予新属性的默认值。
用于配置多语言信息
在 src/store/modules/locale.ts 内配置
import { useCache } from '@/hooks/web/useCache'\nimport zhCn from 'element-plus/lib/locale/lang/zh-cn'\nimport en from 'element-plus/lib/locale/lang/en'\n\nconst { wsCache } = useCache()\n\nexport const elLocaleMap = {\n 'zh-CN': zhCn,\n en: en\n}\nexport interface LocaleState {\n currentLocale: LocaleDropdownType\n localeMap: LocaleDropdownType[]\n}\n\nexport const localeModules: LocaleState = {\n currentLocale: {\n lang: wsCache.get('lang') || 'zh-CN',\n elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']\n },\n // 多语言\n localeMap: [\n {\n lang: 'zh-CN',\n name: '简体中文'\n },\n {\n lang: 'en',\n name: 'English'\n }\n ]\n}\n\n
用于修改项目内组件及 element-plus
组件的 class
前缀。
由于 element-plus
的组件还没有全部采用动态配置前缀,所以目前还是使用 el
前缀。
// 命名空间\n@namespace: v;\n// el命名空间\n@elNamespace: el;\n\n// 导出变量\n:export {\n namespace: @namespace;\n elNamespace: @elNamespace;\n}\n\n
在 css 内
<style lang="less" scoped>\n /* namespace已经全局注入,不需要额外在引入 */\n @prefix-cls: ~'@{namespace}-app';\n\n .@{prefix-cls} {\n width: 100%;\n }\n</style>\n
在 vue/ts 内
import { useDesign } from '/@/hooks/web/useDesign'\n\nconst { prefixCls } = useDesign('app')\n\n// prefixCls => v-app\n
由于 WindiCss 不再维护,所以换成了 unocss, 两者在用法上保持了大部分的一致性,但还是有些地方有特别的差异性,对于 v1 版本需要升级到 unocss 话,需要有一定的改造成本。
所以建议 v1 还是继续使用 WindiCss
v2 版本还是保留了四种布局风格,只是在细节上的把控会比 v1 好,主要体现在一些边框重叠的优化上。
v2 版本升级了 typescript5,在用法上基本上没有区别,只是针对了项目中的一些类型的规范进行了更改,使项目的代码更规范化。
v2 版本最主要的更新,就是组件上的更新
主要体现在了 Form
、 Table
、 Search
、Descriptions
的重构上。
在 V1 版本中,以上四个组件在使用上有许多不足的地方,灵活度不够,扩展性不强而被诟病。
所以在 v2 版本中,以上四个组件,schema
全部采用了 tsx
的书写方式,如果定制化比较多的话,tsx
会比 template
更有优势。
同时,以上四个组件支持嵌套绑定,如 Form
的数据绑定,v1 版本只支持一层嵌套,比较局限,在 v2 版本中,支持 xxx.xxx
的绑定方式。
如果用法比较简单的话,也是支持 template
,不过这里还是推荐使用 tsx
,避免之后扩展带来的负担。
v2 版本丰富了在线例子,如果 权限管理
,后续也会继续持续更新更多的例子来让各位客官可以更快速的了解和使用。
注意
如果 v1 版本已经项目落地,或者已经使用了一段时间,建议还是继续使用 v1 版本,刚开始使用的话,可以直接使用 v2 版本
由于两个版本的不兼容,这里是不推荐进行升级。
剪切板
useClipboard 位于 src/hooks/web/useClipboard.ts
<script setup lang="ts">\nimport { useClipboard } from '@/hooks/web/useClipboard'\n\nconst { copy } = useClipboard()\n\ncopy('复制内容')\n</script>\n\n
const { copy, copied, text, isSupported } = useClipboard()\n
copy
copy
复制,参数传入一个需要复制的内容
copied
copied
是否已复制
text
text
复制的内容
isSupported
isSupported
浏览器是否支持复制
统一生成 Search
、 Form
、 Descriptions
、 Table
组件所需要的数据结构。
由于以上四个组件都需要 Sechema
或者 columns
的字段,如果每个组件都写一遍的话,会造成大量重复代码,所以提供 useCrudSchemas
来进行统一的数据生成。
useCrudSchemas 位于 src/hooks/web/useCrudSchemas.ts
TIP
如果不需要某个字段,如 formSchema
不需要 field
为 index
的字段,可以使用 form: { hidden: true }
进行过滤,其他组件同理。
Search
是基于 Form
进行二次封装的,所以 Search
支持的参数 Form
也都支持。
search
与 form
字段,可以传入 dictName
来获取全局的字典数据,也可以传入 api
来获取接口数据,如果使用 api
,需要主动 return
数据。
如果想看更复杂点的例子,请在线预览
<script setup lang="ts">\nimport { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'\n\nconst crudSchemas = reactive<CrudSchema[]>([\n {\n field: 'index',\n label: t('tableDemo.index'),\n type: 'index',\n form: {\n hidden: true\n },\n detail: {\n hidden: true\n }\n },\n {\n field: 'title',\n label: t('tableDemo.title'),\n search: {\n show: true\n },\n form: {\n colProps: {\n span: 24\n }\n },\n detail: {\n span: 24\n }\n },\n {\n field: 'author',\n label: t('tableDemo.author')\n },\n {\n field: 'display_time',\n label: t('tableDemo.displayTime'),\n form: {\n component: 'DatePicker',\n componentProps: {\n type: 'datetime',\n valueFormat: 'YYYY-MM-DD HH:mm:ss'\n }\n }\n },\n {\n field: 'importance',\n label: t('tableDemo.importance'),\n formatter: (_: Recordable, __: TableColumn, cellValue: number) => {\n return h(\n ElTag,\n {\n type: cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'\n },\n () =>\n cellValue === 1\n ? t('tableDemo.important')\n : cellValue === 2\n ? t('tableDemo.good')\n : t('tableDemo.commonly')\n )\n },\n form: {\n component: 'Select',\n componentProps: {\n options: [\n {\n label: '重要',\n value: 3\n },\n {\n label: '良好',\n value: 2\n },\n {\n label: '一般',\n value: 1\n }\n ]\n }\n }\n },\n {\n field: 'pageviews',\n label: t('tableDemo.pageviews'),\n form: {\n component: 'InputNumber',\n value: 0\n }\n },\n {\n field: 'content',\n label: t('exampleDemo.content'),\n table: {\n hidden: true\n },\n form: {\n component: 'Editor',\n colProps: {\n span: 24\n }\n },\n detail: {\n span: 24\n }\n },\n {\n field: 'action',\n width: '260px',\n label: t('tableDemo.action'),\n form: {\n hidden: true\n },\n detail: {\n hidden: true\n }\n }\n])\n\nconst { allSchemas } = useCrudSchemas(crudSchemas)\n</script>\n\n
const { allSchemas } = useCrudSchemas(crudSchemas)\n
allSchemas
allSchemas
存放着四个组件所需要的数据结果
allSchemas.fromSchema
Form
组件的 Sechema
allSchemas.searchSchema
Search
组件的 Sechema
allSchemas.detailSchema
Descriptions
组件的 Sechema
allSchemas.tableColumns
Table
组件的 columns
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
search | 用于设置 searchSchema | CrudSearchParams | - | - |
table | 用于设置 tableColumns | CrudTableParams | - | - |
form | 用于设置 fromSchema | CrudFormParams | - | - |
detail | 用于设置 DescriptionsSchema | CrudDescriptionsParams | - | - |
children | 如果是 Table 组件,则可能会有多表头的情况存在 | CrudSchema[] | - | - |
监听网络状态
useNetwork 位于 src/hooks/web/useNetwork.ts
<script setup lang="ts">\nimport { useNetwork } from '@/hooks/web/useNetwork'\n\nconst { online } = useNetwork()\n\nconsole.log(online)\n</script>\n\n
const { online } = useNetwork()\n
online
online
网络是否已连接
用于操作 localStorage 和 sessionStorage
useStorage 位于 src/hooks/web/useStorage.ts
默认使用 sessionStorage
,如需要使用 localStorage
,只需要传入 localStorage
即可,如:useStorage('localStorage')
支持非字符串类型存取值
<script setup lang="ts">\nimport { useStorage } from '@/hooks/web/useStorage'\n\nconst { setStorage, getStorage, removeStorage, clear } = useStorage()\n\nsetStorage('key', { name: 'Jok' })\n\ngetStorage('key')\n\nremoveStorage('key')\n\nclear()\n</script>\n\n
const { setStorage, getStorage, removeStorage, clear } = useStorage('localStorage')\n
setStorage
setStorage
存储数据
getStorage
getStorage
获取某个存储数据
removeStorage
removeStorage
清除某个存储数据
clear
clear
清除所有缓存数据,如果需要排除某些数据,可以传入 excludes 来排除,如:clear(['key']),这样 key
就不会被清除
操作标签页
useTagsView 位于 src/hooks/web/useTagsView.ts
<script setup lang="ts">\n<script setup lang="ts">\nimport { ContentWrap } from '@/components/ContentWrap'\nimport { ElButton } from 'element-plus'\nimport { useTagsView } from '@/hooks/web/useTagsView'\nimport { useRouter } from 'vue-router'\n\nconst { push } = useRouter()\n\nconst { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage, setTitle } =\n useTagsView()\n\nconst closeAllTabs = () => {\n closeAll(() => {\n push('/dashboard/analysis')\n })\n}\n\nconst closeLeftTabs = () => {\n closeLeft()\n}\n\nconst closeRightTabs = () => {\n closeRight()\n}\n\nconst closeOtherTabs = () => {\n closeOther()\n}\n\nconst refresh = () => {\n refreshPage()\n}\n\nconst closeCurrentTab = () => {\n closeCurrent(undefined, () => {\n push('/dashboard/analysis')\n })\n}\n\nconst setTabTitle = () => {\n setTitle(new Date().getTime().toString())\n}\n\nconst setAnalysisTitle = () => {\n setTitle(`分析页-${new Date().getTime().toString()}`, '/dashboard/analysis')\n}\n</script>\n\n<template>\n <ContentWrap title="useTagsView">\n <ElButton type="primary" @click="closeAllTabs"> 关闭所有标签页 </ElButton>\n <ElButton type="primary" @click="closeLeftTabs"> 关闭左侧标签页 </ElButton>\n <ElButton type="primary" @click="closeRightTabs"> 关闭右侧标签页 </ElButton>\n <ElButton type="primary" @click="closeOtherTabs"> 关闭其他标签页 </ElButton>\n <ElButton type="primary" @click="closeCurrentTab"> 关闭当前标签页 </ElButton>\n <ElButton type="primary" @click="refresh"> 刷新当前标签页 </ElButton>\n <ElButton type="primary" @click="setTabTitle"> 修改当前标题 </ElButton>\n <ElButton type="primary" @click="setAnalysisTitle"> 修改分析页标题 </ElButton>\n </ContentWrap>\n</template>\n\n</script>\n\n
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage, setTitle } = useTagsView()\n
closeAll
closeAll
用于关闭所有标签页
closeLeft
closeLeft
用于关闭当前左侧标签页
closeRight
closeRight
用于关闭当前右侧标签页
closeOther
closeOther
用于关闭除当前标签页外的所有标签页
closeCurrent
closeCurrent
用于关闭除当前标签页
refreshPage
refreshPage
用于刷新当前标签页
setTitle
setTitle(title: string, path: string)
用于设置某个标签页的标签,接收 标题和一个完整的path路径
为元素设置水印
useWatermark 位于 src/hooks/web/useWatermark.ts
<script setup lang="ts">\nimport { useWatermark } from '@/hooks/web/useWatermark'\nimport { onBeforeUnmount } from 'vue'\n\nconst { setWatermark, clear } = useWatermark()\n\nconst { t } = useI18n()\n\nsetWatermark('ElementPlusAdmin')\n\nonBeforeUnmount(() => {\n clear()\n})\n</script>\n\n
const { setWatermark, clear } = useWatermark()\n
setWatermark
setWatermark
用于设置水印文案,接收一个 string
类型的参数
clear
clear
用于清除水印
展示多个头像集合
Avatars 组件位于 src/components/Avatars 内
<script lang="ts" setup>
+import { Avatars } from '@/components/Avatars'
+
+const data = ref<AvatarItem[]>([
+ {
+ name: 'Lily',
+ url: 'https://avatars.githubusercontent.com/u/3459374?v=4'
+ },
+ {
+ name: 'Amanda',
+ url: 'https://avatars.githubusercontent.com/u/3459375?v=4'
+ },
+ {
+ name: 'Daisy',
+ url: 'https://avatars.githubusercontent.com/u/3459376?v=4'
+ },
+ {
+ name: 'Olivia',
+ url: 'https://avatars.githubusercontent.com/u/3459377?v=4'
+ },
+ {
+ name: 'Tina',
+ url: 'https://avatars.githubusercontent.com/u/3459378?v=4'
+ },
+ {
+ name: 'Kitty',
+ url: 'https://avatars.githubusercontent.com/u/3459323?v=4'
+ },
+ {
+ name: 'Helen',
+ url: 'https://avatars.githubusercontent.com/u/3459324?v=4'
+ },
+ {
+ name: 'Sophia',
+ url: 'https://avatars.githubusercontent.com/u/3459325?v=4'
+ },
+ {
+ name: 'Wendy',
+ url: 'https://avatars.githubusercontent.com/u/3459326?v=4'
+ }
+])
+</script>
+
+<template>
+ <Avatars :data="data" />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
size | 头像尺寸 | ComponentSize、number | - | - |
max | 最大展示个数 | number | - | 5 |
data | 头像数据,详见 | AvatarItem[] | - | - |
showTooltip | 是否展示名称tip | boolean | - | true |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 头像图片地址 | string | - | - |
name | 名称,非必填 | string | - | - |
二次封装 ElButton
,支持修改主题色
BaseButton 组件位于 src/components/Button 内
BaseButton 已经全局引入,无需在手动引入
<template>
+ <BaseButton type="primary"> Add </BaseButton>
+</template>
+
+
支持 ElButton
的所有属性
1.2.4
新增
用于展示详情,自带返回按钮。
ContentDetailWrap 组件位于 src/components/ContentDetailWrap 内
<script setup lang="ts">
+import { ContentDetailWrap } from '@/components/ContentDetailWrap'
+</script>
+
+<template>
+ <ContentDetailWrap title="详情" @back="push('/example/example-page')">
+ Details
+ </ContentDetailWrap>
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
title | 标题 | string | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
back | 返回事件 | - |
插槽名 | 说明 | 子标签 |
---|---|---|
- | 默认展示内容 | - |
title | 自定义标题内容 | - |
right | 自定义右侧内容 | - |
基于 vue-count-to
改造
CountTo 组件位于 src/components/CountTo 内
更复杂点的例子,请在线预览
<script setup lang="ts">
+import { CountTo } from '@/components/CountTo'
+</script>
+
+<template>
+ <CountTo :start-val="0" :end-val="35225" />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
startVal | 初始值 | number | - | 0 |
endVal | 最后展示的值 | number | - | 2021 |
duration | 动画时间 | number | - | 3000 |
autoplay | 是否自动播放 | boolean | - | true |
decimals | 小位数 | number | - | 0 |
decimal | 小位数分割符号 | string | - | . |
separator | 分割符号 | string | - | , |
prefix | 前缀 | string | - | - |
suffix | 后缀 | string | - | - |
useEasing | 过渡动画 | boolean | - | true |
easingFn | 自定义动画效果 | (t: number, b: number, c: number, d: number) => number | - | - |
注意
从 v2.5.3之后,Descriptions 组件不再基于 element-plus
的 Descriptions
进行二次封装,所以可能有的属性无法使用,具体可以自行修改或者改造,或者可以提issue。
对 element-plus
的 Descriptions
组件进行封装。
Descriptions 组件位于 src/components/Descriptions 内
注意
推荐使用 tsx
来使用 Descriptions
组件
更复杂点的例子,请在线预览
<script setup lang="tsx">
+import { Descriptions, DescriptionsSchema } from '@/components/Descriptions'
+import { reactive } from 'vue'
+
+const data = reactive({
+ username: 'chenkl',
+ nickName: '梦似花落。',
+ age: 26,
+ phone: '13655971xxxx',
+ email: '502431556@qq.com',
+ addr: '这是一个很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长很长的地址',
+ sex: '男',
+ certy: '3505831994xxxxxxxx'
+})
+
+const schema = reactive<DescriptionsSchema[]>([
+ {
+ field: 'username',
+ label: 'username'
+ },
+ {
+ field: 'nickName',
+ label: 'nickName'
+ },
+ {
+ field: 'phone',
+ label: 'phone'
+ },
+ {
+ field: 'email',
+ label: 'email'
+ },
+ {
+ field: 'addr',
+ label: 'addr',
+ span: 24
+ }
+])
+</script>
+
+<template>
+ <Descriptions
+ title="descriptions"
+ message="message"
+ :data="data"
+ :schema="schema"
+ />
+</template>
+
+
除以下参数外,还支持 element-plus
的 Descriptions
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
title | 标题 | string | - | - |
message | 提示 | string | - | - |
collapse | 是否显示展开按钮 | boolean | - | true |
schema | 布局结构数据,详见 | DescriptionsSchema[] | - | [] |
data | 展示的数据 | Recordable | - | {} |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
span | 栅格占比 | number | - | - |
field | 字段名,唯一值,需要与 data 中的属性名对应 | string | - | - |
label | 列表标题 | string | - | - |
width | 列表宽度 | string /number | - | - |
minWidth | 列表最小宽度 | string /number | - | - |
align | 内容对齐方式 | string | left/center/right | left |
labelAlign | 标题对齐方式 | string | left/center/right | left |
className | 自定义内容标签类名 | string | - | - |
labelClassName | 自定义标题标签类名 | string | - | - |
slots | 插槽对象 | object | - | - |
对 element-plus
的 Dialog
组件进行封装。
Dialog 组件位于 src/components/Dialog 内
<script setup lang="ts">
+import { Dialog } from '@/components/Dialog'
+import { ElButton } from 'element-plus'
+import { ref } from 'vue'
+
+const dialogVisible = ref(false)
+</script>
+
+<template>
+ <ElButton type="primary" @click="dialogVisible = !dialogVisible">
+ open
+ </ElButton>
+ <Dialog v-model="dialogVisible" title="dialog">
+ <div v-for="v in 10000" :key="v">{{ v }}</div>
+ <template #footer>
+ <el-button @click="dialogVisible = false">close</el-button>
+ </template>
+ </Dialog>
+</template>
+
+
除以下参数外,还支持 element-plus
的 Dialog
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
modelValue | 是否显示弹窗,支持v-model | boolean | - | false |
fullscreen | 是否显示全屏按钮 | boolean | - | true |
title | 弹窗标题 | string | - | Dialog |
maxHeight | 弹窗内容最大高度 | string /number | - | 500px |
插槽名 | 说明 | 子标签 |
---|---|---|
- | 弹窗内容 | - |
title | 弹窗标题内容 | - |
footer | 弹窗底部内容 | - |
对 echarts
进行封装,自适应窗口大小。
Echart 组件位于 src/components/Echart 内
只需传入对应的 options
和 height
即可展示图表。
<template>
+ <Echart :options="pieOptions" :height="300" />
+</template>
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
options | echart 对应的配置项,详见 | EChartsOption | - | [] |
width | 图表宽度 | string /number | - | - |
height | 图表高度 | string /number | - | 500 |
基于 wangeditor 封装。
目前项目中的 editor
只是做了简单的封装,需要开发者根据实际情况,自行配置 editorConfig
属性,如,上传图片功能。
可自行阅读 wangeditor文档
Editor 组件位于 src/components/Editor 内
<script setup lang="ts">
+import { Editor } from '@/components/Editor'
+import { ref} from 'vue'
+
+const defaultHtml = ref('<p>hello <strong>world</strong></p>')
+
+const change = (html: string) => {
+ console.log(html)
+}
+</script>
+
+<template>
+ <Editor v-model="defaultHtml" ref="editorRef" @change="change" />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
editorId | 富文本组件唯一值,必填项 | string | - | wangeEditor-1 |
height | 高度 | string /number | - | 500px |
editorConfig | wangeditor 组件的所有配置项 | IEditorConfig | - | - |
modelValue | 内容双向绑定,支持v-model | string | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
change | 内容改变时,返回 editor 实例 | editor: IDomEditor |
方法名 | 说明 | 回调参数 |
---|---|---|
getEditorRef | 获取 editor 实例 | () => Promise<IDomEditor> |
用于各种占位图组件,如 404
、403
、500
等错误页面。
Error 组件位于 src/components/Error 内
<script setup lang="ts">
+import { Error } from '@/components/Error'
+</script>
+
+<template>
+ <Error />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
type | 占位图类型 | string | - | 404 |
方法名 | 说明 | 回调参数 |
---|---|---|
errorClick | 点击按钮后的回调 | - |
目前只提供了 404
、403
、500
三种类型,如果不满足实际需求,可自行扩展。
只需在 src/components/Error/src/Error.vue 文件的 errorMap
对象扩展对应类型即可。
为整个项目提供页脚信息,自动适应,内容高度不够时,会一直保持在最底部,内容超出则跟随在内容后面。
Footer 组件位于 src/components/Footer 内,如果需要修改页脚信息,可在组件内自定义修改。
<script setup lang="ts">
+import { Footer } from '@/components/Footer'
+</script>
+
+<template>
+ <Footer />
+</template>
+
+
对 element-plus
的 Form
组件进行封装,支持 element-plus
的所有表单组件,并额外扩展了一些功能。
Form 组件位于 src/components/Form 内
注意
推荐使用 tsx 来使用 Form
组件。
目前支持的表单组件,你可以在 在线预览 中看到。
<script setup lang="ts">
+import { Form, FormSchema } from '@/components/Form'
+import { reactive } from 'vue'
+
+const schema = reactive<FormSchema[]>([
+ {
+ field: 'field1',
+ label: 'input',
+ component: 'Input'
+ }
+])
+</script>
+
+<template>
+ <Form :schema="schema" />
+</template>
+
+
对于复杂的场景,可以配合 useForm
来使用。
如果想看更复杂点的例子,请在线预览
<script setup lang="tsx">
+import { Form, FormSchema } from '@/components/Form'
+import { reactive } from 'vue'
+import { useForm } from '@/hooks/web/useForm'
+
+const schema = reactive<FormSchema[]>([
+ {
+ field: 'field1',
+ label: 'input',
+ component: 'Input'
+ }
+])
+
+const { formRegister } = useForm()
+</script>
+
+<template>
+ <Form :schema="schema" @register="formRegister" />
+</template>
+
+
const { formRegister, formMethods } = useForm()
+
register
formRegister
用于注册 useForm
,如果需要使用 useForm
提供的 api
,必须将 formRegister
传入组件的 onRegister
formMethods
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getFormData | 用于获取表单数据 | <T = Recordable>() => Promise<T> |
getComponentExpose | 用于获取表单组件实例,如 ElInput 实例 | (field: string) => any |
getFormItemExpose | 用于获取 formItem 组件实例 | (field: string) => Promise<ComponentRef<typeof ElFormItem>> |
getElFormExpose | 用于获取 elForm 组件实例 | (field: string) => Promise<ComponentRef<typeof ElForm>> |
getFormExpose | 用于获取二次封装的 Form 组件实例 | () => Promise<ComponentRef<typeof Form>> |
除以下参数外,还支持 element-plus
的 Form
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
schema | 生成 Form 的布局结构数组,详见 | FormSchema | - | [] |
isCol | 是否需要栅格布局 | boolean | - | true |
model | 表单数据对象 | Recordable | - | {} |
autoSetPlaceholder | 是否自动设置 placeholder | boolean | - | true |
isCustom | 是否自定义内容 | boolean | - | false |
labelWidth | 表单 label 宽度 | string /number | - | auto |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
field | 唯一值,必填项 | string | - | - |
label | 标题 | string | - | - |
colProps | element-plus 的 col 组件属性 | ColProps | - | - |
componentProps | 表单组件子属性,详见 | any | - | - |
formItemProps | element-plus 的 form-item 组件属性,详见 | FormItemProps | - | - |
component | 需要渲染的表单子组件 | ComponentName | - | - |
value | 表单子组件初始值 | any | - | - |
hidden | 表单子组件是否隐藏 | boolean | - | - |
remove | 表单子组件是否隐藏,如果为true,会连同值一同删除,类似v-if | boolean | - | - |
optionApi | 加载 options 方法 | () => Promise<any> | - | - |
componentProps的类型有: InputComponentProps
AutocompleteComponentProps
InputNumberComponentProps
SelectComponentProps
SelectV2ComponentProps
CascaderComponentProps
SwitchComponentProps
RateComponentProps
ColorPickerComponentProps
TransferComponentProps
RadioGroupComponentProps
RadioButtonComponentProps
DividerComponentProps
DatePickerComponentProps
DateTimePickerComponentProps
TimePickerComponentProps
InputPasswordComponentProps
TreeSelectComponentProps
UploadComponentProps
any
基本上每个表单组件都有 slots
的插槽对象,用于自定义插槽,如 InputComponentProps :
slots?: {
+ prefix?: (...args: any[]) => JSX.Element | null
+ suffix?: (...args: any[]) => JSX.Element | null
+ prepend?: (...args: any[]) => JSX.Element | null
+ append?: (...args: any[]) => JSX.Element | null
+}
+
如果需要监听组件事件,如 change 事件,每个 ComponentProps
基本上都有 on
对象用来接收事件,如 InputComponentProps :
on?: {
+ blur?: (event: FocusEvent) => void
+ focus?: (event: FocusEvent) => void
+ change?: (value: string | number) => void
+ clear?: () => void
+ input?: (value: string | number) => void
+}
+
+
除了以下属性,还支持 element-plus
中的 FormItem
的所有属性
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
slots | FormItem的插槽 | Object | - | - |
style | 子表单项的样式 | CSSProperties | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getComponentExpose | 用于获取表单子组件的实例,如 ElInput 实例 | (field: string) => any |
getFormItemExpose | 用于获取 FormItem 组件的实例 | () => Promise<typeof FormItem> |
当项目中内置的表单组件不满足需求时,可以自行添加组件进去。
ComponentName
添加你组件名称。componentMap
对象中添加键值对即可。componentProps
Highlight 组件位于 src/components/Highlight 内
组件只能接收纯文本。
<script setup lang="ts">
+import { Highlight } from '@/components/Highlight'
+</script>
+
+<template>
+ <Highlight :keys="['十年前', '现在']">
+ 种一棵树最好的时间是十年前,其次就是现在。
+ </Highlight>
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
tag | 包裹标签 | string | - | span |
keys | 高亮的关键字 | string[] | - | [] |
color | 高亮的颜色 | string | - | var(--el-color-primary) |
方法名 | 说明 | 回调参数 |
---|---|---|
click | 关键字点击事件 | key: string |
用于同意协议选项
IAgree 组件位于 src/components/IAgree 内
<template>
+ <IAgree
+ :link="[
+ {
+ text: '《隐私政策》',
+ url: 'https://www.baidu.com'
+ }
+ ]"
+ text="我同意《隐私政策》"
+ />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
text | 文案 | string | - | - |
link | 需要跳转的高亮数据,详见 | LinkItem[] | - | - |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 跳转地址,非必填 | string | - | - |
text | 高亮文案 | string | - | - |
onClick | 点击高亮文案执行的方法,非必填 | () => void | - | - |
用于快速选择 Iconify 图标。
IconPicker 组件位于 src/components/IconPicker 内
TIP
目前只集成了 Ant Design Icons 、Element Plus、TDesign Icons 三个开源项目图标
<script lang="ts" setup>
+import { IconPicker } from '@/components/IconPicker'
+
+const currentIcon = ref('tdesign:book-open')
+</script>
+
+<template>
+ <IconPicker v-model="currentIcon" />
+</template>
+
+
可以执行 pnpm run icon
然后选择你想要的图标集
之后,在 IconPicker.vue 导入,并添加到 icons
中即可。
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
modelValue | 选中项绑定值,支持v-model | string | - | - |
用于项目内组件的展示,基本支持所有图标库(支持按需加载,只打包所用到的图标),支持使用本地 svg 和 Iconify 图标。
Icon 组件位于 src/components/Icon 内
TIP
在 Iconify 上,你可以查询到你想要的所有图标并使用,不管是不是 element-plus
的图标库。
如果以svg-icon:
开头,则会在本地中找到该 svg
图标,否则,会加载 Iconify
图标。
<template>
+ <!-- 加载本地 svg -->
+ <Icon icon="svg-icon:peoples" />
+
+ <!-- 加载 Iconify -->
+ <Icon icon="ep:aim" />
+</template>
+
+
如果需要在其他组件中如 ElButton
传入 icon
属性,可以使用 useIcon
<script setup lang="ts">
+import { useIcon } from '@/hooks/web/useIcon'
+import { ElButton } from 'element-plus'
+
+const icon = useIcon({ icon: 'svg-icon:save' })
+</script>
+
+<template>
+ <ElButton :icon="icon"> button </ElButton>
+</template>
+
const icon = useIcon(props)
+
props
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
icon | 图标名 | string | - | - |
color | 图标颜色 | string | - | - |
size | 图标大小 | number | - | 16 |
hoverColor | hover颜色 | string | - | - |
将 element-plus
的 ImageViewer
组件函数化,通过函数方便创建组件。
ImageViewer 组件位于 src/components/ImageViewer 内
<script setup lang="ts">
+import { createImageViewer } from '@/components/ImageViewer'
+import { ElButton } from 'element-plus'
+
+const open = () => {
+ createImageViewer({
+ urlList: [
+ 'https://img1.baidu.com/it/u=657828739,1486746195&fm=26&fmt=auto&gp=0.jpg',
+ 'https://img0.baidu.com/it/u=3114228356,677481409&fm=26&fmt=auto&gp=0.jpg',
+ 'https://img1.baidu.com/it/u=508846955,3814747122&fm=26&fmt=auto&gp=0.jpg',
+ 'https://img1.baidu.com/it/u=3536647690,3616605490&fm=26&fmt=auto&gp=0.jpg',
+ 'https://img1.baidu.com/it/u=4087287201,1148061266&fm=26&fmt=auto&gp=0.jpg',
+ 'https://img2.baidu.com/it/u=3429163260,2974496379&fm=26&fmt=auto&gp=0.jpg'
+ ]
+ })
+}
+</script>
+
+<template>
+ <ElButton type="primary" @click="open">预览</ElButton>
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
urlList | 图片列表 | string[] | - | - |
zIndex | 层级 | number | - | - |
initialIndex | 默认展示第几张 | number | - | 1 |
infinite | 是否可以循环切换 | boolean | - | true |
hideOnClickModal | 点击蒙版是否关闭 | boolean | - | false |
appendToBody | 是否添加到 body 中 | boolean | - | false |
show | 是否显示图片预览 | boolean | - | false |
基于 Highlight
组件封装。
Infotip 组件位于 src/components/Infotip 内
<script setup lang="ts">
+import { Infotip } from '@/components/Infotip'
+</script>
+
+<template>
+ <Infotip
+ title="推荐使用Iconify组件"
+ :schema="[
+ {
+ label: 'Iconify组件基本包含所有的图标,你可以查询到你想要的任何图标。并且打包只会打包所用到的图标。',
+ keys: ['Iconify']
+ },
+ {
+ label: '访问地址',
+ keys: ['访问地址']
+ }
+ ]"
+ />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
title | 标题 | string | - | - |
schema | 展示的数据内容 | string[] /TipSchema[] | - | [] |
showIndex | 显示序号 | boolean | - | true |
highlightColor | 高亮颜色 | string | - | var(--el-color-primary) |
方法名 | 说明 | 回调参数 |
---|---|---|
click | 关键字点击事件 | key: string |
对 element-plus
的 Input
组件进行封装。
InputPassword 组件位于 src/components/InputPassword 内
<script setup lang="ts">
+import { InputPassword } from '@/components/InputPassword'
+import { ref } from 'vue'
+
+const password = ref('')
+</script>
+
+<template>
+ <InputPassword v-model="password" strength />
+</template>
+
+
除以下参数外,还支持 element-plus
的 Input
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
strength | 是否显示强度校验 | boolean | - | false |
modelValue | 选中项绑定值,支持v-model | string | - | - |
本项目中集成了很多常用的功能组件,方便开发者使用。如果你开发项目中自带的组件不满足你的需求,可以不使用或者自行改造,这都是可以的。
目前本项目中基本上都是采用按需引入的方式,只有 Icon
和 Permission
进行了全局注册。
如果不喜欢按需引入,可以自行去集成 unplugin-auto-import
基于 vue-json-pretty 封装。
可自行阅读 vue-json-pretty文档
JsonEditor 组件位于 src/components/JsonEditor 内
<script setup lang="ts">
+<script setup lang="ts">
+import { ContentWrap } from '@/components/ContentWrap'
+import { JsonEditor } from '@/components/JsonEditor'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ref, watch } from 'vue'
+
+const { t } = useI18n()
+
+const defaultData = ref({
+ title: '标题',
+ content: '内容'
+})
+
+watch(
+ () => defaultData.value,
+ (val) => {
+ console.log(val)
+ },
+ {
+ deep: true
+ }
+)
+
+setTimeout(() => {
+ defaultData.value = {
+ title: '异步标题',
+ content: '异步内容'
+ }
+}, 4000)
+</script>
+
+<template>
+ <ContentWrap :title="t('richText.jsonEditor')" :message="t('richText.jsonEditorDes')">
+ <JsonEditor v-model="defaultData" />
+ </ContentWrap>
+</template>
+
+
用于颗粒级别的按钮权限组件
Permission 组件位于 src/components/Permission 内
由于项目中的颗粒级别的权限,是放在路由表中,所以会判断在当前路由 meta.permission
是否包含传入的权限值,有的话则展示。
如果权限实现不一致的话,可以自行改造下。
<template>
+ <Permission permission="add">
+ <ElButton type="primary"> Add </ElButton>
+ </Permission>
+</template>
+
+
权限控制目前还提供了指令的使用方式,并且已经全局注册,所以可以在任意组件中使用 v-hasPermi
<ElButton v-hasPermi="'add'" type="primary"> Add </ElButton>
+
+
除了以上两种,还可以使用函数的形式进行控制
import { hasPermi } from '@/components/Permission'
+
+
<ElButton v-if="hasPermi('add')" type="primary"> Add </ElButton>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
permission | 权限值 | string | - | - |
基于 qrcode
封装。
Qrcode 组件位于 src/components/Qrcode 内
更复杂点的例子,请在线预览
<script setup lang="ts">
+import { Qrcode } from '@/components/Qrcode'
+</script>
+
+<template>
+ <Qrcode text="vue-element-plus-admin" />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
tag | 以什么标签生成二维码 | string | canvas/img | canvas |
text | 二维码内容 | string /Array | - | - |
options | qrcode.js 配置项 | QRCodeRenderersOptions | - | {} |
width | 二维码宽度 | number | - | 200 |
logo | 二维码 logo | QrcodeLogo /string | - | - |
disabled | 二维码是否过期 | boolean | - | false |
disabledText | 二维码过期提示内容 | string | - | - |
方法名 | 说明 | 回调参数 |
---|---|---|
done | 生成二维码后的回调 | - |
click | 二维码点击事件 | - |
disabled-click | 二维码过期后点击事件 | - |
基于 Form
组件封装,支持收缩展开。
Search 组件位于 src/components/Search 内
注意
推荐使用 tsx
来使用 Search
组件
更复杂例子,请在线预览
<script setup lang="ts">
+import { Search } from '@/components/Search'
+import { FormSchema } from '@/components/Form'
+import { reactive } from 'vue'
+
+const schema = reactive<FormSchema[]>([
+ {
+ field: 'field1',
+ label: 'input',
+ component: 'Input'
+ }
+])
+</script>
+
+<template>
+ <Search :schema="schema" />
+</template>
+
+
对于复杂的场景,可以配合 useSearch
来使用。
<script setup lang="ts">
+import { ContentWrap } from '@/components/ContentWrap'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Search } from '@/components/Search'
+import { reactive, ref, unref } from 'vue'
+import { ElButton } from 'element-plus'
+import { getDictOneApi } from '@/api/common'
+import { FormSchema } from '@/components/Form'
+import { useSearch } from '@/hooks/web/useSearch'
+
+const { t } = useI18n()
+
+const { searchRegister, searchMethods } = useSearch()
+const { setSchema, setProps, setValues } = searchMethods
+
+const schema = reactive<FormSchema[]>([
+ {
+ field: 'field1',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field2',
+ label: t('formDemo.select'),
+ component: 'Select',
+ componentProps: {
+ options: [
+ {
+ label: 'option1',
+ value: '1'
+ },
+ {
+ label: 'option2',
+ value: '2'
+ }
+ ],
+ on: {
+ change: (value: string) => {
+ console.log(value)
+ }
+ }
+ }
+ },
+ {
+ field: 'field3',
+ label: t('formDemo.radio'),
+ component: 'RadioGroup',
+ componentProps: {
+ options: [
+ {
+ label: 'option-1',
+ value: '1'
+ },
+ {
+ label: 'option-2',
+ value: '2'
+ }
+ ]
+ }
+ },
+ {
+ field: 'field5',
+ component: 'DatePicker',
+ label: t('formDemo.datePicker'),
+ componentProps: {
+ type: 'date'
+ }
+ },
+ {
+ field: 'field6',
+ component: 'TimeSelect',
+ label: t('formDemo.timeSelect')
+ },
+ {
+ field: 'field8',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field9',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field10',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field11',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field12',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field13',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field14',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field15',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field16',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field17',
+ label: t('formDemo.input'),
+ component: 'Input'
+ },
+ {
+ field: 'field18',
+ label: t('formDemo.input'),
+ component: 'Input'
+ }
+])
+
+const isGrid = ref(false)
+
+const changeGrid = (grid: boolean) => {
+ setProps({
+ isCol: grid
+ })
+ // isGrid.value = grid
+}
+
+const layout = ref('inline')
+
+const changeLayout = () => {
+ layout.value = unref(layout) === 'inline' ? 'bottom' : 'inline'
+}
+
+const buttonPosition = ref('left')
+
+const changePosition = (position: string) => {
+ layout.value = 'bottom'
+ buttonPosition.value = position
+}
+
+const getDictOne = async () => {
+ const res = await getDictOneApi()
+ if (res) {
+ setSchema([
+ {
+ field: 'field2',
+ path: 'componentProps.options',
+ value: res.data
+ }
+ ])
+ }
+}
+
+const handleSearch = (data: any) => {
+ console.log(data)
+}
+
+const delRadio = () => {
+ setSchema([
+ {
+ field: 'field3',
+ path: 'remove',
+ value: true
+ }
+ ])
+}
+
+const restoreRadio = () => {
+ setSchema([
+ {
+ field: 'field3',
+ path: 'remove',
+ value: false
+ }
+ ])
+}
+
+const setValue = () => {
+ setValues({
+ field1: 'Joy'
+ })
+}
+
+const searchLoading = ref(false)
+const changeSearchLoading = () => {
+ searchLoading.value = true
+ setTimeout(() => {
+ searchLoading.value = false
+ }, 2000)
+}
+
+const resetLoading = ref(false)
+const changeResetLoading = () => {
+ resetLoading.value = true
+ setTimeout(() => {
+ resetLoading.value = false
+ }, 2000)
+}
+</script>
+
+<template>
+ <ContentWrap
+ :title="`${t('searchDemo.search')} ${t('searchDemo.operate')}`"
+ style="margin-bottom: 20px"
+ >
+ <ElButton @click="changeGrid(true)">{{ t('searchDemo.grid') }}</ElButton>
+ <ElButton @click="changeGrid(false)">
+ {{ t('searchDemo.restore') }} {{ t('searchDemo.grid') }}
+ </ElButton>
+
+ <ElButton @click="changeLayout">
+ {{ t('searchDemo.button') }} {{ t('searchDemo.position') }}
+ </ElButton>
+
+ <ElButton @click="changePosition('left')">
+ {{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.left') }}
+ </ElButton>
+ <ElButton @click="changePosition('center')">
+ {{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.center') }}
+ </ElButton>
+ <ElButton @click="changePosition('right')">
+ {{ t('searchDemo.bottom') }} {{ t('searchDemo.position') }}-{{ t('searchDemo.right') }}
+ </ElButton>
+ <ElButton @click="getDictOne">
+ {{ t('formDemo.select') }} {{ t('searchDemo.dynamicOptions') }}
+ </ElButton>
+ <ElButton @click="delRadio">{{ t('searchDemo.deleteRadio') }}</ElButton>
+ <ElButton @click="restoreRadio">{{ t('searchDemo.restoreRadio') }}</ElButton>
+ <ElButton @click="setValue">{{ t('formDemo.setValue') }}</ElButton>
+
+ <ElButton @click="changeSearchLoading">
+ {{ t('searchDemo.search') }} {{ t('searchDemo.loading') }}
+ </ElButton>
+ <ElButton @click="changeResetLoading">
+ {{ t('searchDemo.reset') }} {{ t('searchDemo.loading') }}
+ </ElButton>
+ </ContentWrap>
+
+ <ContentWrap :title="t('searchDemo.search')" :message="t('searchDemo.searchDes')">
+ <Search
+ :schema="schema"
+ :is-col="isGrid"
+ :layout="layout"
+ :button-position="buttonPosition"
+ :search-loading="searchLoading"
+ :reset-loading="resetLoading"
+ show-expand
+ expand-field="field6"
+ @search="handleSearch"
+ @reset="handleSearch"
+ @register="searchRegister"
+ />
+ </ContentWrap>
+</template>
+
+
const { searchRegister, searchMethods } = useSearch()
+
register
searchRegister
用于注册 useSearch
,如果需要使用 useSearch
提供的 api
,必须将 searchRegister
传入组件的 onRegister
formMethods
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getFormData | 用于获取表单数据 | <T = Recordable>() => Promise<T> |
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
schema | 生成 Search 的布局结构数组,详见 | FormSchema | - | [] |
isCol | 是否需要栅格布局 | boolean | - | true |
labelWidth | 表单 label 宽度 | string /number | - | auto |
layout | 操作按钮风格位置 | string | inline/bottom | inline |
buttonPosition | 底部操作按钮的对齐方式 | string | left/center/right | center |
showSearch | 是否显示查询按钮 | boolean | - | true |
showReset | 是否显示重置按钮 | boolean | - | true |
expand | 是否显示伸缩按钮 | boolean | - | false |
expandField | 伸缩的界限字段 | string | - | - |
inline | 是否是行内 | boolean | - | true |
removeNoValueItem | 是否自动去除空值 | boolean | - | true |
model | 初始化数据 | object | - | - |
searchLoading | 查询按钮加载状态 | boolean | - | false |
resetLoading | 重置按钮加载状态 | boolean | - | false |
方法名 | 说明 | 回调参数 |
---|---|---|
search | 查询后的回调 | data: Recordable |
reset | 重置后的回调 | data: Recordable |
方法名 | 说明 | 回调参数 |
---|---|---|
setValues | 用于设置表单值 | (data: Recordable) => void |
setProps | 用于设置表单属性 | (props: Recordable) => void |
delSchema | 用于删除表单结构 | (field: string) => void |
addSchema | 用于新增表单结构 | (formSchema: FormSchema, index?: number) => void |
setSchema | 用于编辑表单结构 | (schemaProps: FormSetPropsType[]) => void |
getElFormExpose | 用于获取 Form 组件的实例 | () => Promise<typeof ElForm> |
1.2.4
新增
Sticky 组件位于 src/components/Sticky 内
<script setup lang="ts">
+import { Sticky } from '@/components/Sticky'
+</script>
+
+<template>
+ <Sticky :offset="90">
+ <div style="padding: 10px; background-color: lightblue"> Sticky 距离顶部90px </div>
+ </Sticky>
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
offset | 距离顶部或者底部的距离 | number | - | 0 |
zIndex | 设置元素的堆叠顺序 | number | - | 999 |
className | 设置指定的class | string /number | - | - |
position | 定位方式,默认为(top),表示距离顶部位置,可以设置为top或者bottom | string | top/bottom | top |
对 element-plus
的 Table
组件进行封装,只需传入 columns
与 data
参数,即可渲染出响应的表格出来。
Table 组件位于 src/components/Table 内
注意
推荐使用 tsx 来使用 Table
组件。
<script setup lang="ts">
+import { reactive } from 'vue'
+import { Table, TableColumn } from '@/components/Table'
+
+const columns = reactive<TableColumn[]>([
+ {
+ field: 'title',
+ label: 'title'
+ },
+ {
+ field: 'author',
+ label: 'author'
+ }
+])
+
+const data = reactive([
+ {
+ title: 'title1',
+ author: 'author1'
+ },
+ {
+ title: 'title2',
+ author: 'author2'
+ },
+ {
+ title: 'title3',
+ author: 'author3'
+ }
+])
+</script>
+
+<template>
+ <Table :columns="columns" :data="data" />
+</template>
+
+
推荐配合 useTable
来使用
复杂点的例子,请在线预览
<script setup lang="tsx">
+import { ContentWrap } from '@/components/ContentWrap'
+import { useI18n } from '@/hooks/web/useI18n'
+import { Table, TableColumn, TableSlotDefault } from '@/components/Table'
+import { getTreeTableListApi } from '@/api/table'
+import { reactive, unref } from 'vue'
+import { ElTag, ElButton } from 'element-plus'
+import { useTable } from '@/hooks/web/useTable'
+
+const { tableRegister, tableState } = useTable({
+ fetchDataApi: async () => {
+ const { currentPage, pageSize } = tableState
+ const res = await getTreeTableListApi({
+ pageIndex: unref(currentPage),
+ pageSize: unref(pageSize)
+ })
+ return {
+ list: res.data.list,
+ total: res.data.total
+ }
+ }
+})
+const { loading, dataList, total, currentPage, pageSize } = tableState
+
+const { t } = useI18n()
+
+const columns = reactive<TableColumn[]>([
+ {
+ field: 'selection',
+ type: 'selection'
+ },
+ {
+ field: 'index',
+ label: t('tableDemo.index'),
+ type: 'index'
+ },
+ {
+ field: 'content',
+ label: t('tableDemo.header'),
+ children: [
+ {
+ field: 'title',
+ label: t('tableDemo.title')
+ },
+ {
+ field: 'author',
+ label: t('tableDemo.author')
+ },
+ {
+ field: 'display_time',
+ label: t('tableDemo.displayTime')
+ },
+ {
+ field: 'importance',
+ label: t('tableDemo.importance'),
+ formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
+ return (
+ <ElTag type={cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'}>
+ {cellValue === 1
+ ? t('tableDemo.important')
+ : cellValue === 2
+ ? t('tableDemo.good')
+ : t('tableDemo.commonly')}
+ </ElTag>
+ )
+ }
+ },
+ {
+ field: 'pageviews',
+ label: t('tableDemo.pageviews')
+ }
+ ]
+ },
+ {
+ field: 'action',
+ label: t('tableDemo.action'),
+ slots: {
+ default: (data) => {
+ return (
+ <ElButton type="primary" onClick={() => actionFn(data)}>
+ {t('tableDemo.action')}
+ </ElButton>
+ )
+ }
+ }
+ }
+])
+
+const actionFn = (data: TableSlotDefault) => {
+ console.log(data)
+}
+</script>
+
+<template>
+ <ContentWrap :title="`${t('router.treeTable')} ${t('tableDemo.example')}`">
+ <Table
+ v-model:pageSize="pageSize"
+ v-model:currentPage="currentPage"
+ :columns="columns"
+ :data="dataList"
+ row-key="id"
+ :loading="loading"
+ sortable
+ :pagination="{
+ total: total
+ }"
+ @register="tableRegister"
+ />
+ </ContentWrap>
+</template>
+
+</script>
+
+<template>
+ <Table
+ v-model:pageSize="tableObject.pageSize"
+ v-model:currentPage="tableObject.currentPage"
+ :data="tableObject.tableList"
+ :loading="tableObject.loading"
+ :pagination="{
+ total: tableObject.total
+ }"
+ @register="register"
+ />
+</template>
+
+
const { tableRegister, tableState, tableMethods } = useTable(props: UseTableConfig)
+
props
在使用 useTable
的时候,需要传入 fetchDataApi
,为了保证可定制,需要自行在 fetchDataApi
中完成请求逻辑,之后返回结果 { list: Array, total?: number },后续分页,就可以自动请求数据。
如果需要删除,同样需要传入 fetchDelApi
,返回一个 Boolean
来判断是否删除完成,后续 useTable
将自行刷新表格。
tableRegister
tableRegister
用于注册 useTable
,如果需要使用 useTable
提供的 api
,必须将 tableRegister
传入组件的 onRegister
tableState
表格状态
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
pageSize | 每页显示多少条 | number | - | 10 |
currentPage | 当前页 | number | - | 1 |
total | 总条数 | number | - | - |
dataList | 表格数据 | any[] | - | [] |
loading | 表格是否加载中 | boolean | - | false |
tableMethods
方法名 | 说明 | 回调参数 |
---|---|---|
setProps | 用于表格组件属性 | (props: Recordable) => void |
getList | 获取表格数据 | () => Promise<void> |
setColumn | 设置表头结构 | (columnProps: TableSetProps[]) => void |
addColumn | 新增表头结构 | (tableColumn: TableColumn, index?: number) => void |
delColumn | 删除表头结构 | (field: string) => void |
getElTableExpose | 获取 ElTable 实例 | () => Promise<typeof ElTable> |
refresh | 刷新表格 | () => void |
delList | 删除数据 | (idsLength: number) => Promise<void> |
除以下参数外,还支持 element-plus
的 Table
所有属性,详见
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
pageSize | 每页显示多少条,支持 v-model 双向绑定 | number | - | 10 |
currentPage | 当前页,支持 v-model 双向绑定 | number | - | 1 |
selection | 是否多选 | boolean | - | true |
showOverflowTooltip | 是否所有的超出隐藏,优先级低于 schema 中的 showOverflowTooltip | boolean | - | true |
columns | 表头结构,详见 | TableColumn[] | - | [] |
expand | 是否显示展开行 | boolean | - | false |
pagination | 是否展示分页,详见 | Pagination /undefined | - | - |
reserveSelection | 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key) | boolean | - | false |
loading | 加载状态 | boolean | - | false |
reserveIndex | 是否叠加索引 | boolean | - | false |
align | 内容对齐方式 | string | left /center /right | left |
headerAlign | 表头对齐方式 | string | left /center /right | left |
data | 表格数据 | Recordable[] | - | [] |
showAction | 是否显示表格操作 | boolean | - | false |
imagePreview | 需要展示图片的字段 | string[] | - | - |
videoPreview | 需要展示视频的字段 | string[] | - | - |
customContent | 是否自定义内容 | boolean | - | false |
cardBodyStyle | 卡片内容样式 | CSSProperties | - | - |
cardBodyClass | 卡片内容类名 | string | - | - |
cardWrapStyle | 卡片容器样式 | CSSProperties | - | - |
cardWrapClass | 卡片容器类名 | string | - | - |
除了以下属性,还支持 element-plus
的 TableColumn
属性。
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
field | 唯一值,如需展示正确的数据,需要与 data 中的属性名对应 | string | - | - |
label | 表头名称 | string | - | - |
hidden | 是否隐藏 | boolean | - | - |
slots | 插槽对象 | object | - | - |
children | 子项,用于多级表头 | TableColumn[] | - | - |
支持 element-plus
的 Pagination
所有属性,详见
方法名 | 说明 | 回调参数 |
---|---|---|
setProps | 用于设置表格属性 | (props: Recordable) => void |
setColumn | 用于修改表头结构 | (columnProps: TableSetPropsType[]) => void |
addColumn | 新增表头结构 | (tableColumn: TableColumn, index?: number) => void |
delColumn | 删除表头结构 | (field: string) => void |
基于 xgplayer
二次封装的视频播放器
VideoPlayer 组件位于 src/components/VideoPlayer 内
<script lang="ts" setup>
+import { VideoPlayer } from '@/components/VideoPlayer'
+</script>
+
+<template>
+ <VideoPlayer
+ url="//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4"
+ poster="//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg"
+ />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 视频的地址 | string | - | - |
poster | 视频的封面 | string | - | - |
将 VideoPlayer
组件函数化,通过函数方便创建组件。
VideoViewer 组件位于 src/components/VideoViewer 内
<script setup lang="ts">
+import { createVideoViewer } from '@/components/VideoPlayer'
+
+const open = () => {
+ createVideoViewer({
+ url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-720p.mp4',
+ poster: '//lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg'
+ })
+}
+</script>
+
+<template>
+ <ElButton type="primary" @click="open">预览</ElButton>
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
url | 视频的地址 | string | - | - |
poster | 视频的封面 | string | - | - |
瀑布流组件
Waterfall 组件位于 src/components/Waterfall 内
TIP
data 数据必须带有高度字段,用于确保计算出正确的位置
<script lang="ts" setup>
+import { Waterfall } from '@/components/Waterfall'
+import Mock from 'mockjs'
+import { ref, unref } from 'vue'
+import { toAnyString } from '@/utils'
+
+const data = ref<any>([])
+
+const getList = () => {
+ const list: any = []
+ for (let i = 0; i < 20; i++) {
+ // 随机 100, 500 之间的整数
+ const height = Mock.Random.integer(100, 500)
+ const width = Mock.Random.integer(100, 500)
+ list.push(
+ Mock.mock({
+ width,
+ height,
+ id: toAnyString(),
+ image_uri: Mock.Random.image(`${width}x${height}`)
+ })
+ )
+ }
+ data.value = [...unref(data), ...list]
+ if (unref(data).length >= 60) {
+ end.value = true
+ }
+}
+getList()
+
+const loading = ref(false)
+
+const end = ref(false)
+
+const loadMore = () => {
+ loading.value = true
+ setTimeout(() => {
+ getList()
+ loading.value = false
+ }, 1000)
+}
+</script>
+
+<template>
+ <Waterfall
+ :data="data"
+ :loading="loading"
+ :end="end"
+ :props="{
+ src: 'image_uri',
+ height: 'height'
+ }"
+ @load-more="loadMore"
+ />
+</template>
+
+
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
data | 要展示的数据 | Array | - | - |
reset | 窗口变化是否重新布局 | boolean | true/false | true |
width | 每个项的宽度 | number | - | 200 |
gap | 每个项的间距 | number | - | 20 |
loadingText | 加载中文字 | string | - | 加载中... |
loading | 是否加载中 | boolean | - | false |
end | 是否加载结束 | boolean | - | false |
endText | 是否加载结束文字 | string | - | 没有更多了 |
props | 字段别名 | object | - | { src: 'src', height: 'height' } |
方法名 | 说明 | 回调参数 |
---|---|---|
loadMore | 加载更多事件 | - |
为了方便开发者快速生成 组件
和 视图
文件,本项目提供了 plop
为开发者生成统一的文件模版。
运行
npm run p
+
选择 component
之后,输入组件名,如 newCom
,既可在 src/components
目录下创建对应的组件。
组件名开头如果是小写,会自动转换为大写。
运行
npm run p
+
选择 view
之后,输入路径,默认为 views
,接着输入模块名,如 newView
,既可在 src/${views}
目录下创建对应的视图文件。
如果需要扩展额外的视图模版,可以在根目录 plopfile.js
文件中,添加初始模版,然后到根目录 plop
文件夹中,添加对应的模块代码。具体可以参考 component
下的代码。
更多的 plop
配置,则可以查阅 文档
默认第一次进入系统,会检测浏览器默认的主题
如果需要切换 明亮
或者 暗黑
,可以执行 appStore.setIsDark(val)
进行主题的切换。
具体例子可以查看登录页的右上角主题切换。
如果你使用的 vscode 开发工具,则推荐安装 I18n-ally 这个插件
安装了该插件后,你的代码内可以实时看到对应的语言内容
在 src/config/locale.ts 内配置 currentLocale
为其他语言。
import { useCache } from '@/hooks/web/useCache'
+import zhCn from 'element-plus/lib/locale/lang/zh-cn'
+import en from 'element-plus/lib/locale/lang/en'
+
+const { wsCache } = useCache()
+
+export const elLocaleMap = {
+ 'zh-CN': zhCn,
+ en: en
+}
+export interface LocaleState {
+ currentLocale: LocaleDropdownType
+ localeMap: LocaleDropdownType[]
+}
+
+export const localeModules: LocaleState = {
+ currentLocale: {
+ lang: wsCache.get('lang') || 'zh-CN',
+ elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']
+ },
+ // 多语言
+ localeMap: [
+ {
+ lang: 'zh-CN',
+ name: '简体中文'
+ },
+ {
+ lang: 'en',
+ name: 'English'
+ }
+ ]
+}
+
+
在 src/locales 可以配置具体的语言,目前项目中的语言都是没有拆分的,全部放一起,后续会考虑拆分出来,比较好维护。
在 src/plugins/vueI18n/index.ts 内可以看到
const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
+
这会导入 src/locales
文件语言包。
引入项目自带的 useI18n
注意不要引入 vue-i18n 的 useI18n
import { useI18n } from '/@/hooks/web/useI18n'
+
+const { t } = useI18n()
+
+const title = t('common.menu')
+
切换语言需要使用 src/locales/useLocale.ts
import { useLocale } from '@/hooks/web/useLocale'
+const { changeLocale } = useLocale()
+
+changeLocale('en')
+
在 src/locales 增加对应语言的文件即可
目前项目自带的语言只有 zh_CN
和 en
两种
如果需要新增,按以下操作即可
LocaleType
添加对应的类型localeMap
中添加对应语言目前项目会在 src/main.ts
内等待 setupI18n
这个函数执行完之后才会渲染界面,所以只需在 setupI18n 内的 createI18nOptions
发送 ajax 请求,将对应的数据设置到 i18n 实例上即可。
const createI18nOptions = async (): Promise<I18nOptions> => {
+ const localeStore = useLocaleStoreWithOut()
+ const locale = localeStore.getCurrentLocale
+ const localeMap = localeStore.getLocaleMap
+ // 这里改为远程请求即可。
+ const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
+ const message = defaultLocal.default ?? {}
+
+ setHtmlPageLang(locale.lang)
+
+ localeStore.setCurrentLocale({
+ lang: locale.lang
+ // elLocale: elLocal
+ })
+
+ return {
+ legacy: false,
+ locale: locale.lang,
+ fallbackLocale: locale.lang,
+ messages: {
+ [locale.lang]: message
+ },
+ availableLocales: localeMap.map((v) => v.lang),
+ sync: true,
+ silentTranslationWarn: true,
+ missingWarn: false,
+ silentFallbackWarn: true
+ }
+}
+
当手动切换语言的时候会触发 useLocale
函数,useLocale 也是异步函数,只需等待接口返回响应的数据后,再进行设置即可
export const useLocale = () => {
+ // Switching the language will change the locale of useI18n
+ // And submit to configuration modification
+ const changeLocale = async (locale: LocaleType) => {
+ const globalI18n = i18n.global
+
+ // 改为远程获取
+ const langModule = await import(`../../locales/${locale}.ts`)
+
+ globalI18n.setLocaleMessage(locale, langModule.default)
+
+ setI18nLanguage(locale)
+ }
+
+ return {
+ changeLocale
+ }
+}
+
使用 lint 的好处
具备基本工程素养的同学都会注重编码规范,而代码风格检查(Code Linting,简称 Lint)是保障代码规范一致性的重要手段。
遵循相应的代码规范有以下好处
项目内集成了以下几种代码校验方式
注意
lint 不是必须的,但是很有必要,一个项目做大了以后或者参与人员过多后,就会出现各种风格迥异的代码,对后续的维护造成了一定的麻烦。
ESLint 是一个代码规范和错误检查工具,可以根据自己的团队设置符合自己团队的规范
# 执行下面代码.能修复的会自动修复,不能修复的需要手动修改
+pnpm run lint:eslint
+
项目的 eslint 配置位于根目录下 .eslintrc.js 内,可以根据团队自行修改代码规范
在一个团队中,每个人的 git 的 commit 信息都不一样,五花八门,没有一个机制很难保证规范化,如何才能规范化呢?可能你想到的是 git 的 hook 机制,去写 shell 脚本去实现。这当然可以,其实 JavaScript 有一个很好的工具可以实现这个模板,它就是 commitlint(用于校验 git 提交信息规范)。
commit-lint 的配置位于项目根目录下 commitlint.config.js
feat
新功能fix
修补 bugdocs
文档style
格式、样式(不影响代码运行的变动)refactor
重构(即不是新增功能,也不是修改 BUG 的代码)perf
优化相关,比如提升性能、体验test
添加测试build
编译相关的修改,对项目构建或者依赖的改动ci
持续集成修改chore
构建过程或辅助工具的变动revert
回滚到上一个版本workflow
工作流改进mod
不确定分类的修改wip
开发中types
类型在 .husky/commit-msg
内注释以下代码即可
# npx --no-install commitlint --edit "$1"
+
+git commit -m 'feat: add new component'
+
+
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
# 加上 --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'],
+};
+
如果你觉得这个项目有帮助,欢迎赞助以示支持~
如果你想进入技术交流群讨论,请扫码入群或者添加我为好友邀请入群
项目中集成了 2 种权限处理方式:
目前项目中提供了测试的帐号:
admin/admin
实现原理: 在前端固定写死路由的权限,指定路由有哪些权限可以查看。只初始化通用的路由,需要权限才能访问的路由没有被加入路由表内。在登陆后或者其他方式获取对应的路由 keys 后,遍历路由表去匹配 keys,过滤生成可以访问的路由表,再通过 router.addRoutes
添加到路由实例,实现权限的过滤。
缺点: 权限相对不自由,因为路由表的控制在前端,不管是要排序还是修改,都需要前端去修改,服务端只提供有权限的路由 keys
实现原理: 是通过接口动态生成路由表,且遵循一定的数据结构返回。前端根据需要处理该数据为可识别的结构,再通过 router.addRoutes
添加到路由实例,实现权限的动态生成。
优点: 所有的菜单控制都是通过服务端的接口返回,前端只负责渲染,后期维护成本降低,优先推荐此方式。
generateRoutes()
进行更改。接收的 type
参数,目前只是针对于本项目的模拟情况,如果不需要或者不适用,可自行改动。
generateRoutes(
+ type: 'server' | 'frontEnd' | 'static',
+ routers?: AppCustomRouteRecordRaw[] | string[]
+): Promise<unknown> {
+ return new Promise<void>((resolve) => {
+ let routerMap: AppRouteRecordRaw[] = []
+ if (type === 'server') {
+ // 模拟后端过滤菜单
+ routerMap = generateRoutesByServer(routers as AppCustomRouteRecordRaw[])
+ } else if (type === 'frontEnd') {
+ // 模拟前端过滤菜单
+ routerMap = generateRoutesByFrontEnd(cloneDeep(asyncRouterMap), routers as string[])
+ } else {
+ // 直接读取静态路由表
+ routerMap = cloneDeep(asyncRouterMap)
+ }
+ // 动态路由,404一定要放到最后面
+ this.addRouters = routerMap.concat([
+ {
+ path: '/:path(.*)*',
+ redirect: '/404',
+ name: '404Page',
+ meta: {
+ hidden: true,
+ breadcrumb: false
+ }
+ }
+ ])
+ // 渲染菜单的所有路由
+ this.routers = cloneDeep(constantRouterMap).concat(routerMap)
+ resolve()
+ })
+}
+
generateRoutesByFrontEnd ()
进行更改。目前本项目的前端权限控制,是根据 path
是否相同来进行过滤演示的,如果不符合需求,需要手动更改以下判断逻辑。// 前端控制路由生成
+export const generateRoutesByFrontEnd = (
+ routes: AppRouteRecordRaw[],
+ keys: string[],
+ basePath = '/'
+): AppRouteRecordRaw[] => {
+ const res: AppRouteRecordRaw[] = [];
+
+ for (const route of routes) {
+ const meta = route.meta as RouteMeta;
+ // skip some route
+ if (meta.hidden && !meta.showMainRoute) {
+ continue;
+ }
+
+ let data: Nullable<AppRouteRecordRaw> = null;
+
+ let onlyOneChild: Nullable<string> = null;
+ if (route.children && route.children.length === 1 && !meta.alwaysShow) {
+ onlyOneChild = (
+ isUrl(route.children[0].path)
+ ? route.children[0].path
+ : pathResolve(pathResolve(basePath, route.path), route.children[0].path)
+ ) as string;
+ }
+
+ // 开发者可以根据实际情况进行扩展
+ for (const item of keys) {
+ // 通过路径去匹配
+ if (isUrl(item) && (onlyOneChild === item || route.path === item)) {
+ data = Object.assign({}, route);
+ } else {
+ const routePath = pathResolve(basePath, onlyOneChild || route.path);
+ if (routePath === item || meta.followRoute === item) {
+ data = Object.assign({}, route);
+ }
+ }
+ }
+
+ // recursive child routes
+ if (route.children && data) {
+ data.children = generateRoutesByFrontEnd (route.children, keys, pathResolve(basePath, data.path));
+ }
+ if (data) {
+ res.push(data as AppRouteRecordRaw);
+ }
+ }
+ return res;
+};
+
generateRoutesByServer ()
进行更改。// 后端控制路由生成
+export const generateRoutesByServer = (routes: AppCustomRouteRecordRaw[]): AppRouteRecordRaw[] => {
+ const res: AppRouteRecordRaw[] = [];
+
+ for (const route of routes) {
+ const data: AppRouteRecordRaw = {
+ path: route.path,
+ name: route.name,
+ redirect: route.redirect,
+ meta: route.meta,
+ };
+ if (route.component) {
+ const comModule =
+ modules[`../${route.component}.vue`] || modules[`../${route.component}.tsx`];
+ const component = route.component as string;
+ if (!comModule && !component.includes('#')) {
+ console.error(`未找到${route.component}.vue文件或${route.component}.tsx文件,请创建`);
+ } else {
+ // 动态加载路由文件,可根据实际情况进行自定义逻辑
+ data.component =
+ component === '#' ? Layout : component.includes('##') ? getParentLayout() : comModule;
+ }
+ }
+ // recursive child routes
+ if (route.children) {
+ data.children = generateRoutesByServer (route.children);
+ }
+ res.push(data as AppRouteRecordRaw);
+ }
+ return res;
+};
+
getRole()
进行更改。需要开发者自行根据需求进行代码变更。
// 获取角色信息
+const getRole = async () => {
+ const formData = await getFormData<UserType>()
+ const params = {
+ roleName: formData.username
+ }
+ const res =
+ appStore.getDynamicRouter && appStore.getServerDynamicRouter
+ ? await getAdminRoleApi(params)
+ : await getTestRoleApi(params)
+ if (res) {
+ const routers = res.data || []
+ setStorage('roleRouters', routers)
+ appStore.getDynamicRouter && appStore.getServerDynamicRouter
+ ? await permissionStore.generateRoutes('server', routers).catch(() => {})
+ : await permissionStore.generateRoutes('frontEnd', routers).catch(() => {})
+
+ permissionStore.getAddRouters.forEach((route) => {
+ addRoute(route as RouteRecordRaw) // 动态添加可访问路由表
+ })
+ permissionStore.setIsAddRouters(true)
+ push({ path: redirect.value || permissionStore.addRouters[0].path })
+ }
+};
+
// 开发者可根据实际情况进行修改
+const roleRouters = getStorage('roleRouters') || []
+
+// 是否使用动态路由
+if (appStore.getDynamicRouter) {
+ appStore.serverDynamicRouter
+ ? await permissionStore.generateRoutes('server', roleRouters as AppCustomRouteRecordRaw[])
+ : await permissionStore.generateRoutes('frontEnd', roleRouters as string[])
+ } else {
+ await permissionStore.generateRoutes('static')
+}
+
+permissionStore.getAddRouters.forEach((route) => {
+ router.addRoute(route as unknown as RouteRecordRaw) // 动态添加可访问路由表
+})
+const redirectPath = from.query.redirect || to.path
+const redirect = decodeURIComponent(redirectPath as string)
+const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }
+permissionStore.setIsAddRouters(true)
+next(nextData)
+
有时候,我们并不需要动态路由,那么可以在 src/config/app.ts
中把 dynamicRouter
设置为 false
,这样我们取得都是项目中的静态路由表了。
内部逻辑已经处理了静态路由的部分,所以可以无需关心其他。
项目目前的组件注册机制是按需注册,是在需要用到的页面才引入。
<script setup lang="ts">
+import { ElBacktop } from 'element-plus'
+import { useDesign } from '@/hooks/web/useDesign'
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('backtop')
+</script>
+
+<template>
+ <ElBacktop
+ :class="`${prefixCls}-backtop`"
+ :target="`.${variables.namespace}-layout-content-scrollbar .${variables.elNamespace}-scrollbar__wrap`"
+ />
+</template>
+
+
tsx 文件内不能使用全局注册组件,需要手动引入组件使用。
如果觉得按需引入太麻烦,可以进行全局注册,在src/components/index.ts,添加需要注册的组件。
目前只有 Icon
组件进行了全局注册。
import type { App } from 'vue'
+import { Icon } from './Icon'
+
+export const setupGlobCom = (app: App<Element>): void => {
+ app.component('Icon', Icon)
+}
+
+
如果 element-plus
的组件需要全局注册,在 src/plugins/elementPlus/index.ts 添加需要注册的组件。
目前 element-plus
中只有 ElLoading
与 ElScrollbar
进行了全局注册。
import type { App } from 'vue'
+
+// 需要全局引入一些组件,如ElScrollbar,不然一些下拉项样式有问题
+import { ElLoading, ElScrollbar } from 'element-plus'
+
+const plugins = [ElLoading]
+
+const components = [ElScrollbar]
+
+export const setupElementPlus = (app: App) => {
+ plugins.forEach((plugin) => {
+ app.use(plugin)
+ })
+
+ components.forEach((component) => {
+ app.component(component.name, component)
+ })
+}
+
+
前言
由于是展示项目,所以打包后相对较大,如果项目中没有用到的插件,可以删除对应的文件或者路由,不引用即可,没有引用就不会打包。
项目开发完成之后,执行以下命令进行构建
构建打包成功之后,会在根目录生成 dist-* 文件夹,里面就是构建打包好的文件。
发布之前可以在本地进行预览
不能直接打开构建后的 html 文件
使用项目自定的命令进行预览(推荐)
# 先打包在进行预览
+
+# 预览开发环境
+pnpm run serve:dev
+
+# 预览测试环境
+pnpm run serve:test
+
+# 预览生产环境
+pnpm run serve:pro
+
注意
项目默认是在生产环境开启 Mock,这样做非常不好,只是为了演示环境有数据,不建议在生产环境使用 Mock,而应该使用真实的后台接口。
简单的部署只需要将最终生成的静态文件,dist-* 文件夹的静态文件发布到你的 cdn 或者静态服务器即可。
部署时可能会发现资源路径不对,只需要修改对应的.env.xxx
文件即可。
# 根据自己路径来配置更改
+VITE_BASE_PATH = /dist-dev/
+
主要介绍如何在项目中使用和规划样式文件。
默认使用 less
作为预处理语言,建议在使用前或者遇到疑问时学习一下 Less 的相关特性。
项目中使用的通用样式,都存放于 src/style/ 下面。
.
+├── index.less # 入口
+├── theme.less # 主题相关
+├── var.css # css变量
+└── variables.module.less # less变量
+
+
全局注入
variables.module.less 这个文件会被全局注入到所有文件,所以在页面内可以直接使用变量而不需要手动引入。
var.css 则是注入到根元素,所以在每个地方也都能用到。
项目中使用了 unocss,具体参见文件使用说明。
可能没有用到人会觉得用起来很不习惯,但就个人而言,用起来还是挺香的。减少了很多不必要的麻烦
语法如下:
<div class="relative w-full h-full px-4"></div>
+
提示
列举了一些常见的问题。有问题可以先来这里寻找,看是否有相关解答,没有的话可以上 issue 中提问或者搜索
因为项目中有的 Store 默认开始了持久化,所以不管你修没修改默认值,都会优先默认取缓存中的值,所以如果修改完默认值之后,还请手动清除下浏览器的 localStorage
,默认值就会生效了。
本地运行之后,会出现路由警告
[Vue Router warn]: No match found for location with path "/authorization/menu"
+
这个无需关心,是vue-router的问题,项目打包上线后是不会有次警告,所以该问题可以忽略。
请自行去百度下 vite 快是怎么个快法,本地运行启动,都是按需加载,一次性加载了几十个资源,当然会比较慢,有了缓存之后非首次就会实现秒开了。
目前项目中已经对于启动时间进行了优化,本地默认加载了全部的 element-plus
的样式文件,会多多少少减少请求资源数量。
启动快慢还是得根据当前文件引用的资源数量来决定。
这是因为你在该路由中使用了第三方模块,这个模块是没有预加载的,所以需要重新去加载这个模块,然后就会出现 page reload
,极大的影响了开发体验,所以可以在 vite.config.ts
中去配置预加载列表:optimizeDeps.include
,这样在服务启动的使用,会先把这些模块给预先加载打包。
pnpm-lock
和 node_modules
,然后重新运行 pnpm i
由于完整版引入了许多第三方模块,所以打包体积会比较大,可以自行删除不需要的第三方模块,或者使用精简版(mini分支)来进行开发。
合理的进行拆包,目前项目中对一些比较大的第三方模块进行了拆包处理。
菜单是根据路由配置来生成,请先看下已有的路由配置是否可以满足你的需要,如果不满足,可以自行去定制化。可以查看路由相关文档
在使用组件的时候,遇到问题,可以先看下对应的在线例子,看是否有对应的代码,基本上覆盖了95%
的使用方式,或者查看对应的组件文档。
项目中大部分使用了 tsx
,所以原先 template
的一些代码规范就不适用了,如 v-if
得使用 {判断条件 ? 成立 : 不成立}
来进行显示隐藏,可以查阅下相关文档。
并且请确保如果要使用 tsx
语法, script
是否声明了 lang="tsx"
如果是在项目中直接添加静态路由,需要确保 appStore 中的 dynamicRouter
和 serverDynamicRouter
为 false
,并且手动清除下浏览器的 localStorage
这是 Volar
插件的问题,一般重启下编辑器即可生效。
设置 VITE_USE_ONLINE_ICON=false ,可能在有的版本设置之后会无效,是因为有BUG,可以复制最新版本的 uno.config.ts
和 Icon.vue
的最新代码。
本文将快速的帮助你从头运行并启动项目。
为什么使用 Pnpm,而不是用其他包管理器,大家可以搜索一下,这里就不做过多的阐述了。
注意
14.x
以上,这里推荐 16.x
及以上。如果你使用的 IDE 是vscode的话,可以安装以下工具来提高开发效率及代码格式化:
注意
注意存放代码的目录及所有父级目录不能存在中文、韩文、日文以及空格,否则安装依赖后启动会出错。
# clone 代码
+git clone https://github.com/kailong321200875/vue-element-plus-admin.git
+
+
git clone https://gitee.com/kailong110120130/vue-element-plus-admin.git
+
如果您电脑未安装Node.js,请安装它,推荐 18.x
及以上
验证
# 验证 npm 是否安装成功
+npm -v
+
+# 验证 node 是否安装成功
+node -v
+
如果你需要同时存在多个 node
版本,可以使用 Nvm 或者其他工具进行 Node.js 进行版本管理。
推荐使用 Pnpm进行依赖安装(若其他包管理器安装不了需要自行处理)。
如果未安装 Pnpm
,可以用下面命令来进行全局安装
# 全局安装 pnpm
+npm i -g pnpm
+
+# 验证
+pnpm -v
+
在项目根目录下,打开命令窗口执行,耐心等待安装完成即可
# 安装依赖
+pnpm i
+
安装依赖时 husky 安装失败
请查看你的源码是否从 Github 或者 Gitee 直接下载的,直接下载是没有 .git
文件夹的,而 husky
需要依赖 git
才能安装。此时需使用 git init
初始化项目,再尝试重新安装即可。
当依赖安装完成后,执行以下命令即可启动项目:
pnpm run dev
+
"scripts": {
+ # 安装依赖
+ "i": "pnpm install",
+ # 本地开发环境运行
+ "dev": "vite --mode base",
+ # typeScript 检测
+ "ts:check": "vue-tsc --noEmit",
+ # 打包生产环境
+ "build:pro": "vite build --mode pro",
+ # 打包开发环境
+ "build:dev": "npm run ts:check && vite build --mode dev",
+ # 打包测试环境
+ "build:test": "npm run ts:check && vite build --mode test",
+ # 本地预览 已打包的生产环境项目包
+ "serve:pro": "vite preview --mode pro",
+ # 本地预览 已打包的开发环境项目包
+ "serve:dev": "vite preview --mode dev",
+ # 本地预览 已打包的测试环境项目包
+ "serve:test": "vite preview --mode test",
+ # 检测可更新依赖
+ "npm:check": "npx npm-check-updates",
+ # 删除 node_modules
+ "clean": "npx rimraf node_modules",
+ # 删除 缓存
+ "clean:cache": "npx rimraf node_modules/.cache",
+ # eslint 检测
+ "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
+ # eslint 格式化
+ "lint:format": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,vue,html,md}\"",
+ # stylelint 格式化
+ "lint:style": "stylelint --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
+ "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js",
+ "lint:pretty": "pretty-quick --staged",
+ "postinstall": "husky install",
+ # 快速生成统一规范的模块
+ "p": "plop"
+},
+
注意
vue-element-plus-admin 是一个基于 element-plus 免费开源的中后台模版。使用了最新的 Vue3,Vite,Typescript等主流技术开发,开箱即用的中后台前端解决方案,可以用来作为项目的启动模版,也可用于学习参考。并且时刻关注着最新技术动向,尽可能的第一时间更新。
vue-element-plus-admin 的定位是后台集成方案,因为集成了很多你可能用不到的功能,会造成不少的代码冗余。如果你的项目不关注这方面的问题,也可以直接基于它进行二次开发。
如需要基础模版,请切换到 mini 分支,mini 只简单集成了一些如:布局、动态菜单等常用布局功能,更适合开发者进行二次开发。
本项目需要一定前端基础知识,请确保掌握 Vue 的基础知识,以便能处理一些常见的问题。
为了能快速上手本项目,请先大致浏览一遍文档及在线示例。
建议在开发前先学一下以下内容,提前了解和学习这些知识,会对项目理解非常有帮助:
.
+├── .github # github workflows 相关
+├── .husky # husky 配置
+├── .vscode # vscode 配置
+├── mock # 自定义 mock 数据及配置
+├── public # 静态资源
+├── src # 项目代码
+│ ├── api # api接口管理
+| |── axios # axios配置
+│ ├── assets # 静态资源
+│ ├── components # 公用组件
+│ ├── constants # 存放常量
+│ ├── hooks # 常用hooks
+│ ├── layout # 布局组件
+│ ├── locales # 语言文件
+│ ├── plugins # 外部插件
+│ ├── router # 路由配置
+│ ├── store # 状态管理
+│ ├── styles # 全局样式
+│ ├── utils # 全局工具类
+│ ├── views # 路由页面
+│ ├── App.vue # 入口vue文件
+│ ├── main.ts # 主入口文件
+│ └── permission.ts # 路由拦截
+├── types # 全局类型
+├── .env.base # 本地开发环境 环境变量配置
+├── .env.dev # 打包到开发环境 环境变量配置
+├── .env.gitee # 针对 gitee 的环境变量 可忽略
+├── .env.pro # 打包到生产环境 环境变量配置
+├── .env.test # 打包到测试环境 环境变量配置
+├── .eslintignore # eslint 跳过检测配置
+├── .eslintrc.js # eslint 配置
+├── .gitignore # git 跳过配置
+├── .prettierignore # prettier 跳过检测配置
+├── .stylelintignore # stylelint 跳过检测配置
+├── .versionrc 自动生成版本号及更新记录配置
+├── CHANGELOG.md # 更新记录
+├── commitlint.config.js # git commit 提交规范配置
+├── index.html # 入口页面
+├── package.json
+├── .postcssrc.js # postcss 配置
+├── prettier.config.js # prettier 配置
+├── README.md # 英文 README
+├── README.zh-CN.md # 中文 README
+├── stylelint.config.js # stylelint 配置
+├── tsconfig.json # typescript 配置
+├── vite.config.ts # vite 配置
+└── uno.config.ts # unocss 配置
+
本地开发推荐使用Chrome 最新版
浏览器。
由于 Vue 3 不再支持 IE11,本项目也不支持 IE。
IE | Edge | Firefox | Chrome | Safari |
---|---|---|---|---|
not support | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
如果前端应用和后端接口服务器没有运行在同一个主机上,你需要在开发环境下将接口请求代理到接口服务器。
如果是同一个主机,可以直接请求具体的接口地址。
在 vite.config.ts
配置文件中,提供了 server 的 proxy 功能,用于代理 API 请求。
server: {
+ proxy: {
+ "/api":{
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ ws: true,
+ rewrite: (path) => path.replace(new RegExp(`^/api`), ''),
+ }
+ },
+},
+
配置接口前缀,可以在对应的 env
文件中,修改 VITE_API_BASE_PATH
的值
注意
该配置只能作用于 本地开发环境。
从浏览器控制台的 Network 看,请求是 http://localhost:3000/api/xxx
,这是因为 proxy 配置不会改变本地请求的 url。
在本项目中,所有的接口数据都是使用 Mock
模拟
接口统一存放于 src/api/ 下面管理
以获取列表接口为例:
在 src/api/ 内新建模块文件,其中参数与返回值最好定义一下类型,方便校验。虽然麻烦,但是后续维护字段很方便。
提示
类型定义文件可以抽取出去统一管理,具体参考项目
import request from '@/axios'
+import type { TableData } from './types'
+
+export const getTableListApi = (params: any) => {
+ return request.get({ url: '/example/list', params })
+}
+
+export const getTreeTableListApi = (params: any) => {
+ return request.get({ url: '/example/treeList', params })
+}
+
+export const saveTableApi = (data: Partial<TableData>): Promise<IResponse> => {
+ return request.post({ url: '/example/save', data })
+}
+
+export const getTableDetApi = (id: string): Promise<IResponse<TableData>> => {
+ return request.get({ url: '/example/detail', params: { id } })
+}
+
+export const delTableListApi = (ids: string[] | number[]): Promise<IResponse> => {
+ return request.post({ url: '/example/delete', data: { ids } })
+}
+
+
axios 请求封装存放于 src/axios 中。
axios 全局配置放在 src/constants 中。
注意
更改之后,将影响所有的请求。
/**
+ * 请求成功状态码
+ */
+export const SUCCESS_CODE = 0
+
+/**
+ * 请求contentType
+ */
+export const CONTENT_TYPE = 'application/json'
+
+/**
+ * 请求超时时间
+ */
+export const REQUEST_TIMEOUT = 60000
+
Mock 数据是前端开发过程中必不可少的一环,是分离前后端开发的关键链路。通过预先跟服务器端约定好的接口,模拟请求数据甚至逻辑,能够让前端开发独立自主,不会被服务端的开发进程所阻塞。
本项目使用 vite-mock-plugin 来进行 mock 数据处理。项目内 mock 服务分本地和线上。
本地 mock 采用 Node.js 中间件进行参数拦截(不采用 mock.js 的原因是本地开发看不到请求参数和响应结果)。
如果你想添加 mock 数据,只要在根目录下找到 mock 文件,添加对应的接口,对其进行拦截和模拟数据。
在 mock 文件夹内新建文件
TIP
文件新增后会自动更新,不需要手动重启,可以在代码控制台查看日志信息 mock 文件夹内会自动注册
TIP
mock 的值可以直接使用 mock.js 的语法。
可以在对应的 env
文件中设置 VITE_USE_MOCK
为 false
,如果想要更彻底一点,可以在vite.config.ts中删除 viteMockServe
对应的代码。
由于该项目是一个展示类项目,线上也是用 mock 数据,所以在打包后同时也集成了 mock。通常项目线上一般为正式接口。
项目线上 mock 采用的是 mock.js 进行 mock 数据模拟。
项目路由配置存放于 src/router/index.ts 中。
为了方便阅读和查找,目前项目中并没有去对路由进行拆分,而是统一写在了一起,如果需要拆分,可自行更改。
因为路由是生成菜单关键,所以本项目中对路由提供了以下配置,方便开发者进行定制。
/**
+* redirect: noredirect 当设置 noredirect 的时候该路由在面包屑导航中不可被点击
+* name:'router-name' 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
+* meta : {
+ hidden: true 当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false)
+
+ alwaysShow: true 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式,
+ 只有一个时,会将那个子路由当做根路由显示在侧边栏,
+ 若你想不管路由下面的 children 声明的个数都显示你的根路由,
+ 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,
+ 一直显示根路由(默认 false)
+
+ title: 'title' 设置该路由在侧边栏和面包屑中展示的名字
+
+ icon: 'svg-name' 设置该路由的图标
+
+ noCache: true 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
+
+ breadcrumb: false 如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
+
+ affix: true 如果设置为true,则会一直固定在tag项中(默认 false)
+
+ noTagsView: true 如果设置为true,则不会出现在tag中(默认 false)
+
+ activeMenu: '/dashboard' 显示高亮的路由路径
+
+ canTo: true 设置为true即使hidden为true,也依然可以进行路由跳转(默认 false)
+
+ permission: ['edit','add', 'delete'] 设置该路由的权限
+ }
+**/
+
如果本项目中的路由配置项,满足不了你当前的开发工作,可以自行添加新的属性。
注意
所有的路由项配置,都必须放在 meta
中。
在 types/router.d.ts 中添加对应的类型,之后就可以在路由中添加你需要的配置项了。
declare module 'vue-router' {
+ interface RouteMeta extends Record<string | number | symbol, unknown> {
+ hidden?: boolean
+ alwaysShow?: boolean
+ title?: string
+ icon?: string
+ noCache?: boolean
+ breadcrumb?: boolean
+ affix?: boolean
+ activeMenu?: string
+ noTagsView?: boolean
+ canTo?: boolean
+ permission?: string[]
+
+ // 添加新的配置类型
+ ...
+ }
+}
+
+
注意事项
name
不能重复/
,其余子路由都不要以/
开头示例
{
+ path: '/level',
+ component: Layout,
+ redirect: '/level/menu1/menu1-1/menu1-1-1',
+ name: 'Level',
+ meta: {
+ title: t('router.level'),
+ icon: 'carbon:skill-level-advanced'
+ },
+ children: [
+ {
+ path: 'menu1',
+ name: 'Menu1',
+ component: getParentLayout(),
+ redirect: '/level/menu1/menu1-1/menu1-1-1',
+ meta: {
+ title: t('router.menu1')
+ },
+ children: [
+ {
+ path: 'menu1-1',
+ name: 'Menu11',
+ component: getParentLayout(),
+ redirect: '/level/menu1/menu1-1/menu1-1-1',
+ meta: {
+ title: t('router.menu11'),
+ alwaysShow: true
+ },
+ children: [
+ {
+ path: 'menu1-1-1',
+ name: 'Menu111',
+ component: () => import('@/views/Level/Menu111.vue'),
+ meta: {
+ title: t('router.menu111')
+ }
+ }
+ ]
+ },
+ {
+ path: 'menu1-2',
+ name: 'Menu12',
+ component: () => import('@/views/Level/Menu12.vue'),
+ meta: {
+ title: t('router.menu12')
+ }
+ }
+ ]
+ },
+ {
+ path: 'menu2',
+ name: 'Menu2Demo',
+ component: () => import('@/views/Level/Menu2.vue'),
+ meta: {
+ title: t('router.menu2')
+ }
+ }
+ ]
+}
+
+
只需要将 path
设置为需要跳转的HTTP 地址即可。
{
+ path: '/external-link',
+ component: Layout,
+ meta: {
+ name: 'ExternalLink'
+ },
+ children: [
+ {
+ path: 'https://github.com/kailong321200875/vue-element-plus-admin-doc',
+ meta: { name: 'Link', title: '文档' }
+ }
+ ]
+}
+
这里的 icon
配置,会同步到 菜单(icon 的值可以查看此处)。
标签页使用的是 keep-alive
和 router-view
实现,实现切换 tab
后还能保存切换之前的状态。
开启缓存有 2 个条件
name
,且不能重复name
,与路由设置的 name
保持一致{
+ path: 'menu2',
+ name: 'Menu2',
+ component: () => import('@/views/Level/Menu2.vue'),
+ meta: {
+ title: t('router.menu2')
+ }
+}
+
+// /@/views/Level/Menu2.vue
+<script setup lang="ts">
+defineOptions({
+ name: 'Menu2'
+})
+</script>
+
+
注意
keep-alive 生效的前提是:需要将路由的 name
属性及对应的页面的 name
设置成一样。因为:
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
可以将 noCache
配置成 true
即可关闭缓存或者组件不添加 name
属性。
{
+ path: 'workplace',
+ component: () => import('@/views/Dashboard/Workplace.vue'),
+ name: 'Workplace',
+ meta: {
+ title: t('router.workplace'),
+ noCache: true
+ }
+}
+
目前项目中,登录进来,默认是进入到当前第一个能找到的路由页面。
后续会考虑弄成一个配置项出来。
本文将介绍一些常用的项目配置,方便开发者可以根据需求进行定制化改造。
项目的环境变量配置位于项目根目录下的,这里主要配置四个个环境变量,分别为:
在开发调试的时候,会读取 .env.base
里面的数据。其他环境亦是如此,根据打包命令的不同,来读取不同的环境变量。
也许你会疑惑,为什么会有多个环境变量?
以 生产环境
为例,当我们执行 pnpm run build:pro
时,输出的包是用于线上环境的,所以代码都应该是压缩,我们需要删除掉代码中的 console.log
和 degubber
,保证打包后代码的整洁度和不可见性。而其他环境,所以应该保留 console.log
和 degubber
用于调试,这样才能快速定位到问题所在。
所以环境变量的作用就是为了,在不同环境下有不同的表现。
提示
VITE_
开头的变量会被嵌入到项目中,你可以项目代码中这样访问它们:console.log(import.meta.env.VITE_APP_TITLE)
+
本地开发环境适用
# 环境
+NODE_ENV = development
+
+# 接口前缀
+VITE_API_BASEPATH = base
+
+# 打包路径
+VITE_BASE_PATH = /
+
+# 标题
+VITE_APP_TITLE = ElementAdmin
+
开发环境适用
# 环境
+NODE_ENV = production
+
+# 接口前缀
+VITE_API_BASEPATH = dev
+
+# 打包路径
+VITE_BASE_PATH = /dist-dev/
+
+# 是否删除debugger
+VITE_DROP_DEBUGGER = false
+
+# 是否删除console.log
+VITE_DROP_CONSOLE = false
+
+# 是否sourcemap
+VITE_SOURCEMAP = true
+
+# 输出路径
+VITE_OUT_DIR = dist-dev
+
+# 标题
+VITE_APP_TITLE = ElementAdmin
+
+
测试环境适用
# 环境
+NODE_ENV = production
+
+# 接口前缀
+VITE_API_BASEPATH = test
+
+# 打包路径
+VITE_BASE_PATH = /dist-test/
+
+# 是否删除debugger
+VITE_DROP_DEBUGGER = false
+
+# 是否删除console.log
+VITE_DROP_CONSOLE = false
+
+# 是否sourcemap
+VITE_SOURCEMAP = true
+
+# 输出路径
+VITE_OUT_DIR = dist-test
+
+
生产环境适用
# 环境
+NODE_ENV = production
+
+# 接口前缀
+VITE_API_BASEPATH = pro
+
+# 打包路径
+VITE_BASE_PATH = /
+
+# 是否删除debugger
+VITE_DROP_DEBUGGER = true
+
+# 是否删除console.log
+VITE_DROP_CONSOLE = true
+
+# 是否sourcemap
+VITE_SOURCEMAP = false
+
+# 输出路径
+VITE_OUT_DIR = dist-pro
+
+# 标题
+VITE_APP_TITLE = ElementAdmin
+
+
提示
项目配置文件用于配置项目内展示的内容、布局、主题色等效果。
修改完之后,会添加到全局的状态管理中,方便其他地方使用。
export const appModules: AppState = {
+ sizeMap: ['default', 'large', 'small'],
+ mobile: false, // 是否是移动端
+ title: import.meta.env.VITE_APP_TITLE as string, // 标题
+ pageLoading: false, // 路由跳转loading
+
+ breadcrumb: true, // 面包屑
+ breadcrumbIcon: true, // 面包屑图标
+ collapse: false, // 折叠菜单
+ hamburger: true, // 折叠图标
+ screenfull: true, // 全屏图标
+ size: true, // 尺寸图标
+ locale: true, // 多语言图标
+ tagsView: true, // 标签页
+ logo: true, // logo
+ fixedHeader: true, // 固定toolheader
+ footer: true, // 显示页脚
+ greyMode: false, // 是否开始灰色模式,用于特殊悼念日
+
+ layout: wsCache.get('layout') || 'classic', // layout布局
+ isDark: wsCache.get('isDark') || false, // 是否是暗黑模式
+ currentSize: wsCache.get('default') || 'default', // 组件尺寸
+ theme: wsCache.get('theme') || {
+ // 主题色
+ elColorPrimary: '#409eff',
+ // 左侧菜单边框颜色
+ leftMenuBorderColor: 'inherit',
+ // 左侧菜单背景颜色
+ leftMenuBgColor: '#001529',
+ // 左侧菜单浅色背景颜色
+ leftMenuBgLightColor: '#0f2438',
+ // 左侧菜单选中背景颜色
+ leftMenuBgActiveColor: 'var(--el-color-primary)',
+ // 左侧菜单收起选中背景颜色
+ leftMenuCollapseBgActiveColor: 'var(--el-color-primary)',
+ // 左侧菜单字体颜色
+ leftMenuTextColor: '#bfcbd9',
+ // 左侧菜单选中字体颜色
+ leftMenuTextActiveColor: '#fff',
+ // logo字体颜色
+ logoTitleTextColor: '#fff',
+ // logo边框颜色
+ logoBorderColor: 'inherit',
+ // 头部背景颜色
+ topHeaderBgColor: '#fff',
+ // 头部字体颜色
+ topHeaderTextColor: 'inherit',
+ // 头部悬停颜色
+ topHeaderHoverColor: '#f6f6f6',
+ // 头部边框颜色
+ topToolBorderColor: '#eee'
+ }
+}
+
如果想要添加新的全局配置属性,需要在 src/store/modules/app.ts 中 AppState
添加对应的类型,并在 appModules
对象中,赋予新属性的默认值。
用于配置多语言信息
在 src/store/modules/locale.ts 内配置
import { useCache } from '@/hooks/web/useCache'
+import zhCn from 'element-plus/lib/locale/lang/zh-cn'
+import en from 'element-plus/lib/locale/lang/en'
+
+const { wsCache } = useCache()
+
+export const elLocaleMap = {
+ 'zh-CN': zhCn,
+ en: en
+}
+export interface LocaleState {
+ currentLocale: LocaleDropdownType
+ localeMap: LocaleDropdownType[]
+}
+
+export const localeModules: LocaleState = {
+ currentLocale: {
+ lang: wsCache.get('lang') || 'zh-CN',
+ elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']
+ },
+ // 多语言
+ localeMap: [
+ {
+ lang: 'zh-CN',
+ name: '简体中文'
+ },
+ {
+ lang: 'en',
+ name: 'English'
+ }
+ ]
+}
+
+
用于修改项目内组件及 element-plus
组件的 class
前缀。
由于 element-plus
的组件还没有全部采用动态配置前缀,所以目前还是使用 el
前缀。
// 命名空间
+@namespace: v;
+// el命名空间
+@elNamespace: el;
+
+// 导出变量
+:export {
+ namespace: @namespace;
+ elNamespace: @elNamespace;
+}
+
+
在 css 内
<style lang="less" scoped>
+ /* namespace已经全局注入,不需要额外在引入 */
+ @prefix-cls: ~'@{namespace}-app';
+
+ .@{prefix-cls} {
+ width: 100%;
+ }
+</style>
+
在 vue/ts 内
import { useDesign } from '/@/hooks/web/useDesign'
+
+const { prefixCls } = useDesign('app')
+
+// prefixCls => v-app
+
由于 WindiCss 不再维护,所以换成了 unocss, 两者在用法上保持了大部分的一致性,但还是有些地方有特别的差异性,对于 v1 版本需要升级到 unocss 话,需要有一定的改造成本。
所以建议 v1 还是继续使用 WindiCss
v2 版本还是保留了四种布局风格,只是在细节上的把控会比 v1 好,主要体现在一些边框重叠的优化上。
v2 版本升级了 typescript5,在用法上基本上没有区别,只是针对了项目中的一些类型的规范进行了更改,使项目的代码更规范化。
v2 版本最主要的更新,就是组件上的更新
主要体现在了 Form
、 Table
、 Search
、Descriptions
的重构上。
在 V1 版本中,以上四个组件在使用上有许多不足的地方,灵活度不够,扩展性不强而被诟病。
所以在 v2 版本中,以上四个组件,schema
全部采用了 tsx
的书写方式,如果定制化比较多的话,tsx
会比 template
更有优势。
同时,以上四个组件支持嵌套绑定,如 Form
的数据绑定,v1 版本只支持一层嵌套,比较局限,在 v2 版本中,支持 xxx.xxx
的绑定方式。
如果用法比较简单的话,也是支持 template
,不过这里还是推荐使用 tsx
,避免之后扩展带来的负担。
v2 版本丰富了在线例子,如果 权限管理
,后续也会继续持续更新更多的例子来让各位客官可以更快速的了解和使用。
注意
如果 v1 版本已经项目落地,或者已经使用了一段时间,建议还是继续使用 v1 版本,刚开始使用的话,可以直接使用 v2 版本
由于两个版本的不兼容,这里是不推荐进行升级。
剪切板
useClipboard 位于 src/hooks/web/useClipboard.ts
<script setup lang="ts">
+import { useClipboard } from '@/hooks/web/useClipboard'
+
+const { copy } = useClipboard()
+
+copy('复制内容')
+</script>
+
+
const { copy, copied, text, isSupported } = useClipboard()
+
copy
copy
复制,参数传入一个需要复制的内容
copied
copied
是否已复制
text
text
复制的内容
isSupported
isSupported
浏览器是否支持复制
统一生成 Search
、 Form
、 Descriptions
、 Table
组件所需要的数据结构。
由于以上四个组件都需要 Sechema
或者 columns
的字段,如果每个组件都写一遍的话,会造成大量重复代码,所以提供 useCrudSchemas
来进行统一的数据生成。
useCrudSchemas 位于 src/hooks/web/useCrudSchemas.ts
TIP
如果不需要某个字段,如 formSchema
不需要 field
为 index
的字段,可以使用 form: { hidden: true }
进行过滤,其他组件同理。
Search
是基于 Form
进行二次封装的,所以 Search
支持的参数 Form
也都支持。
search
与 form
字段,可以传入 dictName
来获取全局的字典数据,也可以传入 api
来获取接口数据,如果使用 api
,需要主动 return
数据。
如果想看更复杂点的例子,请在线预览
<script setup lang="ts">
+import { CrudSchema, useCrudSchemas } from '@/hooks/web/useCrudSchemas'
+
+const crudSchemas = reactive<CrudSchema[]>([
+ {
+ field: 'index',
+ label: t('tableDemo.index'),
+ type: 'index',
+ form: {
+ hidden: true
+ },
+ detail: {
+ hidden: true
+ }
+ },
+ {
+ field: 'title',
+ label: t('tableDemo.title'),
+ search: {
+ show: true
+ },
+ form: {
+ colProps: {
+ span: 24
+ }
+ },
+ detail: {
+ span: 24
+ }
+ },
+ {
+ field: 'author',
+ label: t('tableDemo.author')
+ },
+ {
+ field: 'display_time',
+ label: t('tableDemo.displayTime'),
+ form: {
+ component: 'DatePicker',
+ componentProps: {
+ type: 'datetime',
+ valueFormat: 'YYYY-MM-DD HH:mm:ss'
+ }
+ }
+ },
+ {
+ field: 'importance',
+ label: t('tableDemo.importance'),
+ formatter: (_: Recordable, __: TableColumn, cellValue: number) => {
+ return h(
+ ElTag,
+ {
+ type: cellValue === 1 ? 'success' : cellValue === 2 ? 'warning' : 'danger'
+ },
+ () =>
+ cellValue === 1
+ ? t('tableDemo.important')
+ : cellValue === 2
+ ? t('tableDemo.good')
+ : t('tableDemo.commonly')
+ )
+ },
+ form: {
+ component: 'Select',
+ componentProps: {
+ options: [
+ {
+ label: '重要',
+ value: 3
+ },
+ {
+ label: '良好',
+ value: 2
+ },
+ {
+ label: '一般',
+ value: 1
+ }
+ ]
+ }
+ }
+ },
+ {
+ field: 'pageviews',
+ label: t('tableDemo.pageviews'),
+ form: {
+ component: 'InputNumber',
+ value: 0
+ }
+ },
+ {
+ field: 'content',
+ label: t('exampleDemo.content'),
+ table: {
+ hidden: true
+ },
+ form: {
+ component: 'Editor',
+ colProps: {
+ span: 24
+ }
+ },
+ detail: {
+ span: 24
+ }
+ },
+ {
+ field: 'action',
+ width: '260px',
+ label: t('tableDemo.action'),
+ form: {
+ hidden: true
+ },
+ detail: {
+ hidden: true
+ }
+ }
+])
+
+const { allSchemas } = useCrudSchemas(crudSchemas)
+</script>
+
+
const { allSchemas } = useCrudSchemas(crudSchemas)
+
allSchemas
allSchemas
存放着四个组件所需要的数据结果
allSchemas.fromSchema
Form
组件的 Sechema
allSchemas.searchSchema
Search
组件的 Sechema
allSchemas.detailSchema
Descriptions
组件的 Sechema
allSchemas.tableColumns
Table
组件的 columns
属性 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
search | 用于设置 searchSchema | CrudSearchParams | - | - |
table | 用于设置 tableColumns | CrudTableParams | - | - |
form | 用于设置 fromSchema | CrudFormParams | - | - |
detail | 用于设置 DescriptionsSchema | CrudDescriptionsParams | - | - |
children | 如果是 Table 组件,则可能会有多表头的情况存在 | CrudSchema[] | - | - |
监听网络状态
useNetwork 位于 src/hooks/web/useNetwork.ts
<script setup lang="ts">
+import { useNetwork } from '@/hooks/web/useNetwork'
+
+const { online } = useNetwork()
+
+console.log(online)
+</script>
+
+
const { online } = useNetwork()
+
online
online
网络是否已连接
用于操作 localStorage 和 sessionStorage
useStorage 位于 src/hooks/web/useStorage.ts
默认使用 sessionStorage
,如需要使用 localStorage
,只需要传入 localStorage
即可,如:useStorage('localStorage')
支持非字符串类型存取值
<script setup lang="ts">
+import { useStorage } from '@/hooks/web/useStorage'
+
+const { setStorage, getStorage, removeStorage, clear } = useStorage()
+
+setStorage('key', { name: 'Jok' })
+
+getStorage('key')
+
+removeStorage('key')
+
+clear()
+</script>
+
+
const { setStorage, getStorage, removeStorage, clear } = useStorage('localStorage')
+
setStorage
setStorage
存储数据
getStorage
getStorage
获取某个存储数据
removeStorage
removeStorage
清除某个存储数据
clear
clear
清除所有缓存数据,如果需要排除某些数据,可以传入 excludes 来排除,如:clear(['key']),这样 key
就不会被清除
操作标签页
useTagsView 位于 src/hooks/web/useTagsView.ts
<script setup lang="ts">
+<script setup lang="ts">
+import { ContentWrap } from '@/components/ContentWrap'
+import { ElButton } from 'element-plus'
+import { useTagsView } from '@/hooks/web/useTagsView'
+import { useRouter } from 'vue-router'
+
+const { push } = useRouter()
+
+const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage, setTitle } =
+ useTagsView()
+
+const closeAllTabs = () => {
+ closeAll(() => {
+ push('/dashboard/analysis')
+ })
+}
+
+const closeLeftTabs = () => {
+ closeLeft()
+}
+
+const closeRightTabs = () => {
+ closeRight()
+}
+
+const closeOtherTabs = () => {
+ closeOther()
+}
+
+const refresh = () => {
+ refreshPage()
+}
+
+const closeCurrentTab = () => {
+ closeCurrent(undefined, () => {
+ push('/dashboard/analysis')
+ })
+}
+
+const setTabTitle = () => {
+ setTitle(new Date().getTime().toString())
+}
+
+const setAnalysisTitle = () => {
+ setTitle(`分析页-${new Date().getTime().toString()}`, '/dashboard/analysis')
+}
+</script>
+
+<template>
+ <ContentWrap title="useTagsView">
+ <ElButton type="primary" @click="closeAllTabs"> 关闭所有标签页 </ElButton>
+ <ElButton type="primary" @click="closeLeftTabs"> 关闭左侧标签页 </ElButton>
+ <ElButton type="primary" @click="closeRightTabs"> 关闭右侧标签页 </ElButton>
+ <ElButton type="primary" @click="closeOtherTabs"> 关闭其他标签页 </ElButton>
+ <ElButton type="primary" @click="closeCurrentTab"> 关闭当前标签页 </ElButton>
+ <ElButton type="primary" @click="refresh"> 刷新当前标签页 </ElButton>
+ <ElButton type="primary" @click="setTabTitle"> 修改当前标题 </ElButton>
+ <ElButton type="primary" @click="setAnalysisTitle"> 修改分析页标题 </ElButton>
+ </ContentWrap>
+</template>
+
+</script>
+
+
const { closeAll, closeLeft, closeRight, closeOther, closeCurrent, refreshPage, setTitle } = useTagsView()
+
closeAll
closeAll
用于关闭所有标签页
closeLeft
closeLeft
用于关闭当前左侧标签页
closeRight
closeRight
用于关闭当前右侧标签页
closeOther
closeOther
用于关闭除当前标签页外的所有标签页
closeCurrent
closeCurrent
用于关闭除当前标签页
refreshPage
refreshPage
用于刷新当前标签页
setTitle
setTitle(title: string, path: string)
用于设置某个标签页的标签,接收 标题和一个完整的path路径
为元素设置水印
useWatermark 位于 src/hooks/web/useWatermark.ts
<script setup lang="ts">
+import { useWatermark } from '@/hooks/web/useWatermark'
+import { onBeforeUnmount } from 'vue'
+
+const { setWatermark, clear } = useWatermark()
+
+const { t } = useI18n()
+
+setWatermark('ElementPlusAdmin')
+
+onBeforeUnmount(() => {
+ clear()
+})
+</script>
+
+
const { setWatermark, clear } = useWatermark()
+
setWatermark
setWatermark
用于设置水印文案,接收一个 string
类型的参数
clear
clear
用于清除水印