1.将部门修改为渠道,并且所有dice_表关联渠道表

2.将所有配置表,记录表设置关联渠道
3.优化后台页面设置
This commit is contained in:
2026-05-19 09:49:02 +08:00
parent 085454fb78
commit dd264b1e97
143 changed files with 4741 additions and 1254 deletions

View File

@@ -1,10 +1,8 @@
<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>
@@ -14,30 +12,22 @@
</template>
{{ $t('table.actions.add') }}
</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 ? $t('table.searchBar.collapse') : $t('table.searchBar.expand') }}
<ElButton v-permission="'core:dept:update'" @click="handleSyncConfigs" v-ripple>
补齐渠道配置
</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
@@ -48,20 +38,26 @@
<SaButton
v-permission="'core:dept:destroy'"
type="error"
@click="deleteRow(row, api.delete, refreshData)"
@click="openDeleteDialog(row)"
/>
</div>
</template>
</ArtTable>
</ElCard>
<!-- 编辑弹窗 -->
<EditDialog
v-model="dialogVisible"
:dialog-type="dialogType"
:data="dialogData"
@success="refreshData"
/>
<DeleteChannelDialog
v-model="deleteDialogVisible"
:dept-id="deleteDeptId"
:dept-name="deleteDeptName"
@success="refreshData"
/>
</div>
</template>
@@ -69,27 +65,26 @@
import { useTable } from '@/hooks/core/useTable'
import { useSaiAdmin } from '@/composables/useSaiAdmin'
import api from '@/api/system/dept'
import { ElMessage } from 'element-plus'
import TableSearch from './modules/table-search.vue'
import EditDialog from './modules/edit-dialog.vue'
import DeleteChannelDialog from './modules/delete-channel-dialog.vue'
// 状态管理
const isExpanded = ref(true)
const tableRef = ref()
// 搜索表单
const searchForm = ref({
name: undefined,
code: undefined,
status: undefined
})
// 搜索处理
const deleteDialogVisible = ref(false)
const deleteDeptId = ref<number | null>(null)
const deleteDeptName = ref('')
const handleSearch = (params: Record<string, any>) => {
Object.assign(searchParams, params)
getData()
}
// 表格配置
const {
columns,
columnChecks,
@@ -118,26 +113,20 @@
}
})
// 编辑配置
const { dialogType, dialogVisible, dialogData, showDialog, deleteRow } = useSaiAdmin()
const { dialogType, dialogVisible, dialogData, showDialog } = 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)
}
})
const openDeleteDialog = (row: any) => {
deleteDeptId.value = row.id
deleteDeptName.value = row.name
deleteDialogVisible.value = true
}
const handleSyncConfigs = async () => {
try {
await api.syncChannelConfigs()
ElMessage.success('已为缺失配置的渠道补齐默认配置')
} catch (e: any) {
ElMessage.error(e?.message || '补齐失败')
}
}
</script>

View File

@@ -0,0 +1,115 @@
<template>
<el-dialog
v-model="visible"
title="删除渠道"
width="560px"
align-center
:close-on-click-modal="false"
@close="handleClose"
>
<div v-loading="loading">
<p class="mb-3 text-sm text-gray-600">确定删除渠道{{ deptName }}可勾选一并删除的关联数据</p>
<el-alert
v-if="preview?.user_count > 0"
type="error"
:closable="false"
class="mb-3"
:title="`该渠道下仍有 ${preview.user_count} 个用户,请先转移或删除用户`"
/>
<el-checkbox-group v-model="checkedTables" class="flex flex-col gap-2">
<el-checkbox
v-for="item in preview?.relations || []"
:key="item.table"
:label="item.table"
:disabled="preview?.user_count > 0"
>
{{ item.label }}{{ item.count }}
</el-checkbox>
</el-checkbox-group>
<p v-if="!preview?.relations?.length" class="text-sm text-gray-500">无关联业务数据仅删除渠道本身</p>
</div>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button
type="danger"
:loading="submitting"
:disabled="preview?.user_count > 0"
@click="handleConfirm"
>
确认删除
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import api from '@/api/system/dept'
import { ElMessage } from 'element-plus'
interface Props {
modelValue: boolean
deptId?: number | null
deptName?: string
}
const props = withDefaults(defineProps<Props>(), {
modelValue: false,
deptId: null,
deptName: ''
})
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'success'): void
}>()
const visible = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
const loading = ref(false)
const submitting = ref(false)
const preview = ref<any>(null)
const checkedTables = ref<string[]>([])
watch(
() => props.modelValue,
async (open) => {
if (!open || !props.deptId) {
return
}
loading.value = true
checkedTables.value = []
try {
const res: any = await api.destroyPreview(props.deptId)
const list = res?.data ?? res
preview.value = Array.isArray(list) ? list[0] : list
checkedTables.value = (preview.value?.relations || []).map((r: any) => r.table)
} finally {
loading.value = false
}
}
)
const handleClose = () => {
visible.value = false
}
const handleConfirm = async () => {
if (!props.deptId) {
return
}
submitting.value = true
try {
await api.delete({ ids: props.deptId, delete_tables: checkedTables.value })
ElMessage.success('删除成功')
emit('success')
handleClose()
} catch (e: any) {
ElMessage.error(e?.message || '删除失败')
} finally {
submitting.value = false
}
}
</script>

