初始化本地仓库
This commit is contained in:
@@ -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>
|
||||
+529
@@ -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>
|
||||
+266
@@ -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>
|
||||
+220
@@ -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>
|
||||
Reference in New Issue
Block a user