初始化
This commit is contained in:
429
saiadmin-artd/src/views/system/config/index.vue
Normal file
429
saiadmin-artd/src/views/system/config/index.vue
Normal file
@@ -0,0 +1,429 @@
|
||||
<!-- 左右页面 -->
|
||||
<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 h-full max-md:w-full max-md:h-auto max-md:mb-5">
|
||||
<ElCard class="left-card art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<b>系统设置</b>
|
||||
</template>
|
||||
<ElSpace wrap>
|
||||
<SaButton type="primary" icon="ri:refresh-line" @click="reloadConfigData" />
|
||||
<SaButton v-permission="'core:config:edit'" type="primary" @click="showDialog('add')" />
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="secondary"
|
||||
@click="updateConfigDialog"
|
||||
/>
|
||||
<SaButton v-permission="'core:config:edit'" type="error" @click="deleteConfigData" />
|
||||
</ElSpace>
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="groupData"
|
||||
:columns="groupColumns"
|
||||
:pagination="groupPagination"
|
||||
highlight-current-row
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 基础列 -->
|
||||
<template #name-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="configSearch.name"
|
||||
placeholder="搜索配置名称"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleConfigSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #code-header="{ column }">
|
||||
<ElPopover placement="bottom" :width="200" trigger="hover">
|
||||
<template #reference>
|
||||
<div class="flex items-center gap-2 text-theme c-p custom-header">
|
||||
<span>{{ column.label }}</span>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</div>
|
||||
</template>
|
||||
<ElInput
|
||||
v-model="configSearch.code"
|
||||
placeholder="搜索配置标识"
|
||||
size="small"
|
||||
clearable
|
||||
@input="handleConfigSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<ElIcon>
|
||||
<Search />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</ElInput>
|
||||
</ElPopover>
|
||||
</template>
|
||||
<template #id="{ row }">
|
||||
<ElRadio
|
||||
v-model="selectedId"
|
||||
:value="row.id"
|
||||
@update:modelValue="handleGroupChange(row.id, row)"
|
||||
/>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<ElCard class="art-card-xs flex flex-col h-full mt-0" shadow="never">
|
||||
<template #header>
|
||||
<div class="flex justify-between">
|
||||
<b>{{ selectedRow.name || '未选择配置' }}</b>
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="primary"
|
||||
icon="ri:settings-4-line"
|
||||
@click="handleConfigManage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="max-h-[calc(100vh-250px)] overflow-y-auto">
|
||||
<ElForm ref="formRef" :model="formData" label-width="140px">
|
||||
<template v-for="(item, index) in formArray" :key="index">
|
||||
<ElFormItem :label="item.name" :prop="item.key" v-show="item.display">
|
||||
<template v-if="item.input_type === 'select'">
|
||||
<el-select
|
||||
v-model="item.value"
|
||||
:options="item.config_select_data"
|
||||
@change="handleSelect($event, item)"
|
||||
:placeholder="'请选择' + item.name"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="item.input_type === 'input'">
|
||||
<el-input v-model="item.value" :placeholder="'请输入' + item.name" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'radio'">
|
||||
<el-radio-group v-model="item.value" :options="item.config_select_data" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'textarea'">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="item.value"
|
||||
:placeholder="'请输入' + item.name"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="item.input_type === 'uploadImage'">
|
||||
<sa-image-picker v-model="item.value" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'uploadFile'">
|
||||
<sa-file-upload v-model="item.value" />
|
||||
</template>
|
||||
<template v-if="item.input_type === 'wangEditor'">
|
||||
<sa-editor v-model="item.value" />
|
||||
</template>
|
||||
<div class="text-gray-400 text-xs py-2">{{ item.remark }}</div>
|
||||
</ElFormItem>
|
||||
</template>
|
||||
<ElFormItem v-permission="'core:config:update'" v-if="formArray.length > 0">
|
||||
<ElButton type="primary" @click="submit(formArray)">保存修改</ElButton>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
v-permission="'core:config:update'"
|
||||
label="测试邮件"
|
||||
v-if="selectedRow.code === 'email_config'"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElInput
|
||||
v-model="email"
|
||||
style="width: 300px"
|
||||
placeholder="请输入正确的邮箱接收地址"
|
||||
/>
|
||||
<ElButton @click="sendMail()">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:mail-line" />
|
||||
</template>
|
||||
发送
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<el-empty v-if="selectedId === 0" description="请先选择左侧配置" />
|
||||
</ElForm>
|
||||
</div>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置编辑弹窗 -->
|
||||
<GroupEditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="reloadConfigData()"
|
||||
/>
|
||||
|
||||
<!-- 配置项管理 -->
|
||||
<ConfigList v-model="configVisible" :data="selectedRow" @success="getConfigData()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import api from '@/api/system/config'
|
||||
import GroupEditDialog from './modules/group-edit-dialog.vue'
|
||||
import ConfigList from './modules/config-list.vue'
|
||||
|
||||
defineOptions({ name: 'TreeTable' })
|
||||
|
||||
// 刷新配置数据
|
||||
const reloadConfigData = () => {
|
||||
selectedId.value = 0
|
||||
selectedRow.value = {}
|
||||
formArray.value = []
|
||||
getGroupData()
|
||||
}
|
||||
|
||||
// 修改配置
|
||||
const updateConfigDialog = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要修改的数据')
|
||||
return
|
||||
}
|
||||
showDialog('edit', selectedRow.value)
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
const deleteConfigData = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要修改的数据')
|
||||
return
|
||||
}
|
||||
deleteRow({ ...selectedRow.value }, api.delete, reloadConfigData)
|
||||
}
|
||||
|
||||
// 配置数据
|
||||
const formData = ref({})
|
||||
const formArray = ref<any[]>([])
|
||||
const email = ref('')
|
||||
|
||||
const configVisible = ref(false)
|
||||
|
||||
// 配置选中行
|
||||
const selectedId = ref(0)
|
||||
const selectedRow = ref<any>({})
|
||||
const configSearch = ref({
|
||||
name: '',
|
||||
code: ''
|
||||
})
|
||||
|
||||
// 配置搜索
|
||||
const handleConfigSearch = () => {
|
||||
Object.assign(searchConfigParams, configSearch.value)
|
||||
getGroupData()
|
||||
}
|
||||
|
||||
const searchForm = ref({
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
group_id: null
|
||||
})
|
||||
|
||||
/**
|
||||
* 配置分组改变时,获取配置数据
|
||||
*/
|
||||
const handleGroupChange = (val: any, row?: any) => {
|
||||
selectedId.value = val
|
||||
selectedRow.value = row
|
||||
searchForm.value.group_id = val
|
||||
getConfigData()
|
||||
}
|
||||
|
||||
const getConfigData = () => {
|
||||
api.configList({ group_id: selectedId.value, saiType: 'all' }).then((data) => {
|
||||
formArray.value = data.map((item: any) => {
|
||||
if (
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
} else {
|
||||
item.display = true
|
||||
}
|
||||
return item
|
||||
})
|
||||
if (selectedId.value === 2) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key === 'upload_mode') {
|
||||
handleSelect(item.value, item)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 配置名称
|
||||
const {
|
||||
data: groupData,
|
||||
columns: groupColumns,
|
||||
getData: getGroupData,
|
||||
searchParams: searchConfigParams,
|
||||
loading,
|
||||
pagination: groupPagination,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.groupList,
|
||||
apiParams: {
|
||||
...configSearch.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: '选中', width: 80, align: 'center', useSlot: true },
|
||||
{ prop: 'name', label: '配置名称', useHeaderSlot: true, width: 150 },
|
||||
{ prop: 'code', label: '配置标识', useHeaderSlot: true, width: 150 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
|
||||
|
||||
const handleConfigManage = () => {
|
||||
if (selectedId.value === 0) {
|
||||
ElMessage.error('请选择要管理的配置')
|
||||
return
|
||||
}
|
||||
configVisible.value = true
|
||||
}
|
||||
|
||||
// 发送测试邮件
|
||||
const sendMail = async () => {
|
||||
const reg = /^[a-z0-9]+([._\\-]*[a-z0-9])*@([a-z0-9]+[-a-z0-9]*[a-z0-9]+.){1,63}[a-z0-9]+$/
|
||||
if (!reg.test(email.value)) {
|
||||
ElMessage.warning('请输入正确的邮箱地址')
|
||||
return
|
||||
}
|
||||
await api.emailTest({ email: email.value })
|
||||
ElMessage.success('发送成功')
|
||||
}
|
||||
|
||||
// 自定义处理切换显示
|
||||
const handleSelect = async (val: any, ele: any) => {
|
||||
if (ele.key === 'upload_mode') {
|
||||
if (val == 1) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('local_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 2) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('oss_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 3) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('qiniu_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 4) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('cos_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1 ||
|
||||
item.key.indexOf('s3_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
if (val == 5) {
|
||||
formArray.value.map((item) => {
|
||||
if (item.key.indexOf('s3_') !== -1) {
|
||||
item.display = true
|
||||
}
|
||||
if (
|
||||
item.key.indexOf('qiniu_') !== -1 ||
|
||||
item.key.indexOf('cos_') !== -1 ||
|
||||
item.key.indexOf('local_') !== -1 ||
|
||||
item.key.indexOf('oss_') !== -1
|
||||
) {
|
||||
item.display = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async (params: any) => {
|
||||
const data = {
|
||||
group_id: selectedId.value,
|
||||
config: params
|
||||
}
|
||||
await api.batchUpdate(data)
|
||||
ElMessage.success('保存成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.left-card :deep(.el-card__body) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 10px 2px 10px 10px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增配置' : '编辑配置'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="配置标识" prop="key">
|
||||
<el-input v-model="formData.key" placeholder="请输入配置标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置标题" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入配置标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组件类型" prop="input_type">
|
||||
<el-select v-model="formData.input_type" :options="inputComponent" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="组件数据"
|
||||
prop="config_select_data"
|
||||
v-if="['select', 'radio'].includes(formData.input_type)"
|
||||
>
|
||||
<el-row
|
||||
:gutter="10"
|
||||
class="mb-2"
|
||||
v-for="(item, index) in formData.config_select_data"
|
||||
:key="index"
|
||||
>
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.label" placeholder="请输入label"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="10">
|
||||
<el-input v-model="item.value" placeholder="请输入value"></el-input>
|
||||
</el-col>
|
||||
<el-col :span="4">
|
||||
<el-button type="danger" @click="removeConfigSelectData(index)">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-button type="primary" @click="addConfigSelectData">
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入配置描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/config'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
interface ConfigSelectData {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const inputComponent = [
|
||||
{ label: '文本框', value: 'input' },
|
||||
{ label: '文本域', value: 'textarea' },
|
||||
{ label: '下拉选择框', value: 'select' },
|
||||
{ label: '单选框', value: 'radio' },
|
||||
{ label: '图片上传', value: 'uploadImage' },
|
||||
{ label: '文件上传', value: 'uploadFile' },
|
||||
{ label: '富文本编辑器', value: 'wangEditor' }
|
||||
]
|
||||
|
||||
const addConfigSelectData = () => {
|
||||
formData.config_select_data.push({ label: '', value: '' })
|
||||
}
|
||||
|
||||
const removeConfigSelectData = (index: number) => {
|
||||
formData.config_select_data.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
key: [{ required: true, message: '请输入配置标识', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '请输入配置标题', trigger: 'blur' }],
|
||||
input_type: [{ required: true, message: '请输入组件类型', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
group_id: null,
|
||||
key: '',
|
||||
value: '',
|
||||
name: '',
|
||||
input_type: 'input',
|
||||
config_select_data: [] as ConfigSelectData[],
|
||||
sort: 100,
|
||||
remark: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
formData.config_select_data = []
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.configSave(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.configUpdate(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
192
saiadmin-artd/src/views/system/config/modules/config-list.vue
Normal file
192
saiadmin-artd/src/views/system/config/modules/config-list.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
v-model="visible"
|
||||
title="配置管理"
|
||||
size="70%"
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="art-full-height">
|
||||
<!-- 表格头部 -->
|
||||
<div>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:config:edit'" @click="handleAddClick" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:config:edit'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.configDelete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</div>
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="tableData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:config:edit'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.configDelete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
|
||||
<ConfigEditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/config'
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import ConfigEditDialog from './config-edit-dialog.vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
refreshData()
|
||||
}
|
||||
}
|
||||
|
||||
const refreshData = () => {
|
||||
searchForm.value.group_id = props.data?.id
|
||||
Object.assign(searchParams, searchForm.value)
|
||||
getData()
|
||||
}
|
||||
|
||||
const handleAddClick = () => {
|
||||
showDialog('add', { group_id: searchForm.value.group_id })
|
||||
}
|
||||
|
||||
const searchForm = ref({
|
||||
label: '',
|
||||
value: '',
|
||||
status: '',
|
||||
group_id: null
|
||||
})
|
||||
|
||||
const {
|
||||
loading,
|
||||
data: tableData,
|
||||
columns,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.configList,
|
||||
immediate: false,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'key', label: '配置标识' },
|
||||
{ prop: 'name', label: '配置标题' },
|
||||
{ prop: 'input_type', label: '组件类型', width: 100 },
|
||||
{ prop: 'sort', label: '排序', width: 100, sortable: true },
|
||||
{ prop: 'remark', label: '备注' },
|
||||
{ prop: 'operation', label: '操作', useSlot: true, width: 100 }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange,
|
||||
selectedRows
|
||||
} = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
emit('success')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="80px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典标签" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入字典标签" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="字典键值" prop="value">
|
||||
<el-input v-model="formData.value" placeholder="请输入字典键值" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增配置分组' : '编辑配置分组'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="配置名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入配置名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="配置标识" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入配置标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/config'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入配置名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入配置标识', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
143
saiadmin-artd/src/views/system/dept/index.vue
Normal file
143
saiadmin-artd/src/views/system/dept/index.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:dept:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon v-if="isExpanded" icon="ri:collapse-diagonal-line" />
|
||||
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
|
||||
</template>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:default-expand-all="true"
|
||||
@sort-change="handleSortChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:dept:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:dept:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/dept'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
|
||||
// 状态管理
|
||||
const isExpanded = ref(true)
|
||||
const tableRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ prop: 'name', label: '部门名称', minWidth: 200 },
|
||||
{ prop: 'code', label: '部门编码', minWidth: 120 },
|
||||
{ prop: 'leader.username', label: '部门领导', minWidth: 120 },
|
||||
{ prop: 'remark', label: '描述', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'sort', label: '排序', width: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'create_time', label: '创建日期', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 切换展开/收起所有菜单
|
||||
*/
|
||||
const toggleExpand = (): void => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value?.elTableRef && data.value) {
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
if (row.children?.length) {
|
||||
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
processRows(data.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
194
saiadmin-artd/src/views/system/dept/modules/edit-dialog.vue
Normal file
194
saiadmin-artd/src/views/system/dept/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增部门' : '编辑部门'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="上级部门" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="formData.parent_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入部门名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入部门编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门领导">
|
||||
<sa-user v-model="formData.leader_id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入部门描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/dept'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
parent_id: [{ required: true, message: '请选择上级部门', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入部门名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入部门编码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
parent_id: null,
|
||||
level: '',
|
||||
name: '',
|
||||
code: '',
|
||||
leader_id: null,
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await api.list({ tree: true })
|
||||
optionData.treeData = [
|
||||
{
|
||||
id: 0,
|
||||
value: 0,
|
||||
label: '无上级部门',
|
||||
children: data
|
||||
}
|
||||
]
|
||||
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
77
saiadmin-artd/src/views/system/dept/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/dept/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="部门名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入部门名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="部门编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入部门编码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
223
saiadmin-artd/src/views/system/menu/index.vue
Normal file
223
saiadmin-artd/src/views/system/menu/index.vue
Normal file
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:menu:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:menu:destroy'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
<ElButton @click="toggleExpand" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon v-if="isExpanded" icon="ri:collapse-diagonal-line" />
|
||||
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
|
||||
</template>
|
||||
{{ isExpanded ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:default-expand-all="false"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex justify-end gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:menu:save'"
|
||||
v-if="row.type < 3"
|
||||
type="primary"
|
||||
@click="handleAdd(row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:menu:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:menu:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/menu'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import { h } from 'vue'
|
||||
import ArtSvgIcon from '@/components/core/base/art-svg-icon/index.vue'
|
||||
|
||||
// 状态管理
|
||||
const isExpanded = ref(false)
|
||||
const tableRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
path: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'name', label: '菜单名称', minWidth: 150 },
|
||||
{
|
||||
prop: 'type',
|
||||
label: '菜单类型',
|
||||
align: 'center',
|
||||
saiType: 'dict',
|
||||
saiDict: 'menu_type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
prop: 'icon',
|
||||
label: '图标',
|
||||
align: 'center',
|
||||
width: 80,
|
||||
formatter: (row: any) => {
|
||||
return h(ArtSvgIcon, { icon: row.icon })
|
||||
}
|
||||
},
|
||||
{ prop: 'code', label: '组件名称' },
|
||||
{
|
||||
prop: 'path',
|
||||
label: '路由',
|
||||
formatter: (row: any) => {
|
||||
if (row.type === 3) return ''
|
||||
return row.path || ''
|
||||
}
|
||||
},
|
||||
{
|
||||
prop: 'slug',
|
||||
label: '权限标识',
|
||||
minWidth: 160,
|
||||
formatter: (row: any) => {
|
||||
if (row.type === 2) {
|
||||
return row.children?.length ? row.children.length + '个权限标识' : ''
|
||||
}
|
||||
if (row.type === 3) {
|
||||
return row.slug || ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
},
|
||||
{ prop: 'sort', label: '排序', width: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'operation', label: '操作', width: 140, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
selectedRows,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange
|
||||
} = useSaiAdmin()
|
||||
|
||||
/**
|
||||
* 切换展开/收起所有菜单
|
||||
*/
|
||||
const toggleExpand = (): void => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
nextTick(() => {
|
||||
if (tableRef.value?.elTableRef && data.value) {
|
||||
const processRows = (rows: any[]) => {
|
||||
rows.forEach((row) => {
|
||||
if (row.children?.length) {
|
||||
tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value)
|
||||
processRows(row.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
processRows(data.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子项
|
||||
* @param row
|
||||
*/
|
||||
const handleAdd = (row: any) => {
|
||||
let data = { parent_id: row.id, type: 1 }
|
||||
if (row.type === 2) {
|
||||
data.type = 3
|
||||
}
|
||||
showDialog('add', data)
|
||||
}
|
||||
</script>
|
||||
322
saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue
Normal file
322
saiadmin-artd/src/views/system/menu/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增菜单' : '编辑菜单'"
|
||||
width="820px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="菜单类型" prop="type">
|
||||
<sa-radio v-model="formData.type" type="button" dict="menu_type"></sa-radio>
|
||||
</el-form-item>
|
||||
<el-form-item label="上级菜单" prop="parent_id">
|
||||
<el-tree-select
|
||||
v-model="formData.parent_id"
|
||||
:data="optionData.treeData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type < 3">
|
||||
<el-form-item prop="path">
|
||||
<template #label>
|
||||
<sa-label
|
||||
label="路由地址"
|
||||
tooltip="一级菜单:以 / 开头的绝对路径(如 /dashboard) 二级及以下:相对路径(如 console、user)"
|
||||
/>
|
||||
</template>
|
||||
<el-input v-model="formData.path" placeholder="如:/dashboard 或 console" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type != 3">
|
||||
<el-form-item label="组件名称" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="如: User" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type === 2">
|
||||
<el-form-item prop="component">
|
||||
<template #label>
|
||||
<sa-label label="组件路径" tooltip="填写组件路径(views目录下) 目录菜单:留空" />
|
||||
</template>
|
||||
<el-autocomplete
|
||||
class="w-full"
|
||||
v-model="formData.component"
|
||||
:fetch-suggestions="querySearch"
|
||||
clearable
|
||||
placeholder="如:/system/user 或留空"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type != 3">
|
||||
<el-form-item label="菜单图标" prop="icon">
|
||||
<sa-icon-picker v-model="formData.icon" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.type === 3">
|
||||
<el-form-item label="权限标识" prop="slug">
|
||||
<el-input v-model="formData.slug" placeholder="请输入权限标识" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" v-if="formData.type === 4">
|
||||
<el-form-item label="外链地址" prop="link_url">
|
||||
<el-input v-model="formData.link_url" placeholder="如:https://saithink.top" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="sort">
|
||||
<template #label>
|
||||
<sa-label label="排序" tooltip="数字越大越靠前" />
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="formData.sort"
|
||||
placeholder="请输入排序"
|
||||
controls-position="right"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="status">
|
||||
<template #label>
|
||||
<sa-label label="状态" tooltip="禁用后,该菜单项将不可用" />
|
||||
</template>
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_iframe">
|
||||
<template #label>
|
||||
<sa-label label="是否内嵌" tooltip="外链模式下有效" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_iframe" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_keep_alive">
|
||||
<template #label>
|
||||
<sa-label label="是否缓存" tooltip="切换tabs不刷新" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_keep_alive" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_hidden">
|
||||
<template #label>
|
||||
<sa-label label="是否隐藏" tooltip="不在菜单栏显示,但是可以通过路由访问" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_hidden" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_fixed_tab">
|
||||
<template #label>
|
||||
<sa-label label="是否固定" tooltip="固定在tabs导航栏" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_fixed_tab" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="is_full_page">
|
||||
<template #label>
|
||||
<sa-label label="是否全屏" tooltip="不继承左侧菜单和顶部导航栏" />
|
||||
</template>
|
||||
<sa-switch v-model="formData.is_full_page" dict="yes_or_no" :showText="false" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/menu'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
treeData: <any[]>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 处理自动查找视图文件
|
||||
*/
|
||||
const modules = import.meta.glob('/src/views/**/*.vue')
|
||||
const getModulesKey = () => {
|
||||
return Object.keys(modules).map((item) => item.replace('/src/views/', '/').replace('.vue', ''))
|
||||
}
|
||||
const componentsOptions = ref(getModulesKey())
|
||||
const querySearch = (queryString: string, cb: any) => {
|
||||
const results = queryString
|
||||
? componentsOptions.value.filter((item) =>
|
||||
item.toLowerCase().includes(queryString.toLowerCase())
|
||||
)
|
||||
: componentsOptions.value
|
||||
cb(results.map((item) => ({ value: item })))
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
parent_id: [{ required: true, message: '请选择上级菜单', trigger: 'change' }],
|
||||
name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
path: [{ required: true, message: '请输入路由地址', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入组件名称', trigger: 'blur' }],
|
||||
slug: [{ required: true, message: '请输入权限标识', trigger: 'blur' }],
|
||||
link_url: [{ required: true, message: '请输入外链地址', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
parent_id: null,
|
||||
type: 1,
|
||||
component: '',
|
||||
name: '',
|
||||
slug: '',
|
||||
path: '',
|
||||
icon: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
link_url: '',
|
||||
is_iframe: 2,
|
||||
is_keep_alive: 2,
|
||||
is_hidden: 2,
|
||||
is_fixed_tab: 2,
|
||||
is_full_page: 2,
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
const data = await api.list({ tree: true })
|
||||
optionData.treeData = [
|
||||
{
|
||||
id: 0,
|
||||
value: 0,
|
||||
label: '无上级菜单',
|
||||
children: data
|
||||
}
|
||||
]
|
||||
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
77
saiadmin-artd/src/views/system/menu/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/menu/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="菜单名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入菜单名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="菜单路由" prop="path">
|
||||
<el-input v-model="formData.path" placeholder="请输入菜单路由" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
142
saiadmin-artd/src/views/system/post/index.vue
Normal file
142
saiadmin-artd/src/views/system/post/index.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams" />
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:post:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-permission="'core:post:destroy'"
|
||||
:disabled="selectedRows.length === 0"
|
||||
@click="deleteSelectedRows(api.delete, refreshData)"
|
||||
v-ripple
|
||||
>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:delete-bin-5-line" />
|
||||
</template>
|
||||
删除
|
||||
</ElButton>
|
||||
<SaImport
|
||||
v-permission="'core:post:import'"
|
||||
download-url="/core/post/downloadTemplate"
|
||||
upload-url="/core/post/import"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<SaExport v-permission="'core:post:export'" url="/core/post/export" />
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
ref="tableRef"
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2">
|
||||
<SaButton
|
||||
v-permission="'core:post:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:post:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/post'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
searchParams,
|
||||
pagination,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ type: 'selection' },
|
||||
{ prop: 'id', label: '编号', width: 100, align: 'center' },
|
||||
{ prop: 'name', label: '岗位名称', minWidth: 120 },
|
||||
{ prop: 'code', label: '岗位编码', minWidth: 120 },
|
||||
{ prop: 'remark', label: '描述', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'sort', label: '排序', width: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status', width: 100 },
|
||||
{ prop: 'create_time', label: '创建日期', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 100, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const {
|
||||
dialogType,
|
||||
dialogVisible,
|
||||
dialogData,
|
||||
showDialog,
|
||||
deleteRow,
|
||||
deleteSelectedRows,
|
||||
handleSelectionChange,
|
||||
selectedRows
|
||||
} = useSaiAdmin()
|
||||
</script>
|
||||
164
saiadmin-artd/src/views/system/post/modules/edit-dialog.vue
Normal file
164
saiadmin-artd/src/views/system/post/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增岗位' : '编辑岗位'"
|
||||
width="600px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="岗位名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入岗位编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入岗位描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/post'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入岗位名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入岗位编码', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
77
saiadmin-artd/src/views/system/post/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/post/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="岗位名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入岗位名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="岗位编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入岗位编码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
152
saiadmin-artd/src/views/system/role/index.vue
Normal file
152
saiadmin-artd/src/views/system/role/index.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="art-full-height">
|
||||
<!-- 搜索面板 -->
|
||||
<TableSearch
|
||||
v-model="searchForm"
|
||||
@search="handleSearch"
|
||||
@reset="resetSearchParams"
|
||||
></TableSearch>
|
||||
|
||||
<ElCard class="art-table-card" shadow="never">
|
||||
<!-- 表格头部 -->
|
||||
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
||||
<template #left>
|
||||
<ElSpace wrap>
|
||||
<ElButton v-permission="'core:role:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
rowKey="id"
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2" v-if="row.id !== 1">
|
||||
<SaButton
|
||||
v-permission="'core:role:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:role:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
<ElDropdown>
|
||||
<ArtIconButton
|
||||
icon="ri:more-2-fill"
|
||||
class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-permission="'core:role:menu'"
|
||||
@click="showPermissionDialog('edit', row)"
|
||||
>
|
||||
<div class="flex-c gap-2">
|
||||
<ArtSvgIcon icon="ri:user-3-line" />
|
||||
<span>菜单权限</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
|
||||
<!-- 菜单权限弹窗 -->
|
||||
<PermissionDialog
|
||||
v-model="permissionDialogVisible"
|
||||
:dialog-type="permissionDialogType"
|
||||
:data="permissionDialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import api from '@/api/system/role'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import PermissionDialog from './modules/permission-dialog.vue'
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: undefined,
|
||||
code: undefined,
|
||||
status: undefined
|
||||
})
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
// 表格配置
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
getData,
|
||||
pagination,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
columnsFactory: () => [
|
||||
{ prop: 'id', label: '编号', minWidth: 60, align: 'center' },
|
||||
{ prop: 'name', label: '角色名称', minWidth: 120 },
|
||||
{ prop: 'code', label: '角色编码', minWidth: 120 },
|
||||
{ prop: 'level', label: '角色级别', minWidth: 100, sortable: true },
|
||||
{ prop: 'remark', label: '角色描述', minWidth: 150, showOverflowTooltip: true },
|
||||
{ prop: 'sort', label: '排序', minWidth: 100 },
|
||||
{ prop: 'status', label: '状态', saiType: 'dict', saiDict: 'data_status' },
|
||||
{ prop: 'create_time', label: '创建日期', width: 180, sortable: true },
|
||||
{ prop: 'operation', label: '操作', width: 140, fixed: 'right', useSlot: true }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 编辑配置
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
|
||||
|
||||
// 权限配置
|
||||
const {
|
||||
dialogType: permissionDialogType,
|
||||
dialogVisible: permissionDialogVisible,
|
||||
dialogData: permissionDialogData,
|
||||
showDialog: showPermissionDialog
|
||||
} = useSaiAdmin()
|
||||
</script>
|
||||
171
saiadmin-artd/src/views/system/role/modules/edit-dialog.vue
Normal file
171
saiadmin-artd/src/views/system/role/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,171 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增角色' : '编辑角色'"
|
||||
width="600px"
|
||||
align-center
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色标识" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入角色编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色级别" prop="level">
|
||||
<el-input-number v-model="formData.level" placeholder="角色级别" :max="99" :min="1" />
|
||||
</el-form-item>
|
||||
<div class="text-xs text-gray-400 pl-32 pb-4"
|
||||
>控制角色的权限层级, 不能操作职级高于自己的角色</div
|
||||
>
|
||||
<el-form-item label="描述" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入角色描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入排序" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/role'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
level: [{ required: true, message: '请输入角色级别', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
level: 1,
|
||||
name: '',
|
||||
code: '',
|
||||
remark: '',
|
||||
sort: 100,
|
||||
status: 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<ElDialog
|
||||
v-model="visible"
|
||||
title="菜单权限"
|
||||
width="600px"
|
||||
align-center
|
||||
class="el-dialog-border"
|
||||
@close="handleClose"
|
||||
>
|
||||
<ElScrollbar height="70vh">
|
||||
<ElTree
|
||||
ref="treeRef"
|
||||
:data="menuList"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:default-expand-all="false"
|
||||
:check-strictly="checkStrictly"
|
||||
>
|
||||
<template #default="{ data }">
|
||||
<div class="flex-c gap-2">
|
||||
<span>{{ data.label }}</span>
|
||||
<ElTag :type="getMenuTypeTag(data)" size="small">{{ getMenuTypeText(data) }}</ElTag>
|
||||
</div>
|
||||
</template>
|
||||
</ElTree>
|
||||
</ElScrollbar>
|
||||
<template #footer>
|
||||
<ElButton @click="toggleExpandAll" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon v-if="isExpandAll" icon="ri:collapse-diagonal-line" />
|
||||
<ArtSvgIcon v-else icon="ri:expand-diagonal-line" />
|
||||
</template>
|
||||
{{ isExpandAll ? '收起' : '展开' }}
|
||||
</ElButton>
|
||||
<ElButton @click="toggleCheck" style="margin-left: 8px">{{
|
||||
checkStrictly ? '非关联选择模式' : '关联选择模式'
|
||||
}}</ElButton>
|
||||
<ElButton type="primary" @click="savePermission">保存</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/role'
|
||||
import menuApi from '@/api/system/menu'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const menuList = ref<Api.Common.ApiData[]>([])
|
||||
const treeRef = ref()
|
||||
|
||||
const isExpandAll = ref(true)
|
||||
const checkStrictly = ref(true)
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 菜单列表
|
||||
menuList.value = await menuApi.accessMenu({ tree: true })
|
||||
// 角色数据
|
||||
const data = await api.menuByRole({ id: props.data?.id })
|
||||
treeRef.value.setCheckedKeys(data.menus?.map((item: any) => item.id))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单类型标签颜色
|
||||
* @param row 菜单行数据
|
||||
* @returns 标签颜色类型
|
||||
*/
|
||||
const getMenuTypeTag = (row: any): 'primary' | 'success' | 'warning' | 'info' | 'danger' => {
|
||||
if (row.type == 1) return 'info'
|
||||
if (row.type == 2) return 'primary'
|
||||
if (row.type == 3) return 'danger'
|
||||
if (row.type == 4) return 'success'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取菜单类型文本
|
||||
* @param row 菜单行数据
|
||||
* @returns 菜单类型文本
|
||||
*/
|
||||
const getMenuTypeText = (row: any): string => {
|
||||
if (row.type == 1) return '目录'
|
||||
if (row.type == 2) return '菜单'
|
||||
if (row.type == 3) return '按钮'
|
||||
if (row.type == 4) return '外链'
|
||||
return '未知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并清空选中状态
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
treeRef.value?.setCheckedKeys([])
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存权限配置
|
||||
*/
|
||||
const savePermission = async () => {
|
||||
// TODO: 调用保存权限接口
|
||||
const checkedKeys = treeRef.value.getCheckedKeys()
|
||||
try {
|
||||
await api.menuPermission({
|
||||
id: props.data?.id,
|
||||
menu_ids: checkedKeys
|
||||
})
|
||||
ElMessage.success('保存成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换全部展开/收起状态
|
||||
*/
|
||||
const toggleExpandAll = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
const nodes = tree.store.nodesMap
|
||||
// 这里保留 any,因为 Element Plus 的内部节点类型较复杂
|
||||
Object.values(nodes).forEach((node: any) => {
|
||||
node.expanded = !isExpandAll.value
|
||||
})
|
||||
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换关联选择/非关联选择状态
|
||||
*/
|
||||
const toggleCheck = () => {
|
||||
const tree = treeRef.value
|
||||
if (!tree) return
|
||||
|
||||
checkStrictly.value = !checkStrictly.value
|
||||
}
|
||||
</script>
|
||||
77
saiadmin-artd/src/views/system/role/modules/table-search.vue
Normal file
77
saiadmin-artd/src/views/system/role/modules/table-search.vue
Normal file
@@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
label-width="100px"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入角色名称" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="角色编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="请输入角色编码" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
// 展开/收起
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
280
saiadmin-artd/src/views/system/user/index.vue
Normal file
280
saiadmin-artd/src/views/system/user/index.vue
Normal file
@@ -0,0 +1,280 @@
|
||||
<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-64 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="{ children: 'children', label: 'label' }"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</ElScrollbar>
|
||||
</ElCard>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col flex-grow min-w-0">
|
||||
<!-- 搜索栏 -->
|
||||
<TableSearch v-model="searchForm" @search="handleSearch" @reset="handleReset" />
|
||||
|
||||
<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 v-permission="'core:user:save'" @click="showDialog('add')" v-ripple>
|
||||
<template #icon>
|
||||
<ArtSvgIcon icon="ri:add-fill" />
|
||||
</template>
|
||||
新增
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</template>
|
||||
</ArtTableHeader>
|
||||
|
||||
<!-- 表格 -->
|
||||
<ArtTable
|
||||
:loading="loading"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
@sort-change="handleSortChange"
|
||||
@selection-change="handleSelectionChange"
|
||||
@pagination:size-change="handleSizeChange"
|
||||
@pagination:current-change="handleCurrentChange"
|
||||
>
|
||||
<!-- 操作列 -->
|
||||
<template #operation="{ row }">
|
||||
<div class="flex gap-2" v-if="row.id !== 1 && userStore.info.id !== row.id">
|
||||
<SaButton
|
||||
v-permission="'core:user:update'"
|
||||
type="secondary"
|
||||
@click="showDialog('edit', row)"
|
||||
/>
|
||||
<SaButton
|
||||
v-permission="'core:user:destroy'"
|
||||
type="error"
|
||||
@click="deleteRow(row, api.delete, refreshData)"
|
||||
/>
|
||||
<ElDropdown>
|
||||
<ArtIconButton
|
||||
icon="ri:more-2-fill"
|
||||
class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
class="flex-c gap-2"
|
||||
v-permission="'core:user:home'"
|
||||
@click="showWorkDialog('edit', row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:home-office-line" />
|
||||
<span>设置首页</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
class="flex-c gap-2"
|
||||
v-permission="'core:user:password'"
|
||||
@click="handlePassword(row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:key-line" />
|
||||
<span>修改密码</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<ElDropdownItem>
|
||||
<div
|
||||
class="flex-c gap-2"
|
||||
v-permission="'core:user:cache'"
|
||||
@click="handleCache(row)"
|
||||
>
|
||||
<ArtSvgIcon icon="ri:eraser-line" />
|
||||
<span>清理缓存</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</ArtTable>
|
||||
</ElCard>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 表单弹窗 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:dialog-type="dialogType"
|
||||
:data="dialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
<!-- 工作台弹窗 -->
|
||||
<WorkDialog
|
||||
v-model="workDialogVisible"
|
||||
:dialog-type="workDialogType"
|
||||
:data="workDialogData"
|
||||
@success="refreshData"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useTable } from '@/hooks/core/useTable'
|
||||
import { useSaiAdmin } from '@/composables/useSaiAdmin'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import TableSearch from './modules/table-search.vue'
|
||||
import EditDialog from './modules/edit-dialog.vue'
|
||||
import WorkDialog from './modules/work-dialog.vue'
|
||||
import api from '@/api/system/user'
|
||||
import deptApi from '@/api/system/dept'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const treeData = ref([])
|
||||
|
||||
// 编辑框
|
||||
const { dialogType, dialogVisible, dialogData, showDialog, handleSelectionChange, deleteRow } =
|
||||
useSaiAdmin()
|
||||
|
||||
const {
|
||||
dialogType: workDialogType,
|
||||
dialogVisible: workDialogVisible,
|
||||
dialogData: workDialogData,
|
||||
showDialog: showWorkDialog
|
||||
} = useSaiAdmin()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
username: undefined,
|
||||
phone: undefined,
|
||||
email: undefined,
|
||||
dept_id: undefined,
|
||||
status: ''
|
||||
})
|
||||
|
||||
const {
|
||||
columns,
|
||||
columnChecks,
|
||||
data,
|
||||
loading,
|
||||
pagination,
|
||||
getData,
|
||||
searchParams,
|
||||
resetSearchParams,
|
||||
handleSortChange,
|
||||
handleSizeChange,
|
||||
handleCurrentChange,
|
||||
refreshData
|
||||
} = useTable({
|
||||
core: {
|
||||
apiFn: api.list,
|
||||
apiParams: {
|
||||
...searchForm.value
|
||||
},
|
||||
columnsFactory: () => [
|
||||
{ type: 'index', width: 60, label: '序号' },
|
||||
{
|
||||
prop: 'avatar',
|
||||
label: '用户名',
|
||||
minWidth: 200,
|
||||
saiType: 'imageAndText',
|
||||
saiFirst: 'username',
|
||||
saiSecond: 'email'
|
||||
},
|
||||
{ prop: 'phone', label: '手机号', width: 120 },
|
||||
{ prop: 'depts.name', label: '部门', minWidth: 150 },
|
||||
{ prop: 'status', label: '状态', width: 80, saiType: 'dict', saiDict: 'data_status' },
|
||||
{ prop: 'dashboard', label: '首页', width: 100, saiType: 'dict', saiDict: 'dashboard' },
|
||||
{ prop: 'login_time', label: '上次登录', width: 170, sortable: true },
|
||||
{
|
||||
prop: 'operation',
|
||||
label: '操作',
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
useSlot: true
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 搜索
|
||||
* @param params
|
||||
*/
|
||||
const handleSearch = (params: Record<string, any>) => {
|
||||
Object.assign(searchParams, params)
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置
|
||||
*/
|
||||
const handleReset = () => {
|
||||
searchForm.value.dept_id = undefined
|
||||
resetSearchParams()
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换部门
|
||||
* @param data
|
||||
*/
|
||||
const handleNodeClick = (data: any) => {
|
||||
searchParams.dept_id = data.id
|
||||
getData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门数据
|
||||
*/
|
||||
const getDeptList = () => {
|
||||
deptApi.accessDept().then((data: any) => {
|
||||
treeData.value = data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
* @param row
|
||||
*/
|
||||
const handlePassword = (row: any) => {
|
||||
ElMessageBox.prompt('请输入新密码', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputPattern: /^.{6,16}$/,
|
||||
inputErrorMessage: '密码长度在6到16之间',
|
||||
type: 'warning'
|
||||
}).then(({ value }) => {
|
||||
api.changePassword({ id: row.id, password: value }).then(() => {
|
||||
ElMessage.success('修改密码成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
* @param row
|
||||
*/
|
||||
const handleCache = (row: any) => {
|
||||
ElMessageBox.confirm('确定要清理缓存吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
api.clearCache({ id: row.id }).then(() => {
|
||||
ElMessage.success('清理缓存成功')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDeptList()
|
||||
})
|
||||
</script>
|
||||
301
saiadmin-artd/src/views/system/user/modules/edit-dialog.vue
Normal file
301
saiadmin-artd/src/views/system/user/modules/edit-dialog.vue
Normal file
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="dialogType === 'add' ? '新增用户' : '编辑用户'"
|
||||
width="800px"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
|
||||
<el-form-item label="头像" prop="avatar">
|
||||
<sa-image-picker v-model="formData.avatar" round />
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="formData.username" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="真实姓名" prop="realname">
|
||||
<el-input v-model="formData.realname" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row v-if="dialogType === 'add'">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input type="password" v-model="formData.password" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="确认密码" prop="password_confirm">
|
||||
<el-input type="password" v-model="formData.password_confirm" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门" prop="dept_id">
|
||||
<el-tree-select
|
||||
v-model="formData.dept_id"
|
||||
:data="optionData.deptData"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="角色" prop="role_ids">
|
||||
<el-select v-model="formData.role_ids" multiple clearable>
|
||||
<el-option
|
||||
v-for="role in optionData.roleList"
|
||||
:key="(role as any)?.id"
|
||||
:value="(role as any)?.id"
|
||||
:label="(role as any)?.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="岗位" prop="post_ids">
|
||||
<el-select v-model="formData.post_ids" multiple clearable>
|
||||
<el-option
|
||||
v-for="post in optionData.postList"
|
||||
:key="(post as any)?.id"
|
||||
:value="(post as any)?.id"
|
||||
:label="(post as any)?.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="性别" prop="gender">
|
||||
<sa-radio v-model="formData.gender" dict="gender" valueType="string" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-radio v-model="formData.status" dict="data_status" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="formData.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import api from '@/api/system/user'
|
||||
import deptApi from '@/api/system/dept'
|
||||
import roleApi from '@/api/system/role'
|
||||
import postApi from '@/api/system/post'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const optionData = reactive({
|
||||
deptData: <any>[],
|
||||
roleList: <any>[],
|
||||
postList: <any>[]
|
||||
})
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
const validatePasswordConfirm = (rule: any, value: any, callback: any) => {
|
||||
if (value !== formData.password) {
|
||||
callback(new Error('两次输入的密码不一致'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const rules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password_confirm: [
|
||||
{ required: true, message: '请输入确认密码', trigger: 'blur' },
|
||||
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' },
|
||||
{ validator: validatePasswordConfirm, trigger: 'blur' }
|
||||
],
|
||||
dept_id: [{ required: true, message: '请选择部门', trigger: 'change' }],
|
||||
role_ids: [{ required: true, message: '请选择角色', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// 初始表单数据
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
avatar: '',
|
||||
username: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
realname: '',
|
||||
dept_id: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
role_ids: [],
|
||||
post_ids: [],
|
||||
status: 1,
|
||||
gender: '',
|
||||
remark: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
// 初始化页面数据
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 部门数据
|
||||
const deptData = await deptApi.accessDept()
|
||||
optionData.deptData = deptData
|
||||
// 角色数据
|
||||
const roleData = await roleApi.accessRole()
|
||||
optionData.roleList = roleData
|
||||
// 岗位数据
|
||||
const postData = await postApi.accessPost()
|
||||
optionData.postList = postData
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
if (props.data.id) {
|
||||
let data = await api.read(props.data.id)
|
||||
if (data.postList) {
|
||||
const post = (data.postList as any[])?.map((item: any) => item.id)
|
||||
data.post_ids = post
|
||||
}
|
||||
const role = (data.roleList as any[])?.map((item: any) => item.id)
|
||||
data.role_ids = role
|
||||
data.password = ''
|
||||
initForm(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = (data: any) => {
|
||||
if (data) {
|
||||
for (const key in formData) {
|
||||
if (data[key] != null && data[key] != undefined) {
|
||||
;(formData as any)[key] = data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'add') {
|
||||
await api.save(formData)
|
||||
ElMessage.success('新增成功')
|
||||
} else {
|
||||
await api.update(formData)
|
||||
ElMessage.success('修改成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
76
saiadmin-artd/src/views/system/user/modules/table-search.vue
Normal file
76
saiadmin-artd/src/views/system/user/modules/table-search.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<sa-search-bar
|
||||
ref="searchBarRef"
|
||||
v-model="formData"
|
||||
:label-width="'70px'"
|
||||
:showExpand="false"
|
||||
@reset="handleReset"
|
||||
@search="handleSearch"
|
||||
@expand="handleExpand"
|
||||
>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="formData.username" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="formData.phone" placeholder="请输入手机号" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-bind="setSpan(6)">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<sa-select v-model="formData.status" dict="data_status" clearable />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</sa-search-bar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
modelValue: Record<string, any>
|
||||
}
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
(e: 'search', params: Record<string, any>): void
|
||||
(e: 'reset'): void
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
const isExpanded = ref<boolean>(false)
|
||||
|
||||
// 表单数据双向绑定
|
||||
const searchBarRef = ref()
|
||||
const formData = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (val) => emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置
|
||||
function handleReset() {
|
||||
searchBarRef.value?.ref.resetFields()
|
||||
emit('reset')
|
||||
}
|
||||
|
||||
// 搜索
|
||||
async function handleSearch() {
|
||||
emit('search', formData.value)
|
||||
}
|
||||
|
||||
// 展开/收起
|
||||
function handleExpand(expanded: boolean) {
|
||||
isExpanded.value = expanded
|
||||
}
|
||||
|
||||
// 栅格占据的列数
|
||||
const setSpan = (span: number) => {
|
||||
return {
|
||||
span: span,
|
||||
xs: 24, // 手机:满宽显示
|
||||
sm: span >= 12 ? span : 12, // 平板:大于等于12保持,否则用半宽
|
||||
md: span >= 8 ? span : 8, // 中等屏幕:大于等于8保持,否则用三分之一宽
|
||||
lg: span,
|
||||
xl: span
|
||||
}
|
||||
}
|
||||
</script>
|
||||
132
saiadmin-artd/src/views/system/user/modules/work-dialog.vue
Normal file
132
saiadmin-artd/src/views/system/user/modules/work-dialog.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="设置工作台" width="600px" align-center @close="handleClose">
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
|
||||
<el-form-item label="工作台" prop="dashboard">
|
||||
<sa-select v-model="formData.dashboard" dict="dashboard" placeholder="请选择工作台" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import api from '@/api/system/user'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
interface Props {
|
||||
modelValue: boolean
|
||||
dialogType: string
|
||||
data?: Record<string, any>
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'success'): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
modelValue: false,
|
||||
dialogType: 'add',
|
||||
data: undefined
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 弹窗显示状态双向绑定
|
||||
*/
|
||||
const visible = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单验证规则
|
||||
*/
|
||||
const rules = reactive<FormRules>({
|
||||
dashboard: [{ required: true, message: '请选择工作台', trigger: 'blur' }]
|
||||
})
|
||||
|
||||
/**
|
||||
* 初始数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: null,
|
||||
dashboard: ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
/**
|
||||
* 监听弹窗打开,初始化表单数据
|
||||
*/
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
if (newVal) {
|
||||
initPage()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* 初始化页面数据
|
||||
*/
|
||||
const initPage = async () => {
|
||||
// 先重置为初始值
|
||||
Object.assign(formData, initialFormData)
|
||||
// 如果有数据,则填充数据
|
||||
if (props.data) {
|
||||
await nextTick()
|
||||
initForm()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化表单数据
|
||||
*/
|
||||
const initForm = () => {
|
||||
if (props.data) {
|
||||
for (const key in formData) {
|
||||
if (props.data[key] != null && props.data[key] != undefined) {
|
||||
;(formData as any)[key] = props.data[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗并重置表单
|
||||
*/
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
if (props.dialogType === 'edit') {
|
||||
await api.setHomePage(formData)
|
||||
ElMessage.success('操作成功')
|
||||
}
|
||||
emit('success')
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
console.log('表单验证失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user