初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
@@ -0,0 +1,886 @@
<!-- 表单示例 -->
<template>
<div class="pb-5">
<h2 class="mb-1 text-lg font-medium">表单组件示例</h2>
<ElCard shadow="never" class="art-card-xs">
<ArtForm
ref="formRef"
v-model="formData"
:items="formItems"
:rules="formRules"
:defaultExpanded="true"
:labelWidth="labelWidth"
:labelPosition="labelPosition"
:span="span"
:gutter="gutter"
@reset="handleReset"
@submit="handleSubmit"
>
<template #slots>
<ElInput v-model="formData.slots" placeholder="我是插槽渲染出来的组件" />
</template>
</ArtForm>
</ElCard>
<div class="art-card p-5 !rounded-lg mt-5">
<pre><code>{{ formData }}</code></pre>
</div>
<div class="mt-3.5">
<h3 class="mb-2 text-base font-medium">动态表单操作</h3>
<ElSpace wrap class="mb-3">
<ElButton @click="getLevelOptions"> 获取用户等级数据 </ElButton>
<ElButton @click="addFormItem"> 新增表单项 </ElButton>
<ElButton @click="updateFormItem"> 修改表单项 </ElButton>
<ElButton @click="deleteFormItem"> 删除表单项 </ElButton>
<ElButton @click="batchAddFormItems"> 批量新增 </ElButton>
<ElButton @click="resetDynamicItems"> 重置动态项 </ElButton>
</ElSpace>
<h3 class="mb-2 text-base font-medium">其他操作</h3>
<ElSpace wrap>
<ElButton @click="validateForm"> 校验表单 </ElButton>
<ElButton @click="resetForm"> 重置 </ElButton>
<ElButton v-if="showUserName" @click="updateUserName"> 修改用户名 </ElButton>
<ElButton v-if="showUserName" @click="deleteUserName"> 删除用户名 </ElButton>
<ElButton @click="labelWidth = 120"> 修改 label 宽度 </ElButton>
<ElButton @click="span = 8"> 设置一行显示的组件数 </ElButton>
<ElButton @click="gutter = 50"> 修改 gutter </ElButton>
<ElButton @click="labelPosition = 'left'"> label 左对齐 </ElButton>
<ElButton @click="labelPosition = 'right'"> label 右对齐 </ElButton>
<ElButton @click="labelPosition = 'top'"> label 顶部对齐 </ElButton>
</ElSpace>
</div>
<!-- 图片预览对话框 -->
<ElDialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" class="w-full h-auto" />
</ElDialog>
</div>
</template>
<script setup lang="ts">
import ArtWangEditor from '@/components/core/forms/art-wang-editor/index.vue'
import { SearchFormItem } from '@/components/core/forms/art-search-bar/index.vue'
import { ElMessage, ElUpload, ElButton, ElIcon, ElInput } from 'element-plus'
import type { UploadFile, UploadFiles, UploadUserFile } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
interface Emits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'search', params: Record<string, any>): void
(e: 'reset'): void
}
interface OptionItem {
label: string
value: string
disabled?: boolean
}
interface FormData {
name?: string
phone?: string
level?: string
address?: string
slots?: string
date?: string
daterange?: string[]
cascader?: string[]
checkboxgroup?: string[]
userGender?: string
iconSelector?: string
status?: boolean
systemName?: string
fileUpload: UploadUserFile[]
imageUpload: UploadUserFile[]
multipleFiles: UploadUserFile[]
richTextContent: string
}
const emit = defineEmits<Emits>()
const FETCH_DELAY = 500
const formRef = ref()
const dialogVisible = ref(false)
const dialogImageUrl = ref('')
/**
* 表单数据
*/
const formData = ref<FormData>({
name: undefined,
phone: undefined,
level: undefined,
address: undefined,
slots: undefined,
date: undefined,
daterange: undefined,
cascader: undefined,
checkboxgroup: undefined,
userGender: undefined,
iconSelector: undefined,
status: undefined,
systemName: undefined,
fileUpload: [],
imageUpload: [],
multipleFiles: [],
richTextContent: ''
})
/**
* 表单校验规则
*/
const formRules = {
name: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}
const labelWidth = ref(100)
const labelPosition = ref<'right' | 'left' | 'top'>('right')
const span = ref(6)
const gutter = ref(12)
const levelOptions = ref<OptionItem[]>([])
/**
* 用户等级选项
*/
const LEVEL_OPTIONS: OptionItem[] = [
{ label: '普通用户', value: 'normal' },
{ label: 'VIP用户', value: 'vip' },
{ label: '高级VIP', value: 'svip' },
{ label: '企业用户', value: 'enterprise', disabled: true }
]
/**
* 性别选项
*/
const GENDER_OPTIONS: OptionItem[] = [
{ label: '男', value: '1' },
{ label: '女', value: '2' }
]
/**
* 日期快捷选项
*/
const DATE_SHORTCUTS = [
{ text: '今日', value: new Date() },
{ text: '昨日', value: () => new Date(Date.now() - 86400000) },
{ text: '一周前', value: () => new Date(Date.now() - 604800000) }
]
/**
* 模拟接口获取用户等级数据
* @returns 用户等级选项列表
*/
const fetchLevelOptions = (): Promise<OptionItem[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(LEVEL_OPTIONS)
}, FETCH_DELAY)
})
}
/**
* 获取用户等级数据
*/
const getLevelOptions = async (): Promise<void> => {
levelOptions.value = await fetchLevelOptions()
if (levelOptions.value.length) {
ElMessage.success('成功获取到数据')
}
}
/**
* 表单项配置类型
*/
interface FormItemConfig {
label: string
key: string
type: string
placeholder?: string
props?: Record<string, any>
[key: string]: any
}
/**
* 创建表单项的工厂函数
*/
const createFormItem = (config: FormItemConfig) => config
// 基础表单项配置
const baseFormItems = {
username: createFormItem({
label: '用户名',
key: 'name',
type: 'input',
placeholder: '请输入用户名',
clearable: true
}),
phone: createFormItem({
label: '手机号',
key: 'phone',
type: 'input',
props: { placeholder: '请输入手机号', maxlength: '11' }
}),
level: createFormItem({
label: '用户等级',
key: 'level',
type: 'select',
props: {
placeholder: '请选择等级',
options: LEVEL_OPTIONS
}
}),
address: createFormItem({
label: '地址',
key: 'address',
type: 'input',
placeholder: '请输入地址'
}),
date: createFormItem({
label: '日期',
key: 'date',
type: 'datetime',
props: {
style: { width: '100%' },
placeholder: '请选择日期',
type: 'date',
valueFormat: 'YYYY-MM-DD',
shortcuts: DATE_SHORTCUTS
}
}),
gender: createFormItem({
label: '性别',
key: 'userGender',
type: 'radiogroup',
props: {
options: GENDER_OPTIONS
}
})
}
const userItem = ref<SearchFormItem>({
label: '用户名',
key: 'name',
type: 'input',
props: {
placeholder: '请输入用户名',
clearable: true
}
})
// 控制用户名字段是否显示
const showUserName = ref(true)
// 动态表单项列表
const dynamicFormItems = ref<SearchFormItem[]>([])
// 动态表单项计数器(用于生成唯一 key)
let dynamicItemCounter = 0
// 级联选择器数据
const cascaderOptions = [
{
value: 'guide',
label: '指南',
children: [
{
value: 'disciplines',
label: '规范',
children: [
{ value: 'consistency', label: '一致性' },
{ value: 'feedback', label: '反馈' },
{ value: 'efficiency', label: '效率' },
{ value: 'controllability', label: '可控性' }
]
}
]
},
{
value: 'components',
label: '组件',
children: [
{
value: 'basic',
label: '基础组件',
children: [
{ value: 'button', label: '按钮' },
{ value: 'form', label: '表单' },
{ value: 'table', label: '表格' }
]
}
]
}
]
// 树选择器数据
const treeSelectData = [
{
value: '1',
label: '一级 1',
children: [
{
value: '1-1',
label: '二级 1-1',
children: [{ value: '1-1-1', label: '三级 1-1-1' }]
}
]
},
{
value: '2',
label: '一级 2',
children: [
{
value: '2-1',
label: '二级 2-1',
children: [{ value: '2-1-1', label: '三级 2-1-1' }]
},
{
value: '2-2',
label: '二级 2-2',
children: [{ value: '2-2-1', label: '三级 2-2-1' }]
}
]
}
]
// 复选框选项
const checkboxOptions = [
{ label: '选项1', value: 'option1' },
{ label: '选项2', value: 'option2' },
{ label: '选项3', value: 'option3' },
{ label: '选项4', value: 'option4' },
{ label: '选项5(disabled)', value: 'option5', disabled: true }
]
// 表单配置
const formItems = computed(() => [
...(showUserName.value ? [userItem.value] : []),
// 动态表单项
...dynamicFormItems.value,
{
...baseFormItems.phone
},
{
...baseFormItems.level,
props: { placeholder: '请选择等级', options: levelOptions.value }
},
baseFormItems.address,
baseFormItems.date,
// 日期时间
{
label: '日期时间',
key: 'datetime',
type: 'datetime',
props: {
style: { width: '100%' },
placeholder: '请选择日期时间',
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
},
{
label: '日期范围',
key: 'daterange',
type: 'datetime',
props: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期'
}
},
// 日期时间范围
{
label: '日期时间范围',
key: 'datetimerange',
type: 'datetime',
props: {
type: 'datetimerange',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
rangeSeparator: '至',
startPlaceholder: '开始日期时间',
endPlaceholder: '结束日期时间'
}
},
// 时间选择
{
label: '时间选择',
key: 'timeselect',
type: 'timeselect',
props: {
placeholder: '请选择时间',
type: 'time',
valueFormat: 'HH:mm:ss'
}
},
// 时间选择器
{
label: '时间选择器',
key: 'timepicker',
type: 'timepicker',
props: {
style: { width: '100%' },
placeholder: '请选择时间',
type: 'time',
valueFormat: 'HH:mm:ss'
}
},
// 级联选择
{
label: '级联选择',
key: 'cascader',
type: 'cascader',
props: {
placeholder: '请选择级联选择器',
clearable: true,
style: { width: '100%' },
collapseTags: true,
maxCollapseTags: 1,
props: { multiple: true },
options: cascaderOptions
}
},
// 树型选择器
{
label: '树型选择器',
key: 'treeSelect',
type: 'treeselect',
props: {
showCheckbox: true,
multiple: true,
clearable: true,
data: treeSelectData
}
},
{ label: '插槽', key: 'slots', type: 'input', placeholder: '请输入邮箱' },
{
label: '渲染组件',
key: 'iconSelector',
render: () => h(ElInput, { placeholder: '渲染自定义 input' })
},
{
label: '自定义组件',
key: 'customComponent',
render: () =>
h(
'div',
{
style:
'color: var(--art-gray-600); border: 1px solid var(--default-border-dashed); padding: 0px 15px; border-radius: 6px'
},
'我是一个自定义组件'
)
},
{
label: '复选框',
key: 'checkboxgroup',
type: 'checkboxgroup',
span: 12,
props: {
options: checkboxOptions
}
},
{
...baseFormItems.gender
},
{
label: '是否启用',
key: 'isEnabled',
type: 'switch',
props: {
placeholder: '请选择是否启用'
}
},
{
label: '年龄',
key: 'age',
type: 'number',
slots: {
suffix: () => h('span', { style: 'color: #909399; font-size: 12px' }, '岁')
}
},
{
label: '网站地址',
key: 'website',
type: 'input',
placeholder: '请输入网站名称',
slots: {
prepend: () => h('span', 'https://'),
append: () => h('span', '.com')
}
},
{
label: '事件演示',
key: 'event',
type: 'input',
props: {
placeholder: '输入内容触发事件,控制台查看',
clearable: true,
prefixIcon: 'Search',
// prefix: () => h('span', {}, '123'),
// 事件必须以 on 开头,然后驼峰式命名拼接 ElementPlus 事件名
onInput(val: string) {
console.log('输入事件', val)
},
onClear() {
console.log('清空事件')
}
}
},
{
label: '多行输入',
key: 'remark',
type: 'input',
props: {
placeholder: '请输入备注',
type: 'textarea',
rows: 2
}
},
{
label: '评分',
key: 'rate',
type: 'rate',
props: {
size: 'large',
placeholder: '请选择评分'
}
},
{
label: '禁用',
key: 'diaabled',
type: 'input',
placeholder: '我被禁用了',
disabled: true // 禁用
},
{
label: '滑块',
key: 'slider',
type: 'slider'
// props: {
// step: 10,
// showStops: true
// }
},
{
label: '隐藏',
key: 'email',
type: 'input',
hidden: true
},
// 根据条件隐藏
{
label: '根据条件隐藏',
key: 'systemName',
type: 'input',
hidden: formData.value.systemName === 'mac',
placeholder: '输入 mac 组件隐藏'
},
{
label: '栅格布局',
key: 'sg1',
type: 'input',
span: 12,
placeholder: '示例:栅格 span=12 占容器一半宽度,span=24 占满容器'
},
// 文件上传示例 - 使用 render 函数渲染
{
label: '文件上传',
key: 'multipleFiles',
span: 12,
render: () =>
h(
ElUpload,
{
multiple: true,
limit: 5,
action: '#',
autoUpload: false,
showFileList: true,
// accept: '.pdf,.doc,.docx,.txt',
beforeUpload: (file: File) => {
console.log('准备上传文件:', file.name)
return true
},
onChange: (file: UploadFile, fileList: UploadFiles) => {
console.log('多文件变化:', file, fileList)
formData.value.multipleFiles = fileList as UploadUserFile[]
},
onRemove: (file: UploadFile, fileList: UploadFiles) => {
console.log('删除文件:', file, fileList)
formData.value.multipleFiles = fileList as UploadUserFile[]
},
onExceed: (files: File[], fileList: UploadUserFile[]) => {
ElMessage.warning(
`最多只能上传 5 个文件,当前选择了 ${files.length + fileList.length} 个文件`
)
}
},
{
default: () => [h(ElButton, { type: 'primary' }, () => '点击上传')]
}
)
},
// 图片上传示例 - 使用 render 函数渲染
{
label: '图片上传',
key: 'imageUpload',
span: 12,
render: () =>
h(
ElUpload,
{
accept: '.jpg,.jpeg,.png,.gif,.webp',
limit: 4,
action: '#',
autoUpload: false,
showFileList: true,
listType: 'picture-card',
beforeUpload: (file: File) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
return true
},
onChange: (file: UploadFile, fileList: UploadFiles) => {
console.log('图片变化:', file, fileList)
formData.value.imageUpload = fileList as UploadUserFile[]
},
onRemove: (file: UploadFile, fileList: UploadFiles) => {
console.log('删除图片:', file, fileList)
formData.value.imageUpload = fileList as UploadUserFile[]
},
onPreview: (file: UploadFile) => {
dialogImageUrl.value = file.url || ''
dialogVisible.value = true
}
},
{
default: () => [h(ElIcon, { type: 'primary' }, () => h(Plus))]
}
)
},
// 富文本编辑器示例 - 使用 render 函数渲染
{
label: '富文本编辑器',
key: 'richTextContent',
span: 24,
render: () =>
h(ArtWangEditor, {
modelValue: formData.value.richTextContent,
height: '300px',
placeholder: '请输入富文本内容...',
'onUpdate:modelValue': (value: string) => {
formData.value.richTextContent = value
console.log('富文本内容变化:', value)
},
toolbarKeys: [
'headerSelect',
'bold',
'italic',
'underline',
'|',
'bulletedList',
'numberedList',
'|',
'insertLink',
'insertImage',
'|',
'undo',
'redo'
]
})
}
])
/**
* 处理表单重置事件
*/
const handleReset = (): void => {
console.log('重置表单')
emit('reset')
}
/**
* 处理表单提交事件
*/
const handleSubmit = async (): Promise<void> => {
await formRef.value.validate()
emit('search', formData.value)
console.log('表单数据', formData.value)
}
/**
* 校验表单
*/
const validateForm = () => formRef.value.validate()
/**
* 重置表单
*/
const resetForm = () => formRef.value.reset()
/**
* 更新用户名字段配置
*/
const updateUserName = (): void => {
userItem.value = {
...userItem.value,
label: '昵称',
props: {
placeholder: '请输入昵称'
}
}
}
/**
* 删除用户名字段
*/
const deleteUserName = (): void => {
showUserName.value = false
formData.value.name = undefined
}
/**
* 新增表单项
*/
const addFormItem = (): void => {
dynamicItemCounter++
const newItem: SearchFormItem = {
label: `动态字段${dynamicItemCounter}`,
key: `dynamic_${dynamicItemCounter}`,
type: 'input',
props: {
placeholder: `请输入动态字段${dynamicItemCounter}`,
clearable: true
}
}
dynamicFormItems.value.push(newItem)
ElMessage.success(`已新增表单项:${newItem.label}`)
}
/**
* 修改表单项(修改最后一个动态表单项)
*/
const updateFormItem = (): void => {
if (dynamicFormItems.value.length === 0) {
ElMessage.warning('没有可修改的动态表单项,请先新增')
return
}
const lastIndex = dynamicFormItems.value.length - 1
const lastItem = dynamicFormItems.value[lastIndex]
// 修改最后一个表单项的配置
dynamicFormItems.value[lastIndex] = {
...lastItem,
label: `已修改`,
type: 'select',
props: {
placeholder: '修改为下拉选择',
options: [
{ label: '选项A', value: 'a' },
{ label: '选项B', value: 'b' },
{ label: '选项C', value: 'c' }
]
}
}
ElMessage.success(`已修改表单项:${lastItem.label}`)
}
/**
* 删除表单项(删除最后一个动态表单项)
*/
const deleteFormItem = (): void => {
if (dynamicFormItems.value.length === 0) {
ElMessage.warning('没有可删除的动态表单项')
return
}
const deletedItem = dynamicFormItems.value.pop()
if (deletedItem) {
// 清除对应的表单数据
delete formData.value[deletedItem.key as keyof FormData]
ElMessage.success(`已删除表单项:${deletedItem.label}`)
}
}
/**
* 批量新增表单项
*/
const batchAddFormItems = (): void => {
const batchItems: SearchFormItem[] = [
{
label: '公司名称',
key: `company_${++dynamicItemCounter}`,
type: 'input',
props: {
placeholder: '请输入公司名称',
clearable: true
}
},
{
label: '部门',
key: `department_${++dynamicItemCounter}`,
type: 'select',
props: {
placeholder: '请选择部门',
options: [
{ label: '技术部', value: 'tech' },
{ label: '产品部', value: 'product' },
{ label: '运营部', value: 'operation' }
]
}
},
{
label: '入职日期',
key: `joinDate_${++dynamicItemCounter}`,
type: 'datetime',
props: {
style: { width: '100%' },
placeholder: '请选择入职日期',
type: 'date',
valueFormat: 'YYYY-MM-DD'
}
}
]
dynamicFormItems.value.push(...batchItems)
ElMessage.success(`已批量新增 ${batchItems.length} 个表单项`)
}
/**
* 重置动态表单项
*/
const resetDynamicItems = (): void => {
if (dynamicFormItems.value.length === 0) {
ElMessage.info('当前没有动态表单项')
return
}
// 清除所有动态表单项的数据
dynamicFormItems.value.forEach((item) => {
delete formData.value[item.key as keyof FormData]
})
dynamicFormItems.value = []
dynamicItemCounter = 0
ElMessage.success('已重置所有动态表单项')
}
</script>
@@ -0,0 +1,841 @@
<!-- 表格搜索栏示例 -->
<template>
<div class="pb-5">
<h2 class="mb-1 text-lg font-medium">基础示例(默认收起)</h2>
<ArtSearchBar
ref="searchBarBasicRef"
v-model="formDataBasic"
:items="formItemsBasic"
@reset="handleBasicReset"
@search="handleBasicSearch"
>
</ArtSearchBar>
<h2 class="mb-1 mt-3.5 text-lg font-medium">完整示例(默认展开)</h2>
<ArtSearchBar
ref="searchBarAdvancedRef"
v-model="formDataAdvanced"
:items="formItemsAdvanced"
:rules="rulesAdvanced"
:defaultExpanded="true"
:labelWidth="labelWidthAdvanced"
:labelPosition="labelPositionAdvanced"
:span="spanAdvanced"
:gutter="gutterAdvanced"
@reset="handleAdvancedReset"
@search="handleAdvancedSearch"
>
<template #slots>
<ElInput v-model="formDataAdvanced.slots" placeholder="我是插槽渲染出来的组件" />
</template>
</ArtSearchBar>
<div class="art-card p-5 !rounded-lg mt-5">
<pre><code>{{ formDataAdvanced }}</code></pre>
</div>
<div class="mt-3.5">
<h3 class="mb-2 text-base font-medium">动态表单操作</h3>
<ElSpace wrap class="mb-3">
<ElButton @click="getLevelOptions"> 获取用户等级数据 </ElButton>
<ElButton @click="addFormItem"> 新增表单项 </ElButton>
<ElButton @click="updateFormItem"> 修改表单项 </ElButton>
<ElButton @click="deleteFormItem"> 删除表单项 </ElButton>
<ElButton @click="batchAddFormItems"> 批量新增 </ElButton>
<ElButton @click="resetDynamicItems"> 重置动态项 </ElButton>
</ElSpace>
<h3 class="mb-2 text-base font-medium">其他操作</h3>
<ElSpace wrap>
<ElButton @click="advancedValidate"> 校验表单 </ElButton>
<ElButton @click="advancedReset"> 重置 </ElButton>
<ElButton v-if="showUserName" @click="updateUserName"> 修改用户名 </ElButton>
<ElButton v-if="showUserName" @click="deleteUserName"> 删除用户名 </ElButton>
<ElButton @click="labelWidthAdvanced = 120"> 修改 label 宽度 </ElButton>
<ElButton @click="spanAdvanced = 8"> 设置一行显示的组件数 </ElButton>
<ElButton @click="gutterAdvanced = 50"> 修改 gutter </ElButton>
<ElButton @click="labelPositionAdvanced = 'left'"> label 左对齐 </ElButton>
<ElButton @click="labelPositionAdvanced = 'right'"> label 右对齐 </ElButton>
<ElButton @click="labelPositionAdvanced = 'top'"> label 顶部对齐 </ElButton>
</ElSpace>
</div>
</div>
</template>
<script setup lang="ts">
import { ElInput } from 'element-plus'
import { SearchFormItem } from '@/components/core/forms/art-search-bar/index.vue'
interface Emits {
(e: 'update:modelValue', value: Record<string, any>): void
(e: 'search', params: Record<string, any>): void
(e: 'reset'): void
}
interface OptionItem {
label: string
value: string
disabled?: boolean
}
interface BasicFormData {
name?: string
phone?: string
level?: string
address?: string
date?: string
daterange?: string[]
status?: boolean
}
interface AdvancedFormData extends BasicFormData {
slots?: string
cascader?: string[]
checkboxgroup?: string[]
userGender?: string
iconSelector?: string
systemName?: string
}
const emit = defineEmits<Emits>()
const FETCH_DELAY = 500
const searchBarBasicRef = ref()
const searchBarAdvancedRef = ref()
/**
* 基础示例表单数据
*/
const formDataBasic = ref<BasicFormData>({
name: undefined,
phone: undefined,
level: undefined,
address: undefined,
date: undefined,
daterange: undefined,
status: undefined
})
/**
* 完整示例表单数据
*/
const formDataAdvanced = ref<AdvancedFormData>({
name: undefined,
phone: undefined,
level: undefined,
address: undefined,
slots: undefined,
date: undefined,
daterange: undefined,
cascader: undefined,
checkboxgroup: undefined,
userGender: undefined,
iconSelector: undefined,
status: undefined,
systemName: undefined
})
/**
* 完整示例校验规则
*/
const rulesAdvanced = {
name: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ min: 11, max: 11, message: '请输入11位手机号', trigger: 'blur' },
{ pattern: /^1[3456789]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
level: [{ required: true, message: '请选择等级', trigger: 'change' }],
address: [{ required: true, message: '请输入地址', trigger: 'blur' }]
}
const labelWidthAdvanced = ref(100)
const labelPositionAdvanced = ref<'right' | 'left' | 'top'>('right')
const spanAdvanced = ref(6)
const gutterAdvanced = ref(12)
const levelOptions = ref<OptionItem[]>([])
/**
* 用户等级选项
*/
const LEVEL_OPTIONS: OptionItem[] = [
{ label: '普通用户', value: 'normal' },
{ label: 'VIP用户', value: 'vip' },
{ label: '高级VIP', value: 'svip' },
{ label: '企业用户', value: 'enterprise', disabled: true }
]
/**
* 性别选项
*/
const GENDER_OPTIONS: OptionItem[] = [
{ label: '男', value: '1' },
{ label: '女', value: '2' }
]
/**
* 日期快捷选项
*/
const DATE_SHORTCUTS = [
{ text: '今日', value: new Date() },
{ text: '昨日', value: () => new Date(Date.now() - 86400000) },
{ text: '一周前', value: () => new Date(Date.now() - 604800000) }
]
/**
* 模拟接口获取用户等级数据
* @returns 用户等级选项列表
*/
const fetchLevelOptions = (): Promise<OptionItem[]> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve(LEVEL_OPTIONS)
}, FETCH_DELAY)
})
}
/**
* 获取用户等级数据
*/
const getLevelOptions = async (): Promise<void> => {
levelOptions.value = await fetchLevelOptions()
if (levelOptions.value.length) {
ElMessage.success('成功获取到数据')
}
}
/**
* 表单项配置类型
*/
interface FormItemConfig {
label: string
key: string
type: string
placeholder?: string
clearable?: boolean
props?: Record<string, any>
[key: string]: any
}
/**
* 创建表单项的工厂函数
*/
const createFormItem = (config: FormItemConfig) => config
// 基础表单项配置
const baseFormItems = {
username: createFormItem({
label: '用户名',
key: 'name',
type: 'input',
placeholder: '请输入用户名',
clearable: true
}),
phone: createFormItem({
label: '手机号',
key: 'phone',
type: 'input',
props: { placeholder: '请输入手机号', maxlength: '11' }
}),
level: createFormItem({
label: '用户等级',
key: 'level',
type: 'select',
props: {
placeholder: '请选择等级',
options: LEVEL_OPTIONS
}
}),
address: createFormItem({
label: '地址',
key: 'address',
type: 'input',
placeholder: '请输入地址'
}),
date: createFormItem({
label: '日期',
key: 'date',
type: 'datetime',
props: {
style: { width: '100%' },
placeholder: '请选择日期',
type: 'date',
valueFormat: 'YYYY-MM-DD',
shortcuts: DATE_SHORTCUTS
}
}),
gender: createFormItem({
label: '性别',
key: 'userGender',
type: 'radiogroup',
props: {
options: GENDER_OPTIONS
}
})
}
// 表单配置
const formItemsBasic = computed(() => [
baseFormItems.username,
{
label: '密码',
key: 'password',
type: 'input',
props: {
type: 'password',
placeholder: '请输入密码',
clearable: true
}
},
baseFormItems.phone,
baseFormItems.level,
baseFormItems.address,
baseFormItems.date,
baseFormItems.gender
])
const userItem = ref<SearchFormItem>({
label: '用户名',
key: 'name',
type: 'input',
props: {
placeholder: '请输入用户名',
clearable: true
}
})
// 控制用户名字段是否显示
const showUserName = ref(true)
// 动态表单项列表
const dynamicFormItems = ref<SearchFormItem[]>([])
// 动态表单项计数器(用于生成唯一 key)
let dynamicItemCounter = 0
// 级联选择器数据
const cascaderOptions = [
{
value: 'guide',
label: '指南',
children: [
{
value: 'disciplines',
label: '规范',
children: [
{ value: 'consistency', label: '一致性' },
{ value: 'feedback', label: '反馈' },
{ value: 'efficiency', label: '效率' },
{ value: 'controllability', label: '可控性' }
]
}
]
},
{
value: 'components',
label: '组件',
children: [
{
value: 'basic',
label: '基础组件',
children: [
{ value: 'button', label: '按钮' },
{ value: 'form', label: '表单' },
{ value: 'table', label: '表格' }
]
}
]
}
]
// 树选择器数据
const treeSelectData = [
{
value: '1',
label: '一级 1',
children: [
{
value: '1-1',
label: '二级 1-1',
children: [{ value: '1-1-1', label: '三级 1-1-1' }]
}
]
},
{
value: '2',
label: '一级 2',
children: [
{
value: '2-1',
label: '二级 2-1',
children: [{ value: '2-1-1', label: '三级 2-1-1' }]
},
{
value: '2-2',
label: '二级 2-2',
children: [{ value: '2-2-1', label: '三级 2-2-1' }]
}
]
}
]
// 复选框选项
const checkboxOptions = [
{ label: '选项1', value: 'option1' },
{ label: '选项2', value: 'option2' },
{ label: '选项3', value: 'option3' },
{ label: '选项4', value: 'option4' },
{ label: '选项5(disabled)', value: 'option5', disabled: true }
]
// 完整示例表单配置
const formItemsAdvanced = computed(() => [
...(showUserName.value ? [userItem.value] : []),
// 动态表单项
...dynamicFormItems.value,
{
...baseFormItems.phone
},
{
...baseFormItems.level,
props: { placeholder: '请选择等级', options: levelOptions.value }
},
baseFormItems.address,
baseFormItems.date,
// 日期时间
{
label: '日期时间',
key: 'datetime',
type: 'datetime',
props: {
style: { width: '100%' },
placeholder: '请选择日期时间',
type: 'datetime',
valueFormat: 'YYYY-MM-DD HH:mm:ss'
}
},
{
label: '日期范围',
key: 'daterange',
type: 'datetime',
props: {
type: 'daterange',
valueFormat: 'YYYY-MM-DD',
rangeSeparator: '至',
startPlaceholder: '开始日期',
endPlaceholder: '结束日期'
}
},
// 日期时间范围
{
label: '日期时间范围',
key: 'datetimerange',
type: 'datetime',
props: {
type: 'datetimerange',
valueFormat: 'YYYY-MM-DD HH:mm:ss',
rangeSeparator: '至',
startPlaceholder: '开始日期时间',
endPlaceholder: '结束日期时间'
}
},
// 时间选择
{
label: '时间选择',
key: 'timeselect',
type: 'timeselect',
props: {
placeholder: '请选择时间',
type: 'time',
valueFormat: 'HH:mm:ss'
}
},
// 时间选择器
{
label: '时间选择器',
key: 'timepicker',
type: 'timepicker',
props: {
style: { width: '100%' },
placeholder: '请选择时间',
type: 'time',
valueFormat: 'HH:mm:ss'
}
},
// 级联选择
{
label: '级联选择',
key: 'cascader',
type: 'cascader',
props: {
placeholder: '请选择级联选择器',
clearable: true,
style: { width: '100%' },
collapseTags: true,
maxCollapseTags: 1,
props: { multiple: true },
options: cascaderOptions
}
},
// 树型选择器
{
label: '树型选择器',
key: 'treeSelect',
type: 'treeselect',
props: {
showCheckbox: true,
multiple: true,
clearable: true,
data: treeSelectData
}
},
{ label: '插槽', key: 'slots', type: 'input', placeholder: '请输入邮箱' },
{
label: '渲染组件',
key: 'iconSelector',
render: () => h(ElInput, { placeholder: '渲染自定义 input' })
},
{
label: '自定义组件',
key: 'customComponent',
render: () =>
h(
'div',
{
style:
'color: var(--art-gray-600); border: 1px solid var(--default-border-dashed); padding: 0px 15px; border-radius: 6px'
},
'我是一个自定义组件'
)
},
{
label: '复选框',
key: 'checkboxgroup',
type: 'checkboxgroup',
span: 12,
props: {
options: checkboxOptions
}
},
{
...baseFormItems.gender
},
{
label: '是否启用',
key: 'isEnabled',
type: 'switch',
props: {
placeholder: '请选择是否启用'
}
},
{
label: '年龄',
key: 'age',
type: 'number',
slots: {
suffix: () => h('span', { style: 'color: #909399; font-size: 12px' }, '岁')
}
},
{
label: '网站地址',
key: 'website',
type: 'input',
placeholder: '请输入网站名称',
slots: {
prepend: () => h('span', 'https://'),
append: () => h('span', '.com')
}
},
{
label: '事件演示',
key: 'event',
type: 'input',
props: {
placeholder: '输入内容触发事件,控制台查看',
clearable: true,
prefixIcon: 'Search',
// prefix: () => h('span', {}, '123'),
// 事件必须以 on 开头,然后驼峰式命名拼接 ElementPlus 事件名
onInput(val: string) {
console.log('输入事件', val)
},
onClear() {
console.log('清空事件')
}
}
},
{
label: '多行输入',
key: 'remark',
type: 'input',
props: {
placeholder: '请输入备注',
type: 'textarea',
rows: 2
}
},
{
label: '评分',
key: 'rate',
type: 'rate',
props: {
size: 'large',
placeholder: '请选择评分'
}
},
{
label: '禁用',
key: 'diaabled',
type: 'input',
placeholder: '我被禁用了',
disabled: true // 禁用
},
{
label: '滑块',
key: 'slider',
type: 'slider'
// props: {
// step: 10,
// showStops: true
// }
},
{
label: '隐藏',
key: 'email',
type: 'input',
hidden: true
},
// 根据条件隐藏
{
label: '根据条件隐藏',
key: 'systemName',
type: 'input',
hidden: formDataAdvanced.value.systemName === 'mac',
placeholder: '输入 mac 组件隐藏'
},
{
label: '栅格布局',
key: 'sg1',
type: 'input',
span: 12,
placeholder: '示例:栅格 span=12 占容器一半宽度,span=24 占满容器'
}
])
/**
* 创建统一的表单处理函数
* @param ref 表单引用
* @param formData 表单数据
* @param type 表单类型描述
*/
const createFormHandler = (ref: Ref<any>, formData: Record<string, any>, type: string) => ({
reset: () => {
console.log(`重置${type}表单`)
emit('reset')
},
search: async () => {
await ref.value.validate()
emit('search', formData.value)
console.log(`${type}表单数据`, formData.value)
},
validate: () => ref.value.validate()
})
/**
* 基础表单处理器
*/
const basicFormHandler = computed(() =>
createFormHandler(searchBarBasicRef, formDataBasic, '基础')
)
/**
* 完整表单处理器
*/
const advancedFormHandler = computed(() =>
createFormHandler(searchBarAdvancedRef, formDataAdvanced, '完整')
)
/**
* 处理基础表单重置事件
*/
const handleBasicReset = () => basicFormHandler.value.reset()
/**
* 处理基础表单搜索事件
*/
const handleBasicSearch = () => basicFormHandler.value.search()
/**
* 处理完整表单重置事件
*/
const handleAdvancedReset = () => advancedFormHandler.value.reset()
/**
* 处理完整表单搜索事件
*/
const handleAdvancedSearch = () => advancedFormHandler.value.search()
/**
* 校验完整表单
*/
const advancedValidate = () => advancedFormHandler.value.validate()
/**
* 重置完整表单
*/
const advancedReset = () => searchBarAdvancedRef.value.reset()
/**
* 更新用户名字段配置
*/
const updateUserName = (): void => {
userItem.value = {
...userItem.value,
label: '昵称',
props: {
placeholder: '请输入昵称'
}
}
}
/**
* 删除用户名字段
*/
const deleteUserName = (): void => {
showUserName.value = false
formDataAdvanced.value.name = undefined
}
/**
* 新增表单项
*/
const addFormItem = (): void => {
dynamicItemCounter++
const newItem: SearchFormItem = {
label: `动态字段${dynamicItemCounter}`,
key: `dynamic_${dynamicItemCounter}`,
type: 'input',
props: {
placeholder: `请输入动态字段${dynamicItemCounter}`,
clearable: true
}
}
dynamicFormItems.value.push(newItem)
ElMessage.success(`已新增表单项:${newItem.label}`)
}
/**
* 修改表单项(修改最后一个动态表单项)
*/
const updateFormItem = (): void => {
if (dynamicFormItems.value.length === 0) {
ElMessage.warning('没有可修改的动态表单项,请先新增')
return
}
const lastIndex = dynamicFormItems.value.length - 1
const lastItem = dynamicFormItems.value[lastIndex]
// 修改最后一个表单项的配置
dynamicFormItems.value[lastIndex] = {
...lastItem,
label: `已修改`,
type: 'select',
props: {
placeholder: '修改为下拉选择',
options: [
{ label: '选项A', value: 'a' },
{ label: '选项B', value: 'b' },
{ label: '选项C', value: 'c' }
]
}
}
ElMessage.success(`已修改表单项:${lastItem.label}`)
}
/**
* 删除表单项(删除最后一个动态表单项)
*/
const deleteFormItem = (): void => {
if (dynamicFormItems.value.length === 0) {
ElMessage.warning('没有可删除的动态表单项')
return
}
const deletedItem = dynamicFormItems.value.pop()
if (deletedItem) {
// 清除对应的表单数据
delete formDataAdvanced.value[deletedItem.key as keyof AdvancedFormData]
ElMessage.success(`已删除表单项:${deletedItem.label}`)
}
}
/**
* 批量新增表单项
*/
const batchAddFormItems = (): void => {
const batchItems: SearchFormItem[] = [
{
label: '公司名称',
key: `company_${++dynamicItemCounter}`,
type: 'input',
props: {
placeholder: '请输入公司名称',
clearable: true
}
},
{
label: '部门',
key: `department_${++dynamicItemCounter}`,
type: 'select',
props: {
placeholder: '请选择部门',
options: [
{ label: '技术部', value: 'tech' },
{ label: '产品部', value: 'product' },
{ label: '运营部', value: 'operation' }
]
}
},
{
label: '入职日期',
key: `joinDate_${++dynamicItemCounter}`,
type: 'datetime',
props: {
style: { width: '100%' },
placeholder: '请选择入职日期',
type: 'date',
valueFormat: 'YYYY-MM-DD'
}
}
]
dynamicFormItems.value.push(...batchItems)
ElMessage.success(`已批量新增 ${batchItems.length} 个表单项`)
}
/**
* 重置动态表单项
*/
const resetDynamicItems = (): void => {
if (dynamicFormItems.value.length === 0) {
ElMessage.info('当前没有动态表单项')
return
}
// 清除所有动态表单项的数据
dynamicFormItems.value.forEach((item) => {
delete formDataAdvanced.value[item.key as keyof AdvancedFormData]
})
dynamicFormItems.value = []
dynamicItemCounter = 0
ElMessage.success('已重置所有动态表单项')
}
</script>
@@ -0,0 +1,529 @@
<!-- 按钮权限示例页面 -->
<template>
<div class="w-full py-2">
<!-- 页面头部 -->
<div class="mb-6">
<h2 class="m-0 mb-2 text-xl font-medium">{{ $t('menus.examples.permission.buttonAuth') }}</h2>
<p class="m-0 text-sm leading-[1.6] text-g-700">
此页面演示按钮级别的权限控制,不同用户身份下可见的按钮会有所不同。权限控制基于用户角色和权限码实现。
</p>
</div>
<div class="mb-6">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-cb font-semibold">
<span class="flex-1">当前用户权限信息</span>
</div>
</template>
<div>
<div class="flex items-start mb-4 last:mb-0">
<span class="min-w-20 font-semibold">用户角色:</span>
<ElTag :type="getRoleTagType(currentUserRole)">
{{ getRoleDisplayName(currentUserRole) }}
</ElTag>
</div>
<div class="flex items-start mb-4 last:mb-0">
<span class="min-w-20 font-semibold">权限码:</span>
<div class="flex flex-wrap gap-2">
<ElTag
v-for="permission in currentUserPermissions"
:key="permission"
size="small"
type="info"
class="m-0"
>
{{ permission }}
</ElTag>
<span v-if="!currentUserPermissions.length" class="italic text-red-500"
>无权限码</span
>
</div>
</div>
</div>
</ElCard>
</div>
<!-- 基于角色的权限控制演示 -->
<div class="mb-6 last:mb-0">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-cb font-semibold">
<span class="flex-1">基于角色的权限控制(v-roles 指令)</span>
</div>
</template>
<div>
<p class="p-3 m-0 mb-5 text-sm leading-[1.6] text-g-700 bg-g-200 rounded">
使用
<code class="px- 1.5 py-0.5 font-mono text-xs text-theme bg-theme/10 rounded"
>v-roles</code
>
指令控制按钮显示,只有拥有指定角色的用户才能看到对应按钮。
</p>
<div class="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-5">
<div class="flex flex-col gap-2">
<ElButton type="primary" plain v-roles="'R_SUPER'"> 超级管理员可见 </ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-roles="'R_SUPER'"</code
>
<span class="text-g-700">只有超级管理员可见</span>
</div>
</div>
<!-- 管理员级别按钮 -->
<div class="flex flex-col gap-2">
<ElButton type="warning" plain v-roles="['R_SUPER', 'R_ADMIN']">
管理员可见
</ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-roles="['R_SUPER', 'R_ADMIN']"</code
>
<span class="text-g-700">超级管理员和管理员可见</span>
</div>
</div>
<!-- 所有已登录用户可见 -->
<div class="flex flex-col gap-2">
<ElButton type="success" plain v-roles="['R_SUPER', 'R_ADMIN', 'R_USER']">
所有用户可见
</ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-roles="['R_SUPER', 'R_ADMIN', 'R_USER']"</code
>
<span class="text-g-700">所有已登录用户可见</span>
</div>
</div>
</div>
</div>
</ElCard>
</div>
<!-- 后端模式:基于路由权限配置的控制演示 -->
<div class="mb-6 last:mb-0">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-cb font-semibold">
<span class="flex-1">后端模式权限控制(v-auth 指令)</span>
<ElTag v-if="!isFrontendMode" type="success" size="small">当前模式</ElTag>
<ElTag v-else type="info" size="small">非当前模式</ElTag>
</div>
</template>
<div>
<p class="p-3 m-0 mb-5 text-sm leading-[1.6] text-g-700 bg-g-200 rounded">
<code class="px- 1.5 py-0.5 font-mono text-xs text-theme bg-theme/10 rounded"
>v-auth</code
>
指令在后端模式下根据当前路由的
<code class="px- 1.5 py-0.5 font-mono text-xs text-theme bg-theme/10 rounded"
>meta.authList</code
>
配置检查权限。 权限列表来源于路由元数据,适用于权限配置由后端统一管理的场景。
</p>
<div class="mb-5">
<h4 class="m-0 mb-2 text-sm font-semibold"> 当前路由权限配置: </h4>
<div
class="max-h-30 p-3 overflow-y-auto font-mono text-xs break-all whitespace-pre-wrap bg-g-200 border-full-d rounded"
>
<code>{{ JSON.stringify(backendAuthList, null, 2) }}</code>
</div>
</div>
<div class="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-5">
<!-- 新增权限 -->
<div class="flex flex-col gap-2">
<ElButton type="primary" plain v-auth="'add'"> 新增 </ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-auth="'add'"</code
>
<span class="text-g-700">检查路由 meta.authList 中是否存在 authMark: 'add'</span>
</div>
</div>
<!-- 编辑权限 -->
<div class="flex flex-col gap-2">
<ElButton type="warning" plain v-auth="'edit'"> 编辑 </ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-auth="'edit'"</code
>
<span class="text-g-700">检查路由 meta.authList 中是否存在 authMark: 'edit'</span>
</div>
</div>
<!-- 删除权限 -->
<div class="flex flex-col gap-2">
<ElButton type="danger" plain v-auth="'delete'"> 删除 </ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-auth="'delete'"</code
>
<span class="text-g-700">检查路由 meta.authList 中是否存在 authMark: 'delete'</span>
</div>
</div>
<!-- 导出权限 -->
<div class="flex flex-col gap-2">
<ElButton type="info" plain v-auth="'export'"> 导出 </ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-auth="'export'"</code
>
<span class="text-g-700">检查路由 meta.authList 中是否存在 authMark: 'export'</span>
</div>
</div>
</div>
<div class="mt-4">
<ElAlert
:type="isFrontendMode ? 'warning' : 'success'"
:title="
isFrontendMode
? '注意:当前为前端模式,v-auth 指令在后端模式下才生效'
: '当前为后端模式,v-auth 指令正常工作'
"
show-icon
:closable="false"
/>
</div>
</div>
</ElCard>
</div>
<!-- 前端模式:编程式权限控制演示 -->
<div class="mb-6 last:mb-0">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-cb font-semibold">
<span class="flex-1">前端模式权限控制(hasAuth 方法)</span>
<ElTag v-if="isFrontendMode" type="success" size="small">当前模式</ElTag>
<ElTag v-else type="info" size="small">非当前模式</ElTag>
</div>
</template>
<div>
<p class="p-3 m-0 mb-5 text-sm leading-[1.6] text-g-700 bg-g-200 rounded">
<code class="px- 1.5 py-0.5 font-mono text-xs text-theme bg-theme/10 rounded"
>hasAuth</code
>
方法在前端模式下根据用户信息中的
<code class="px- 1.5 py-0.5 font-mono text-xs text-theme bg-theme/10 rounded"
>buttons</code
>
字段检查权限。 权限列表存储在用户状态中,适用于权限完全由前端管理的场景。
</p>
<div class="mb-5">
<h4 class="m-0 mb-2 text-sm font-semibold">当前用户权限列表(buttons 字段):</h4>
<div
class="max-h-30 p-3 overflow-y-auto font-mono text-xs break-all whitespace-pre-wrap bg-g-200 border-full-d rounded"
>
<code>{{ JSON.stringify(frontendAuthList, null, 2) }}</code>
</div>
</div>
<div class="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-5">
<!-- 条件渲染按钮 -->
<div class="flex flex-col gap-2">
<ElButton v-if="hasAuth('view')" type="primary"> 查看详情 </ElButton>
<ElButton v-else type="info" disabled> 无查看权限 </ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-if="hasAuth('view')"</code
>
<span class="text-g-700">检查用户 buttons 数组是否包含 'view'</span>
</div>
</div>
<!-- 动态按钮状态 -->
<div class="flex flex-col gap-2">
<ElButton
:disabled="!hasAuth('publish')"
:type="hasAuth('publish') ? 'success' : 'info'"
@click="handlePublish"
>
{{ hasAuth('publish') ? '发布' : '无发布权限' }}
</ElButton>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>:disabled="!hasAuth('publish')"</code
>
<span class="text-g-700">检查用户 buttons 数组是否包含 'publish'</span>
</div>
</div>
<!-- 批量操作按钮 -->
<div class="flex flex-col gap-2">
<ElDropdown @command="handleBatchAction" :disabled="!hasBatchPermissions">
<ElButton :type="hasBatchPermissions ? 'warning' : 'info'">
批量操作
<ElIcon class="el-icon--right">
<ArrowDown />
</ElIcon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="batchEdit" :disabled="!hasAuth('edit')">
批量编辑
</ElDropdownItem>
<ElDropdownItem command="batchDelete" :disabled="!hasAuth('delete')">
批量删除
</ElDropdownItem>
<ElDropdownItem command="batchExport" :disabled="!hasAuth('export')">
批量导出
</ElDropdownItem>
</ElDropdownMenu>
</template>
</ElDropdown>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>computed(() => hasAuth('edit') || hasAuth('delete'))</code
>
<span class="text-g-700">复合权限检查,检查多个权限的并集</span>
</div>
</div>
</div>
</div>
</ElCard>
</div>
<!-- 后端模式:编程式权限控制演示 -->
<div class="mb-6 last:mb-0">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-cb font-semibold">
<span class="flex-1">后端模式权限控制(hasAuth 方法)</span>
<ElTag v-if="!isFrontendMode" type="success" size="small">当前模式</ElTag>
<ElTag v-else type="info" size="small">非当前模式</ElTag>
</div>
</template>
<div>
<p class="p-3 m-0 mb-5 text-sm leading-[1.6] text-g-700 bg-g-200 rounded">
<code class="px- 1.5 py-0.5 font-mono text-xs text-theme bg-theme/10 rounded"
>hasAuth</code
>
方法在后端模式下根据当前路由的
<code class="px- 1.5 py-0.5 font-mono text-xs text-theme bg-theme/10 rounded"
>meta.authList</code
>
配置检查权限。与 v-auth 指令使用相同的权限来源,但提供编程式的权限检查能力。
</p>
<div class="grid grid-cols-[repeat(auto-fit,minmax(280px,1fr))] gap-5">
<!-- 动态功能切换 -->
<div class="flex flex-col gap-2">
<ElSwitch
v-model="dynamicFeatureEnabled"
:disabled="!hasAuth('config')"
active-text="功能开启"
inactive-text="功能关闭"
/>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>:disabled="!hasAuth('config')"</code
>
<span class="text-g-700">检查路由权限控制功能开关</span>
</div>
</div>
<!-- 条件显示操作区域 -->
<div class="flex flex-col gap-2">
<div v-if="hasAuth('manage')" class="flex gap-2 p-3 bg-g-200 border-full-d rounded">
<ElButton type="primary" size="small">管理操作</ElButton>
<ElButton type="warning" size="small">高级设置</ElButton>
</div>
<div
v-else
class="flex-c gap-2 p-3 text-g-500 bg-g-200 border border-dashed border-g-400 rounded"
>
<ElIcon><Lock /></ElIcon>
<span class="text-sm">需要管理权限才能查看此区域</span>
</div>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>v-if="hasAuth('manage')"</code
>
<span class="text-g-700">基于后端路由权限的条件渲染</span>
</div>
</div>
<!-- 权限状态指示器 -->
<div class="flex flex-col gap-2">
<div class="flex flex-wrap gap-3">
<ElBadge
:value="hasAuth('add') ? '✓' : '✗'"
:type="hasAuth('add') ? 'success' : 'danger'"
>
<ElButton size="small">新增权限</ElButton>
</ElBadge>
<ElBadge
:value="hasAuth('edit') ? '✓' : '✗'"
:type="hasAuth('edit') ? 'success' : 'danger'"
>
<ElButton size="small">编辑权限</ElButton>
</ElBadge>
<ElBadge
:value="hasAuth('delete') ? '✓' : '✗'"
:type="hasAuth('delete') ? 'success' : 'danger'"
>
<ElButton size="small">删除权限</ElButton>
</ElBadge>
</div>
<div class="text-xs">
<code class="block p-1 px-2 mb-1 font-mono bg-g-200 border-full-d rounded"
>hasAuth('permission')</code
>
<span class="text-g-700">实时权限状态指示器</span>
</div>
</div>
</div>
</div>
</ElCard>
</div>
<!-- 权限模式对比说明 -->
<div class="mb-6 last:mb-0">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-cb font-semibold">
<span class="flex-1">权限控制模式对比</span>
</div>
</template>
<div>
<div class="mt-4">
<ElTable :data="comparisonData" border>
<ElTableColumn prop="feature" label="功能特性" width="150" />
<ElTableColumn prop="frontend" label="前端模式" />
<ElTableColumn prop="backend" label="后端模式" />
</ElTable>
</div>
</div>
</ElCard>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ArrowDown, Lock } from '@element-plus/icons-vue'
import { useAuth } from '@/hooks/core/useAuth'
import { useUserStore } from '@/store/modules/user'
import { useAppMode } from '@/hooks/core/useAppMode'
import { useRoute } from 'vue-router'
import type { AppRouteRecord } from '@/types/router'
defineOptions({ name: 'PermissionButtonAuth' })
const { hasAuth } = useAuth()
const { isFrontendMode } = useAppMode()
const userStore = useUserStore()
const route = useRoute()
// 动态功能状态
const dynamicFeatureEnabled = ref(false)
// 当前用户角色
const currentUserRole = computed(() => {
return userStore.info?.roles?.[0] || ''
})
// 当前用户权限码
const currentUserPermissions = computed(() => {
return userStore.info?.buttons || []
})
// 前端模式权限列表(用户的 buttons 字段)
const frontendAuthList = computed(() => {
return userStore.info?.buttons || []
})
// 后端模式权限列表(路由 meta.authList 配置)
const backendAuthList = computed(() => {
type AuthItem = NonNullable<AppRouteRecord['meta']['authList']>[number]
return Array.isArray(route.meta.authList) ? (route.meta.authList as AuthItem[]) : []
})
// 是否拥有批量操作权限
const hasBatchPermissions = computed(() => {
return hasAuth('edit') || hasAuth('delete') || hasAuth('export')
})
// 权限对比数据
const comparisonData = computed(() => [
{
feature: '权限来源',
frontend: '用户状态中的 buttons 字段',
backend: '路由配置中的 meta.authList'
},
{
feature: 'v-auth 指令',
frontend: '不可用(前端模式下指令无效)',
backend: '可用(根据路由权限配置检查)'
},
{
feature: 'hasAuth 方法',
frontend: '可用(检查 buttons 数组)',
backend: '可用(检查 meta.authList)'
},
{
feature: '权限管理',
frontend: '完全由前端控制,灵活度高',
backend: '后端统一管理,安全性更高'
},
{
feature: '适用场景',
frontend: '快速原型、简单应用',
backend: '企业级应用、复杂权限体系'
}
])
// 获取角色标签类型
const getRoleTagType = (role: string): 'primary' | 'success' | 'info' | 'warning' | 'danger' => {
const roleMap: Record<string, 'primary' | 'success' | 'info' | 'warning' | 'danger'> = {
R_SUPER: 'warning',
R_ADMIN: 'primary',
R_USER: 'success'
}
return roleMap[role] || 'info'
}
// 获取角色显示名称
const getRoleDisplayName = (role: string) => {
const roleMap: Record<string, string> = {
R_SUPER: '超级管理员',
R_ADMIN: '管理员',
R_USER: '普通用户'
}
return roleMap[role] || '未知角色'
}
// 处理发布操作
const handlePublish = () => {
if (hasAuth('publish')) {
ElMessage.success('发布成功!')
} else {
ElMessage.warning('您没有发布权限')
}
}
// 处理批量操作
const handleBatchAction = (command: string) => {
const actions: Record<string, string> = {
batchEdit: '批量编辑',
batchDelete: '批量删除',
batchExport: '批量导出'
}
const permission = command.replace('batch', '').toLowerCase()
if (hasAuth(permission)) {
ElMessage.success(`${actions[command]}操作执行成功!`)
} else {
ElMessage.warning(`您没有${actions[command]}权限`)
}
}
</script>
@@ -0,0 +1,266 @@
<!-- 权限页面可见页面 -->
<template>
<div class="w-full py-2">
<!-- 页面头部 -->
<div class="mb-6">
<h2 class="m-0 mb-2 text-xl font-medium">
{{ $t("menus.examples.permission.pageVisibility") }}
</h2>
<p class="m-0 text-sm leading-[1.6] text-g-700">
此页面仅对<strong class="font-semibold text-warning">超级管理员</strong
>用户可见,演示页面级别的权限控制。 如果您能看到此页面,说明您拥有相应的访问权限。
</p>
</div>
<div class="mb-6">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-c gap-2 font-semibold">
<span>权限验证成功</span>
</div>
</template>
<div>
<div class="flex-c gap-5">
<div>
<h3 class="m-0 mb-2 text-lg font-semibold">您拥有访问此页面的权限</h3>
<p class="my-1 text-sm text-g-700">
当前用户:<strong class="font-semibold">{{
currentUser.userName
}}</strong>
</p>
<p class="my-1 text-sm text-g-700">
用户角色:
<ElTag type="warning">{{
getRoleDisplayName(currentUser.roles?.[0] || "")
}}</ElTag>
</p>
</div>
</div>
</div>
</ElCard>
</div>
<!-- 页面级权限控制说明 -->
<div class="mb-6 last:mb-0">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="flex-c font-semibold">
<span>页面级权限控制说明</span>
</div>
</template>
<div>
<ElTimeline>
<ElTimelineItem timestamp="前端控制模式" type="primary" size="large">
<ElCard shadow="never">
<h4 class="m-0 mb-2 text-base font-semibold">基于角色的权限控制</h4>
<p class="m-0 mb-2 leading-[1.6] text-g-700">
在前端控制模式下,页面访问权限由路由配置文件中的
<code
class="px-1.5 py-0.5 font-mono text-xs text-theme bg-theme/12 rounded"
>meta.roles</code
>
字段定义,前端会根据用户接口所拥有的角色对路由和菜单进行过滤与控制
</p>
<pre
class="p-4 mt-3 mb-0 overflow-x-auto font-mono text-xs leading-[1.5] bg-g-200 border-full-d rounded-md"
><code class="">{
path: 'page-visibility',
name: 'PermissionPageVisibility',
component: '/examples/permission/page-visibility',
meta: {
title: 'menus.permission.pageVisibility',
roles: ['R_SUPER'], // 仅超级管理员可访问
keepAlive: true
}
}</code></pre>
<p class="m-0 mb-2 leading-[1.6] text-g-700">
<strong>权限验证流程:</strong>
</p>
<ul class="pl-5 my-2">
<li class="my-1 leading-[1.5] text-g-700">
用户登录后,接口返回用户角色信息(如 R_SUPER、R_ADMIN、R_USER)
</li>
<li class="my-1 leading-[1.5] text-g-700">
在
<code
class="px-1.5 py-0.5 font-mono text-xs text-theme bg-theme/12 rounded"
>beforeEach</code
>
路由守卫中检查目标路由的
<code
class="px-1.5 py-0.5 font-mono text-xs text-theme bg-theme/12 rounded"
>roles</code
>
配置
</li>
<li class="my-1 leading-[1.5] text-g-700">
比较用户角色是否包含在允许访问的角色列表中
</li>
<li class="my-1 leading-[1.5] text-g-700">权限不足时跳转到 403 页面</li>
</ul>
</ElCard>
</ElTimelineItem>
<ElTimelineItem timestamp="后端控制模式" type="warning" size="large">
<ElCard shadow="never">
<h4 class="m-0 mb-2 text-base font-semibold">基于菜单接口的权限控制</h4>
<p class="m-0 mb-2 leading-[1.6] text-g-700">
在后端控制模式下,页面访问权限由后端统一管理,前端通过解析后端接口返回的菜单列表来生成可访问的路由,从而实现权限控制
</p>
<p class="m-0 mb-2 leading-[1.6] text-g-700">
接口地址:src/api/menuApi.ts getMenuList
</p>
<pre
class="p-4 mt-3 mb-0 overflow-x-auto font-mono text-xs leading-[1.5] bg-g-200 border-full-d rounded-md"
><code class="">
{
"code": 200,
"data": [
{
"id": 1,
"path": "/permission",
"name": "Permission",
"component": "Layout",
"meta": {
"title": "menus.permission.title",
"icon": ""
},
"children": [
{
"id": 11,
"path": "page-visibility",
"name": "PermissionPageVisibility",
"component": "permission/page-visibility/index",
"meta": {
"title": "menus.permission.pageVisibility",
"keepAlive": true
}
}
]
}
]
}</code></pre>
<p><strong>权限验证流程:</strong></p>
<ul>
<li>用户登录成功后获取 Token</li>
<li>前端调用菜单接口获取用户可访问的菜单列表</li>
<li>前端根据菜单列表动态注册路由</li>
<li>菜单中存在的页面用户可以正常访问,不存在的页面会跳转到 404</li>
</ul>
</ElCard>
</ElTimelineItem>
<ElTimelineItem timestamp="菜单显示控制" type="success" size="large">
<ElCard shadow="never">
<h4>侧边栏菜单可见性</h4>
<p><strong>前端控制模式:</strong></p>
<ul>
<li>有权限的用户:菜单项正常显示,可以点击访问</li>
<li>无权限的用户:菜单项不显示,无法通过菜单导航到页面</li>
<li>即使通过直接输入URL尝试访问,也会被路由守卫拦截</li>
</ul>
<p><strong>后端控制模式:</strong></p>
<ul>
<li>侧边栏菜单根据后端返回的菜单列表进行渲染</li>
<li>后端应该根据用户权限过滤,只返回用户有权限访问的菜单项</li>
<li>前端只显示后端返回的菜单,确保用户只能看到和访问有权限的页面</li>
</ul>
</ElCard>
</ElTimelineItem>
</ElTimeline>
</div>
</ElCard>
</div>
<!-- 权限控制最佳实践 -->
<div class="best-practices">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="card-header">
<span>权限控制最佳实践</span>
</div>
</template>
<div class="practices-content">
<ElRow :gutter="24">
<ElCol :span="12" class="mb-5!">
<div class="flex-c">
<div class="size-10 bg-g-200 flex-cc rounded mr-2">
<ElIcon size="20" color="#409EFF"><Lock /></ElIcon>
</div>
<div>
<h4>多层权限验证</h4>
<p class="text-g-700 text-sm">
在前端路由、后端接口、UI组件等多个层面实施权限控制,确保安全性。
</p>
</div>
</div>
</ElCol>
<ElCol :span="12">
<div class="flex-c">
<div class="size-10 bg-g-200 flex-cc rounded mr-2">
<ElIcon size="20" color="#67C23A"><User /></ElIcon>
</div>
<div>
<h4>基于角色的访问控制</h4>
<p class="text-g-700 text-sm">
采用RBAC模型,通过角色分配权限,简化权限管理复杂度。
</p>
</div>
</div>
</ElCol>
<ElCol :span="12">
<div class="flex-c">
<div class="size-10 bg-g-200 flex-cc rounded mr-2">
<ElIcon size="20" color="#E6A23C"><Key /></ElIcon>
</div>
<div>
<h4>细粒度权限控制</h4>
<p class="text-g-700 text-sm">
支持页面级、按钮级、数据级等多种粒度的权限控制。
</p>
</div>
</div>
</ElCol>
<ElCol :span="12">
<div class="flex-c">
<div class="size-10 bg-g-200 flex-cc rounded mr-2">
<ElIcon size="20" color="#F56C6C"><View /></ElIcon>
</div>
<div>
<h4>安全性优先原则</h4>
<p class="text-g-700 text-sm">
始终遵循最小权限原则,确保用户只能访问必要的功能和数据。
</p>
</div>
</div>
</ElCol>
</ElRow>
</div>
</ElCard>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { Lock, User, Key, View } from "@element-plus/icons-vue";
import { useUserStore } from "@/store/modules/user";
defineOptions({ name: "PermissionPageVisibility" });
const userStore = useUserStore();
// 当前用户信息
const currentUser = computed(() => userStore.info);
// 获取角色显示名称
const getRoleDisplayName = (role: string) => {
const roleMap: Record<string, string> = {
R_SUPER: "超级管理员",
R_ADMIN: "管理员",
R_USER: "普通用户",
};
return roleMap[role] || "未知角色";
};
</script>
@@ -0,0 +1,220 @@
<!-- 切换权限页面 -->
<template>
<div class="py-2">
<!-- 页面头部 -->
<div class="mb-6">
<h2 class="m-0 mb-2 text-xl font-medium">权限切换演示</h2>
<p class="m-0 leading-[1.6] text-g-700">
点击下方按钮切换不同用户身份,模拟不同用户登录系统的效果。切换后会影响整个系统的菜单显示和按钮权限。
</p>
</div>
<!-- 当前用户信息 -->
<div class="mb-6">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="font-semibold">
<span>当前登录用户</span>
</div>
</template>
<div>
<div>
<div class="flex items-start mb-3 last:mb-0">
<span class="min-w-30 font-semibold">用户名:</span>
<span>{{ currentUser.userName || "未登录" }}</span>
</div>
<div class="flex items-start mb-3 last:mb-0">
<span class="min-w-30 font-semibold">角色:</span>
<ElTag :type="getRoleTagType(currentUser.roles?.[0])">
{{ getRoleDisplayName(currentUser.roles?.[0]) }}
</ElTag>
</div>
<div class="flex items-start mb-3 last:mb-0">
<span class="min-w-30 font-semibold">权限码:</span>
<div class="flex flex-wrap gap-2">
<ElTag
v-for="button in currentUser.buttons"
:key="button"
size="small"
type="info"
>
{{ button }}
</ElTag>
<span v-if="!currentUser.buttons?.length" class="italic text-g-500"
>无权限码</span
>
</div>
</div>
</div>
</div>
</ElCard>
</div>
<!-- 用户角色切换 -->
<div class="mb-6">
<ElCard shadow="never" class="art-card-xs">
<template #header>
<div class="font-semibold">
<span>账号切换</span>
</div>
</template>
<div class="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-4">
<div
v-for="account in accounts"
:key="account.key"
class="p-5 border border-g-400 rounded-lg tad-300"
:class="{
'bg-theme/12 !border-theme': currentUser.userName === account.userName,
}"
>
<div class="mb-4">
<div>
<h4 class="m-0 mb-2 text-base font-semibold">{{ account.label }}</h4>
<p class="m-0 mb-2 leading-[1.5] text-g-700">{{ account.description }}</p>
<div class="flex flex-col gap-1">
<span class="text-xs text-g-600">用户名: {{ account.userName }}</span>
<span class="text-xs text-g-600"
>角色: {{ account.roles.join(", ") }}</span
>
</div>
</div>
</div>
<div class="text-right">
<ElButton
v-if="currentUser.userName !== account.userName"
type="primary"
@click="switchRole(account)"
:loading="switching"
>
切换到此角色
</ElButton>
<ElTag v-else type="success">当前用户</ElTag>
</div>
</div>
</div>
</ElCard>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { useUserStore } from "@/store/modules/user";
import { useI18n } from "vue-i18n";
import { fetchLogin, fetchGetUserInfo } from "@/apps/demo/api/auth";
defineOptions({ name: "PermissionSwitchRole" });
const { t } = useI18n();
const userStore = useUserStore();
// 响应式数据
const switching = ref(false);
// 当前用户信息
const currentUser = computed(() => userStore.info);
// 账号列表 - 与登录页面保持一致
const accounts = computed(() => [
{
key: "super",
label: t("login.roles.super"),
userName: "Super",
password: "123456",
roles: ["R_SUPER"],
color: "#E6A23C",
description: "拥有系统最高权限,可以访问所有功能模块",
},
{
key: "admin",
label: t("login.roles.admin"),
userName: "Admin",
password: "123456",
roles: ["R_ADMIN"],
color: "#409EFF",
description: "拥有管理权限,可以管理用户和部分系统设置",
},
{
key: "user",
label: t("login.roles.user"),
userName: "User",
password: "123456",
roles: ["R_USER"],
color: "#67C23A",
description: "普通用户权限,只能访问基础功能模块",
},
]);
// 获取角色标签类型
const getRoleTagType = (
role?: string
): "info" | "warning" | "primary" | "success" | "danger" => {
if (!role) return "info";
const roleMap: Record<string, "info" | "warning" | "primary" | "success" | "danger"> = {
R_SUPER: "warning",
R_ADMIN: "primary",
R_USER: "success",
};
return roleMap[role] || "info";
};
// 获取角色显示名称
const getRoleDisplayName = (role?: string): string => {
if (!role) return "未知角色";
const roleMap: Record<string, string> = {
R_SUPER: "超级管理员",
R_ADMIN: "管理员",
R_USER: "普通用户",
};
return roleMap[role] || "未知角色";
};
/**
* 账号信息类型
*/
interface AccountInfo {
userName: string;
password: string;
role?: string;
roles?: string[];
[key: string]: any;
}
/**
* 切换角色
* @param account 账号信息
*/
const switchRole = async (account: AccountInfo) => {
try {
switching.value = true;
// 模拟登录请求
const { token, refreshToken } = await fetchLogin({
userName: account.userName,
password: account.password,
});
// 验证token
if (!token) {
throw new Error("Login failed - no token received");
}
// 存储token和用户信息
userStore.setToken(token, refreshToken);
const userInfo = await fetchGetUserInfo();
userStore.setUserInfo(userInfo);
// 延迟刷新页面以应用新权限
setTimeout(() => {
window.location.reload();
}, 100);
} catch (error) {
if (error !== "cancel") {
ElMessage.error("切换用户身份失败,请稍后重试");
console.error("[SwitchRole] Error:", error);
}
} finally {
switching.value = false;
}
};
</script>
@@ -0,0 +1,499 @@
<template>
<div class="page-content mb-5">
<div class="mb-15 text-center">
<h1 class="my-4 text-2xl font-semibold leading-tight">WebSocket 连接示例</h1>
<p class="m-0 text-base leading-relaxed text-g-700">
基于 WebSocketClient 的实时通信演示,支持连接管理、消息收发和状态监控
</p>
</div>
<!-- 连接状态和统计信息 -->
<ElRow :gutter="20" class="mb-15">
<ElCol :xs="24" :sm="12" :md="8">
<ElCard class="h-full border-0" :body-style="{ padding: '20px' }" shadow="never">
<div class="text-center">
<div class="text-2xl font-bold text-blue-500 mb-1">{{ messageCount }}</div>
<div class="text-sm font-medium text-gray-900 mb-1">消息统计</div>
<div class="text-xs text-gray-500">接收到的消息数量</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="8">
<ElCard class="h-full border-0" :body-style="{ padding: '20px' }" shadow="never">
<div class="text-center">
<ElTag :type="connectionTagType" size="large" class="mb-2">
{{ wsClient?.connectionStatusText || '未连接' }}
</ElTag>
<div class="text-sm font-medium text-gray-900">连接状态</div>
<div class="text-xs text-gray-500">当前WebSocket连接状态</div>
</div>
</ElCard>
</ElCol>
<ElCol :xs="24" :sm="12" :md="8">
<ElCard class="h-full border-0" :body-style="{ padding: '20px' }" shadow="never">
<div class="text-center">
<div class="text-2xl font-bold text-amber-500 mb-1">{{ reconnectCount }}</div>
<div class="text-sm font-medium text-gray-900 mb-1">重连次数</div>
<div class="text-xs text-gray-500">自动重连尝试次数</div>
</div>
</ElCard>
</ElCol>
</ElRow>
<!-- 连接配置和发送消息 -->
<ElRow :gutter="20" class="mb-15">
<ElCol :xs="24" :md="12">
<ElCard class="h-full border-0" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span class="text-base font-bold">连接配置</span>
<ElTag :type="connectionTagType" size="large">
{{ wsClient?.connectionStatusText || '未连接' }}
</ElTag>
</div>
</template>
<ElForm :model="connectForm" label-width="100px" class="max-w-md">
<ElFormItem label="服务器地址">
<ElInput v-model="connectForm.url" placeholder="ws://localhost:8080/ws" clearable />
</ElFormItem>
<ElFormItem label="连接选项">
<ElSpace>
<ElCheckbox v-model="connectForm.autoReconnect">自动重连</ElCheckbox>
<ElCheckbox v-model="connectForm.heartbeat">心跳检测</ElCheckbox>
</ElSpace>
</ElFormItem>
<ElFormItem>
<ElSpace>
<ElButton
type="primary"
@click="handleConnect"
:loading="isConnecting"
:disabled="isConnected"
>
{{ isConnecting ? '连接中...' : '连接' }}
</ElButton>
<ElButton type="danger" @click="handleDisconnect" :disabled="!isConnected">
断开连接
</ElButton>
<ElButton @click="handleReconnect" :disabled="isConnecting">重连</ElButton>
</ElSpace>
</ElFormItem>
</ElForm>
</ElCard>
</ElCol>
<ElCol :xs="24" :md="12">
<ElCard class="h-full border-0" shadow="never">
<template #header>
<span class="text-base font-bold">发送消息</span>
</template>
<ElForm :model="messageForm" @submit.prevent="handleSendMessage">
<ElFormItem label="消息类型">
<ElSelect v-model="messageForm.type" class="w-full">
<ElOption label="文本消息" value="text" />
<ElOption label="JSON数据" value="json" />
<ElOption label="心跳包" value="ping" />
</ElSelect>
</ElFormItem>
<ElFormItem label="消息内容">
<ElInput
v-model="messageForm.content"
type="textarea"
:rows="4"
placeholder="请输入要发送的消息内容"
/>
</ElFormItem>
<ElFormItem>
<ElSpace>
<ElButton
type="primary"
@click="handleSendMessage"
:disabled="!isConnected || !messageForm.content"
>
发送消息
</ElButton>
<ElButton @click="clearMessageForm">清空</ElButton>
</ElSpace>
</ElFormItem>
</ElForm>
</ElCard>
</ElCol>
</ElRow>
<!-- 接收消息 - 单独占一行 -->
<ElRow class="mb-15">
<ElCol :span="24">
<ElCard class="border-0" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span class="text-base font-bold">接收消息</span>
<ElButton size="small" @click="clearMessages">清空记录</ElButton>
</div>
</template>
<div class="message-container">
<div v-for="(message, index) in messageList" :key="index" class="message-item">
<div class="message-header">
<ElTag size="small" :type="message.type === 'received' ? 'success' : 'info'">
{{ message.type === 'received' ? '接收' : '发送' }}
</ElTag>
<span class="message-time">{{ message.time }}</span>
</div>
<div class="message-content">{{ message.content }}</div>
</div>
<ElEmpty v-if="messageList.length === 0" description="暂无消息记录" :image-size="100" />
</div>
</ElCard>
</ElCol>
</ElRow>
<!-- 连接日志 -->
<ElCard class="border-0" shadow="never">
<template #header>
<div class="flex items-center justify-between">
<span class="text-base font-bold">连接日志</span>
<ElButton size="small" @click="clearLogs">清空日志</ElButton>
</div>
</template>
<div class="log-container">
<ElAlert
v-for="(log, index) in logList"
:key="index"
:type="log.type"
:closable="false"
class="!mb-2"
>
<template #title>
<div class="flex items-start gap-2">
<span class="text-xs opacity-70 whitespace-nowrap">{{ log.time }}</span>
<span class="flex-1">{{ log.message }}</span>
</div>
</template>
</ElAlert>
<ElEmpty v-if="logList.length === 0" description="暂无日志记录" :image-size="100" />
</div>
</ElCard>
</div>
</template>
<script setup lang="ts">
import WebSocketClient from '@/utils/socket'
import { ElMessage } from 'element-plus'
defineOptions({ name: 'WidgetsSocketChat' })
// WebSocket客户端实例
const wsClient = ref<WebSocketClient | null>(null)
// 连接状态
const isConnecting = ref(false)
const isConnected = ref(false)
const reconnectCount = ref(0)
const messageCount = ref(0)
// 用于清理 watch 的函数
let stopWatchConnection: (() => void) | null = null
let stopWatchStatus: (() => void) | null = null
// 表单数据
const connectForm = ref({
url: 'ws://localhost:8080/ws',
autoReconnect: true,
heartbeat: true
})
const messageForm = ref({
type: 'text',
content: ''
})
// 消息和日志列表
const messageList = ref<
Array<{
type: 'sent' | 'received'
content: string
time: string
}>
>([])
const logList = ref<
Array<{
type: 'info' | 'success' | 'warning' | 'error'
message: string
time: string
}>
>([])
// 计算属性
const connectionTagType = computed(() => {
if (isConnecting.value) return 'warning'
if (isConnected.value) return 'success'
return 'danger'
})
/**
* 添加日志
*/
const addLog = (type: 'info' | 'success' | 'warning' | 'error', message: string) => {
logList.value.unshift({
type,
message,
time: new Date().toLocaleTimeString()
})
// 限制日志数量
if (logList.value.length > 100) {
logList.value = logList.value.slice(0, 100)
}
}
/**
* 添加消息记录
*/
const addMessage = (type: 'sent' | 'received', content: string) => {
messageList.value.unshift({
type,
content,
time: new Date().toLocaleTimeString()
})
// 限制消息数量
if (messageList.value.length > 50) {
messageList.value = messageList.value.slice(0, 50)
}
}
/**
* 处理WebSocket消息
*/
const handleSocketMessage = (event: MessageEvent) => {
messageCount.value++
addMessage('received', event.data)
addLog('success', `收到消息: ${event.data}`)
}
/**
* 连接WebSocket
*/
const handleConnect = () => {
if (isConnecting.value || isConnected.value) {
return
}
// 清理之前的 watch
if (stopWatchConnection) {
stopWatchConnection()
stopWatchConnection = null
}
if (stopWatchStatus) {
stopWatchStatus()
stopWatchStatus = null
}
isConnecting.value = true
addLog('info', `开始连接到 ${connectForm.value.url}`)
try {
wsClient.value = WebSocketClient.getInstance({
url: connectForm.value.url,
messageHandler: handleSocketMessage,
reconnectInterval: connectForm.value.autoReconnect ? 5000 : 0,
heartbeatInterval: connectForm.value.heartbeat ? 10000 : 0,
maxReconnectAttempts: 5
})
wsClient.value.init()
// 监听连接状态变化
stopWatchConnection = watch(
() => wsClient.value?.isWebSocketConnected,
(connected) => {
isConnected.value = connected || false
isConnecting.value = false
if (connected) {
addLog('success', 'WebSocket连接成功')
reconnectCount.value = 0
}
},
{ immediate: true }
)
// 监听连接状态文本变化
stopWatchStatus = watch(
() => wsClient.value?.connectionStatusText,
(status) => {
if (status && status.includes('重连中')) {
reconnectCount.value++
addLog('warning', `自动重连中 (第${reconnectCount.value}次)`)
}
}
)
} catch (error) {
isConnecting.value = false
const errorMessage = error instanceof Error ? error.message : String(error)
addLog('error', `连接失败: ${errorMessage}`)
ElMessage.error('连接失败,请检查服务器地址')
}
}
/**
* 断开连接
*/
const handleDisconnect = () => {
if (wsClient.value) {
wsClient.value.close()
addLog('info', '手动断开WebSocket连接')
}
// 清理 watch
if (stopWatchConnection) {
stopWatchConnection()
stopWatchConnection = null
}
if (stopWatchStatus) {
stopWatchStatus()
stopWatchStatus = null
}
isConnected.value = false
isConnecting.value = false
}
/**
* 重新连接
*/
const handleReconnect = () => {
handleDisconnect()
setTimeout(() => {
handleConnect()
}, 1000)
}
/**
* 发送消息
*/
const handleSendMessage = () => {
if (!isConnected.value || !wsClient.value) {
ElMessage.warning('请先建立WebSocket连接')
return
}
let message = messageForm.value.content
// 根据消息类型处理内容
switch (messageForm.value.type) {
case 'json':
try {
// 验证是否为有效JSON
JSON.parse(message)
} catch {
ElMessage.error('请输入有效的JSON格式数据')
return
}
break
case 'ping':
message = 'ping'
break
}
try {
wsClient.value.send(message)
addMessage('sent', message)
addLog('info', `发送消息: ${message}`)
ElMessage.success('消息发送成功')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
addLog('error', `发送失败: ${errorMessage}`)
ElMessage.error('发送消息失败')
}
}
/**
* 清空消息表单
*/
const clearMessageForm = () => {
messageForm.value.content = ''
}
/**
* 清空消息记录
*/
const clearMessages = () => {
messageList.value = []
}
/**
* 清空日志
*/
const clearLogs = () => {
logList.value = []
}
/**
* 页面卸载时清理连接
*/
onUnmounted(() => {
handleDisconnect()
})
/**
* 监听页面可见性变化,页面隐藏时断开连接
*/
onMounted(() => {
document.addEventListener('visibilitychange', () => {
if (document.hidden && isConnected.value) {
addLog('info', '页面隐藏,保持连接')
}
})
})
</script>
<style scoped>
@reference '@styles/core/tailwind.css';
.message-container {
@apply max-h-96 overflow-y-auto space-y-3;
}
.message-item {
@apply p-3 rounded-lg border border-gray-200 bg-white dark:bg-gray-800 dark:border-gray-700;
}
.message-header {
@apply flex items-center justify-between mb-2;
}
.message-time {
@apply text-xs text-gray-500;
}
.message-content {
@apply text-sm text-gray-800 dark:text-gray-200 break-words font-mono bg-gray-50 dark:bg-gray-900 p-2 rounded;
}
.log-container {
@apply max-h-64 overflow-y-auto;
}
/* 滚动条样式 */
.message-container::-webkit-scrollbar,
.log-container::-webkit-scrollbar {
@apply w-4;
}
.message-container::-webkit-scrollbar-track,
.log-container::-webkit-scrollbar-track {
@apply bg-gray-100 rounded;
}
.message-container::-webkit-scrollbar-thumb,
.log-container::-webkit-scrollbar-thumb {
@apply bg-gray-300 rounded hover:bg-gray-400;
}
</style>
@@ -0,0 +1,63 @@
<!-- 基础表格 -->
<template>
<div class="user-page art-full-height">
<ElCard class="art-table-card" shadow="never" style="margin-top: 0">
<!-- 表格 -->
<ArtTable
rowKey="id"
:show-table-header="false"
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
</ArtTable>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { useTable } from '@/hooks/core/useTable'
import { fetchGetUserList } from '@/apps/demo/api/system-manage'
defineOptions({ name: 'UserMixedUsageExample' })
const { data, columns, loading, pagination, handleSizeChange, handleCurrentChange } = useTable({
core: {
apiFn: fetchGetUserList,
apiParams: {
current: 1,
size: 20,
userName: '',
userPhone: '',
userEmail: ''
},
columnsFactory: () => [
{
prop: 'id',
label: 'ID'
},
{
prop: 'nickName',
label: '昵称'
},
{
prop: 'userGender',
label: '性别',
sortable: true,
formatter: (row) => row.userGender || '未知'
},
{
prop: 'userPhone',
label: '手机号'
},
{
prop: 'userEmail',
label: '邮箱'
}
]
}
})
</script>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,245 @@
<!-- 左树右表示例页面 -->
<template>
<div class="art-full-height">
<div class="box-border flex gap-4 h-full max-md:block max-md:gap-0 max-md:h-auto">
<div class="flex-shrink-0 w-58 h-full max-md:w-full max-md:h-auto max-md:mb-5">
<ElCard class="tree-card art-card-xs flex flex-col h-full mt-0" shadow="never">
<template #header>
<b>分类树</b>
</template>
<ElScrollbar>
<ElTree
:data="treeData"
:props="treeProps"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
/>
</ElScrollbar>
</ElCard>
</div>
<div class="flex flex-col flex-grow min-w-0">
<UserSearch v-model="defaultFilter" />
<ElCard class="flex flex-col flex-1 min-h-0 art-table-card" shadow="never">
<ArtTableHeader
v-model:columns="columnChecks"
:loading="loading"
@refresh="refreshData"
>
<template #left>
<ElSpace wrap>
<ElButton
@click="showButtons = !showButtons"
v-ripple
type="primary"
plain
>{{ showButtons ? "收起" : "展开" }}按钮组</ElButton
>
<ElButton v-show="showButtons" v-ripple v-for="value in 12" :key="value"
>表格自适应</ElButton
>
</ElSpace>
</template>
</ArtTableHeader>
<ArtTable
rowKey="id"
:loading="loading"
:data="data"
:columns="columns"
:pagination="pagination"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
</ArtTable>
</ElCard>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useTable } from "@/hooks/core/useTable";
import { UserService } from "@/api/system/user";
import UserSearch from "@apps/demo/views/system/user/modules/user-search.vue";
defineOptions({ name: "TreeTable" });
const showButtons = ref(false);
// 树形数据 - 组织架构示例
const treeData = ref([
{
id: 1,
label: "技术部",
children: [
{
id: 11,
label: "前端开发组",
children: [
{ id: 111, label: "React 团队" },
{ id: 112, label: "Vue 团队" },
{ id: 113, label: "移动端团队" },
],
},
{
id: 12,
label: "后端开发组",
children: [
{ id: 121, label: "Java 团队" },
{ id: 122, label: "Node.js 团队" },
{ id: 123, label: "Python 团队" },
],
},
{
id: 13,
label: "测试组",
children: [
{ id: 131, label: "功能测试" },
{ id: 132, label: "自动化测试" },
{ id: 133, label: "性能测试" },
],
},
{
id: 14,
label: "运维组",
children: [
{ id: 141, label: "系统运维" },
{ id: 142, label: "DevOps" },
],
},
],
},
{
id: 2,
label: "产品部",
children: [
{
id: 21,
label: "产品设计组",
children: [
{ id: 211, label: "UI 设计" },
{ id: 212, label: "UX 设计" },
{ id: 213, label: "交互设计" },
],
},
{
id: 22,
label: "产品运营组",
children: [
{ id: 221, label: "用户运营" },
{ id: 222, label: "内容运营" },
{ id: 223, label: "活动运营" },
],
},
{ id: 23, label: "数据分析组" },
],
},
{
id: 3,
label: "市场部",
children: [
{ id: 31, label: "品牌推广组" },
{ id: 32, label: "渠道拓展组" },
{
id: 33,
label: "销售组",
children: [
{ id: 331, label: "企业客户" },
{ id: 332, label: "个人客户" },
],
},
],
},
{
id: 4,
label: "行政部",
children: [
{ id: 41, label: "人力资源组" },
{ id: 42, label: "财务组" },
{ id: 43, label: "行政后勤组" },
],
},
{
id: 5,
label: "客服部",
children: [
{ id: 51, label: "售前咨询" },
{ id: 52, label: "售后支持" },
{ id: 53, label: "客户成功" },
],
},
]);
const treeProps = {
children: "children",
label: "label",
};
const handleNodeClick = (data: any) => {
console.log("选中节点:", data);
// 可以根据选中的节点更新右侧表格数据
};
// 表单搜索初始值
const defaultFilter = ref({
name: undefined,
});
const {
data,
columns,
columnChecks,
loading,
pagination,
refreshData,
handleSizeChange,
handleCurrentChange,
} = useTable({
core: {
apiFn: fetchGetUserList,
apiParams: {
current: 1,
size: 20,
userName: "",
userPhone: "",
userEmail: "",
},
columnsFactory: () => [
{
prop: "id",
label: "ID",
},
{
prop: "nickName",
label: "昵称",
},
{
prop: "userGender",
label: "性别",
sortable: true,
formatter: (row) => row.userGender || "未知",
},
{
prop: "userPhone",
label: "手机号",
},
{
prop: "userEmail",
label: "邮箱",
},
],
},
});
</script>
<style scoped>
.tree-card :deep(.el-card__body) {
flex: 1;
min-height: 0;
padding: 10px 2px 10px 10px;
}
</style>
@@ -0,0 +1,129 @@
<!-- 标签页示例页面 -->
<template>
<div class="page-content">
<h3 class="mb-5 text-xl font-normal">标签页操作</h3>
<!-- 修改标签标题模块 -->
<ElCard class="mb-7.5" header="修改标签标题" shadow="never">
<div class="flex gap-2">
<ElInput
v-model="newTabTitle"
placeholder="请输入新的标签页标题"
clearable
class="!max-w-75"
/>
<ElButton
type="primary"
@click="handleUpdateTabTitle"
:disabled="!newTabTitle.trim()"
>
修改
</ElButton>
<ElButton @click="handleResetTabTitle"> 重置 </ElButton>
</div>
</ElCard>
<!-- 获取标签页模块 -->
<ElCard class="mb-7.5" header="获取标签页信息" shadow="never">
<div class="mb-4">
<p class="m-0 mb-2 text-sm text-g-600">当前标签页信息:{{ currentTab }}</p>
</div>
<ElRow :gutter="20">
<ElCol :span="24">
<ElButton type="success" plain @click="handleGetCurrentTabTitle(routePath)">
获取当前标签页信息
</ElButton>
</ElCol>
</ElRow>
</ElCard>
<!-- 关闭标签页模块 -->
<ElCard class="mb-7.5" header="关闭标签页" shadow="never">
<ElRow :gutter="20">
<ElCol :span="24">
<ElButton type="danger" plain @click="handleCloseTab(routePath)">
关闭当前标签
</ElButton>
<ElButton type="warning" plain @click="handleCloseOthersTab(routePath)">
关闭其他标签
</ElButton>
<ElButton type="danger" plain @click="handleCloseAllTab">
关闭所有标签
</ElButton>
</ElCol>
</ElRow>
</ElCard>
</div>
</template>
<script setup lang="ts">
import { useWorktabStore } from "@/store/modules/worktab";
import { WorkTab } from "@/types";
defineOptions({ name: "TabsExample" });
const worktabStore = useWorktabStore();
const currentTab = ref<WorkTab | null>(null);
const newTabTitle = ref("");
const routePath = "/demo-examples/tabs";
/**
* 更新当前标签页标题
* 验证输入内容后调用 store 方法更新标题
*/
const handleUpdateTabTitle = (): void => {
const trimmedTitle = newTabTitle.value.trim();
if (trimmedTitle) {
worktabStore.updateTabTitle(routePath, trimmedTitle);
ElMessage.success("标签页标题已更新");
}
};
/**
* 重置标签页标题
* 将标题重置为默认值并清空输入框
*/
const handleResetTabTitle = (): void => {
worktabStore.resetTabTitle(routePath);
newTabTitle.value = "";
ElMessage.success("标签页标题已重置");
};
/**
* 获取指定路径的标签页信息
* @param path 标签页路径
*/
const handleGetCurrentTabTitle = (path: string): void => {
const tab = worktabStore.getTabTitle(path);
if (tab) {
currentTab.value = tab;
ElMessage.success("已获取标签页信息");
} else {
ElMessage.warning("未找到标签信息");
}
};
/**
* 关闭指定路径的标签页
* @param path 要关闭的标签页路径
*/
const handleCloseTab = (path: string): void => {
worktabStore.removeTab(path);
};
/**
* 关闭除指定路径外的其他所有标签页
* @param path 要保留的标签页路径
*/
const handleCloseOthersTab = (path: string): void => {
worktabStore.removeOthers(path);
};
/**
* 关闭所有标签页
*/
const handleCloseAllTab = (): void => {
worktabStore.removeAll();
};
</script>