View File

@@ -8,15 +8,6 @@
@close="handleClose"
>
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px">
<el-form-item :label="$t('page.form.labelParentDept')" 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="$t('page.form.labelDeptName')" prop="name">
<el-input v-model="formData.name" :placeholder="$t('page.form.placeholderDeptName')" />
</el-form-item>
@@ -75,36 +66,21 @@
const { t } = useI18n()
const formRef = ref<FormInstance>()
const optionData = reactive({
treeData: <any[]>[]
})
/**
* 弹窗显示状态双向绑定
*/
const visible = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
/**
* 表单验证规则
*/
const rules = computed<FormRules>(() => ({
parent_id: [
{ required: true, message: t('page.form.ruleParentDeptRequired'), trigger: 'change' }
],
name: [{ required: true, message: t('page.form.ruleDeptNameRequired'), trigger: 'blur' }],
code: [{ required: true, message: t('page.form.ruleDeptCodeRequired'), trigger: 'blur' }]
}))
/**
* 初始数据
*/
const initialFormData = {
id: null,
parent_id: null,
level: '',
parent_id: 0,
level: '0',
name: '',
code: '',
leader_id: null,
@@ -113,14 +89,8 @@
status: 1
}
/**
* 表单数据
*/
const formData = reactive({ ...initialFormData })
/**
* 监听弹窗打开,初始化表单数据
*/
watch(
() => props.modelValue,
(newVal) => {
@@ -130,33 +100,14 @@
}
)
/**
* 初始化页面数据
*/
const initPage = async () => {
// 先重置为初始值
Object.assign(formData, initialFormData)
const data = await api.list({ tree: true })
optionData.treeData = [
{
id: 0,
value: 0,
label: t('page.form.noParentDept'),
children: data
}
]
// 如果有数据,则填充数据
if (props.data) {
await nextTick()
initForm()
}
}
/**
* 初始化表单数据
*/
const initForm = () => {
if (props.data) {
for (const key in formData) {
@@ -167,21 +118,17 @@
}
}
/**
* 关闭弹窗并重置表单
*/
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
/**
* 提交表单
*/
const handleSubmit = async () => {
if (!formRef.value) return
try {
await formRef.value.validate()
formData.parent_id = 0
formData.level = '0'
if (props.dialogType === 'add') {
await api.save(formData)
ElMessage.success(t('page.form.addSuccess'))

View File

@@ -41,6 +41,7 @@
<script setup lang="ts">
import api from '@/api/system/role'
import { withChannelDeptParams } from '@/composables/useChannelDeptScope'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { useI18n } from 'vue-i18n'
@@ -155,11 +156,12 @@
if (!formRef.value) return
try {
await formRef.value.validate()
const payload = withChannelDeptParams({ ...formData })
if (props.dialogType === 'add') {
await api.save(formData)
await api.save(payload)
ElMessage.success(t('page.form.addSuccess'))
} else {
await api.update(formData)
await api.update(payload)
ElMessage.success(t('page.form.editSuccess'))
}
emit('success')

View File

@@ -1,16 +1,24 @@
<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">
<div
v-show="showChannelSidebar"
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"
v-loading="channelTreeLoading"
>
<template #header>
<b>部门列表</b>
<b>{{ $t('page.ui.channelList') }}</b>
</template>
<ElScrollbar>
<ElTree
:data="treeData"
:props="{ children: 'children', label: 'label' }"
node-key="id"
:current-node-key="currentChannelId"
default-expand-all
highlight-current
@node-click="handleNodeClick"
@@ -136,10 +144,19 @@
import WorkDialog from './modules/work-dialog.vue'
import api from '@/api/system/user'
import deptApi from '@/api/system/dept'
import { isSuperAdminUser } from '@/utils/channelLayout'
const userStore = useUserStore()
const treeData = ref([])
interface ChannelTreeNode {
id: number
label: string
children?: ChannelTreeNode[]
}
const treeData = ref<ChannelTreeNode[]>([])
const channelTreeLoading = ref(false)
const currentChannelId = ref<number | undefined>(undefined)
// 编辑框
const { dialogType, dialogVisible, dialogData, showDialog, handleSelectionChange, deleteRow } =
@@ -228,24 +245,79 @@
const handleReset = () => {
searchForm.value.dept_id = undefined
resetSearchParams()
if (isSuperAdminUser()) {
currentChannelId.value = undefined
} else {
applyDefaultChannelSelection()
}
}
/**
* 切换部门
* @param data
*/
const handleNodeClick = (data: any) => {
const handleNodeClick = (data: ChannelTreeNode) => {
currentChannelId.value = data.id
searchParams.dept_id = data.id
getData()
}
/** 仅超管显示左侧渠道列表;渠道管理员固定本渠道,由后端过滤 */
const showChannelSidebar = computed(() => isSuperAdminUser())
const normalizeChannelTree = (list: unknown[]): ChannelTreeNode[] => {
if (!Array.isArray(list)) {
return []
}
return list
.map((item) => {
const row = item as Record<string, unknown>
const id = Number(row.id ?? row.value ?? 0)
const label = String(row.label ?? row.name ?? '')
return { id, label }
})
.filter((node) => node.id > 0 && node.label !== '')
}
const fallbackChannelTree = (): ChannelTreeNode[] => {
const dept = userStore.info?.department
if (!dept || Number(dept.id) <= 0) {
return []
}
return [
{
id: Number(dept.id),
label: String(dept.name ?? '')
}
]
}
const applyDefaultChannelSelection = () => {
if (treeData.value.length === 0 || isSuperAdminUser()) {
return
}
const first = treeData.value[0]
currentChannelId.value = first.id
searchParams.dept_id = first.id
getData()
}
/**
* 获取部门数据
* 获取可操作渠道(渠道管理员至少展示本渠道)
*/
const getDeptList = () => {
deptApi.accessDept().then((data: any) => {
treeData.value = data
})
const getDeptList = async () => {
channelTreeLoading.value = true
try {
const data = await deptApi.accessDept()
const nodes = normalizeChannelTree(Array.isArray(data) ? data : [])
treeData.value = nodes.length > 0 ? nodes : fallbackChannelTree()
applyDefaultChannelSelection()
} catch {
treeData.value = fallbackChannelTree()
applyDefaultChannelSelection()
} finally {
channelTreeLoading.value = false
}
}
/**
@@ -283,6 +355,10 @@
}
onMounted(() => {
getDeptList()
if (isSuperAdminUser()) {
getDeptList()
} else {
applyDefaultChannelSelection()
}
})
</script>

View File

@@ -52,13 +52,14 @@
<el-row>
<el-col :span="12">
<el-form-item :label="$t('page.form.labelDept')" prop="dept_id">
<el-tree-select
v-model="formData.dept_id"
:data="optionData.deptData"
:render-after-expand="false"
check-strictly
clearable
/>
<el-select v-model="formData.dept_id" clearable filterable>
<el-option
v-for="item in optionData.deptData"
:key="item.id"
:label="item.label"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
@@ -206,6 +207,41 @@
/**
* 监听弹窗打开,初始化表单数据
*/
const flattenDeptOptions = (list: any[], result: { id: number; label: string }[] = []) => {
for (const item of list) {
const id = item.id ?? item.value
if (id !== undefined && id !== null) {
result.push({
id,
label: String(item.label ?? item.name ?? id)
})
}
if (item.children?.length) {
flattenDeptOptions(item.children, result)
}
}
return result
}
const loadRoleOptions = async () => {
const deptId = formData.dept_id
const params =
deptId !== undefined && deptId !== null && deptId !== ''
? { dept_id: deptId }
: undefined
const roleData = await roleApi.accessRole(params)
optionData.roleList = Array.isArray(roleData) ? roleData : []
}
watch(
() => formData.dept_id,
() => {
if (props.modelValue) {
void loadRoleOptions()
}
}
)
watch(
() => props.modelValue,
(newVal) => {
@@ -214,26 +250,23 @@
}
}
)
// 初始化页面数据
const initPage = async () => {
// 先重置为初始值
Object.assign(formData, initialFormData)
// 部门数据
const deptData = await deptApi.accessDept()
optionData.deptData = deptData
optionData.deptData = flattenDeptOptions(Array.isArray(deptData) ? deptData : [])
// 角色数据
const roleData = await roleApi.accessRole()
optionData.roleList = roleData
// 如果有数据,则填充数据
if (props.data) {
if (props.data?.id) {
await nextTick()
if (props.data.id) {
let data = await api.read(props.data.id)
const role = (data.roleList as any[])?.map((item: any) => item.id)
data.role_ids = role
data.password = ''
initForm(data)
}
const data = await api.read(props.data.id)
const role = (data.roleList as any[])?.map((item: any) => item.id)
data.role_ids = role
data.password = ''
initForm(data)
await loadRoleOptions()
} else {
await loadRoleOptions()
}
}