365 lines
11 KiB
Vue
365 lines
11 KiB
Vue
<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
|
|
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>{{ $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"
|
|
/>
|
|
</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>
|
|
{{ $t('table.actions.add') }}
|
|
</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 { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { $t } from '@/locales'
|
|
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'
|
|
import { isSuperAdminUser } from '@/utils/channelLayout'
|
|
|
|
const userStore = useUserStore()
|
|
|
|
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 } =
|
|
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: 'table.column.index' },
|
|
{
|
|
prop: 'avatar',
|
|
label: 'page.table.username',
|
|
minWidth: 200,
|
|
saiType: 'imageAndText',
|
|
saiFirst: 'username',
|
|
saiSecond: 'email'
|
|
},
|
|
{ prop: 'phone', label: 'page.table.phone', width: 120 },
|
|
{
|
|
prop: 'dept_id',
|
|
label: 'page.table.dept',
|
|
minWidth: 150,
|
|
sortable: true,
|
|
formatter: (row: any) => row.depts?.name ?? ''
|
|
},
|
|
{ prop: 'status', label: 'page.table.status', width: 80, saiType: 'dict', saiDict: 'data_status' },
|
|
{ prop: 'agent_id', label: 'page.table.agentId', width: 120, showOverflowTooltip: true },
|
|
{ prop: 'dashboard', label: 'page.table.dashboard', width: 100, saiType: 'dict', saiDict: 'dashboard' },
|
|
{ prop: 'login_time', label: 'page.table.loginTime', width: 170, sortable: true },
|
|
{
|
|
prop: 'operation',
|
|
label: 'table.actions.operation',
|
|
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()
|
|
if (isSuperAdminUser()) {
|
|
currentChannelId.value = undefined
|
|
} else {
|
|
applyDefaultChannelSelection()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 切换部门
|
|
* @param data
|
|
*/
|
|
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 = 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
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 修改密码
|
|
* @param row
|
|
*/
|
|
const handlePassword = (row: any) => {
|
|
ElMessageBox.prompt($t('page.ui.promptNewPassword'), $t('uiMsg.titlePrompt'), {
|
|
confirmButtonText: $t('uiMsg.btnOk'),
|
|
cancelButtonText: $t('uiMsg.btnCancel'),
|
|
inputPattern: /^.{6,16}$/,
|
|
inputErrorMessage: $t('page.ui.passwordLengthError'),
|
|
type: 'warning'
|
|
}).then(({ value }) => {
|
|
api.changePassword({ id: row.id, password: value }).then(() => {
|
|
ElMessage.success($t('page.ui.passwordChanged'))
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* 清理缓存
|
|
* @param row
|
|
*/
|
|
const handleCache = (row: any) => {
|
|
ElMessageBox.confirm($t('page.ui.clearCacheConfirm'), $t('uiMsg.titlePrompt'), {
|
|
confirmButtonText: $t('uiMsg.btnOk'),
|
|
cancelButtonText: $t('uiMsg.btnCancel'),
|
|
type: 'warning'
|
|
}).then(() => {
|
|
api.clearCache({ id: row.id }).then(() => {
|
|
ElMessage.success($t('uiMsg.clearCacheSuccess'))
|
|
})
|
|
})
|
|
}
|
|
|
|
onMounted(() => {
|
|
if (isSuperAdminUser()) {
|
|
getDeptList()
|
|
} else {
|
|
applyDefaultChannelSelection()
|
|
}
|
|
})
|
|
</script>
|