893 lines
27 KiB
Vue
893 lines
27 KiB
Vue
<!-- 职位管理 -->
|
||
<!-- eslint-disable vue/no-v-model-argument -->
|
||
<template>
|
||
<div class="page-container">
|
||
<section class="panel">
|
||
<div class="panel-top">
|
||
<h2>职位列表</h2>
|
||
</div>
|
||
|
||
<div class="filter-block">
|
||
<el-form :model="searchForm" class="filter-form" @submit.prevent>
|
||
<div class="filter-grid">
|
||
<el-form-item label="关键词">
|
||
<el-input v-model="searchForm.keyword" placeholder="职位名称/编码" clearable />
|
||
</el-form-item>
|
||
</div>
|
||
<div class="filter-actions-row">
|
||
<span class="filter-hint">按职位名称、编码模糊查询。</span>
|
||
<div class="filter-actions-end">
|
||
<el-button class="btn-ghost" @click="handleReset"
|
||
><el-icon><RefreshLeft /></el-icon>重置</el-button
|
||
>
|
||
<el-button class="btn-main" type="primary" @click="handleSearch"
|
||
><el-icon><Search /></el-icon>搜索</el-button
|
||
>
|
||
</div>
|
||
</div>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="list-toolbar">
|
||
<div class="list-toolbar-row">
|
||
<div class="list-toolbar-selected">
|
||
已选 <span class="sel-num">{{ selectedRows.length }}</span> 项
|
||
</div>
|
||
<el-button class="btn-main" type="primary" @click="handleAdd">
|
||
<el-icon><Plus /></el-icon>
|
||
新增职位
|
||
</el-button>
|
||
<el-button
|
||
class="btn-ghost"
|
||
type="danger"
|
||
:disabled="selectedRows.length === 0"
|
||
@click="handleBatchDelete"
|
||
>
|
||
批量删除
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="list-body list-wrap">
|
||
<el-table
|
||
class="list-table"
|
||
:data="tableData"
|
||
v-loading="loading"
|
||
stripe
|
||
row-key="id"
|
||
@selection-change="handleSelectionChange"
|
||
@row-dblclick="handleView"
|
||
>
|
||
<el-table-column type="selection" min-width="55" align="center" header-align="center" />
|
||
<el-table-column
|
||
type="index"
|
||
label="序号"
|
||
min-width="68"
|
||
align="center"
|
||
header-align="center"
|
||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||
/>
|
||
<el-table-column
|
||
prop="postName"
|
||
label="职位名称"
|
||
min-width="140"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="postCode"
|
||
label="职位编码"
|
||
min-width="120"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
prop="status"
|
||
label="状态"
|
||
min-width="80"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<el-tag :type="dictStore.getTagType('sys_status', row.status)" size="small">
|
||
{{ dictStore.getLabel('sys_status', row.status) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column
|
||
prop="createTime"
|
||
label="创建时间"
|
||
min-width="180"
|
||
align="center"
|
||
header-align="center"
|
||
/>
|
||
<el-table-column
|
||
label="操作"
|
||
min-width="160"
|
||
fixed="right"
|
||
align="center"
|
||
header-align="center"
|
||
>
|
||
<template #default="{ row }">
|
||
<div style="display: flex; justify-content: center; gap: 8px">
|
||
<el-button class="btn-op-item primary" @click="handlePostBindUsers(row)">
|
||
<el-icon><User /></el-icon>
|
||
关联用户
|
||
</el-button>
|
||
<el-dropdown trigger="hover" @command="(cmd: string) => handleDropdownCommand(cmd, row)">
|
||
<el-button class="btn-op-item">
|
||
<el-icon><More /></el-icon>
|
||
</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item command="view">
|
||
<el-icon><View /></el-icon>查看
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="edit">
|
||
<el-icon><Edit /></el-icon>编辑
|
||
</el-dropdown-item>
|
||
<el-dropdown-item command="delete" divided>
|
||
<el-icon><Delete /></el-icon>删除
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</div>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<div class="pagination list-pager-bar">
|
||
<el-pagination
|
||
:current-page="pagination.page"
|
||
:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
:page-sizes="[10, 20, 50]"
|
||
layout="total, sizes, prev, pager, next"
|
||
@size-change="(val) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||
@current-change="(val) => ((pagination.page = val), loadData())"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<el-dialog
|
||
v-model="dialogVisible"
|
||
:title="dialogTitle"
|
||
width="560px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="formData"
|
||
:rules="formRules"
|
||
label-position="top"
|
||
class="post-dialog-form"
|
||
scroll-to-error
|
||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||
>
|
||
<div class="form-grid-2">
|
||
<el-form-item label="职位名称" prop="postName">
|
||
<el-input v-model="formData.postName" placeholder="请输入职位名称" />
|
||
</el-form-item>
|
||
<el-form-item label="职位编码" prop="postCode">
|
||
<el-input v-model="formData.postCode" placeholder="请输入职位编码" />
|
||
</el-form-item>
|
||
<el-form-item label="状态">
|
||
<el-select v-model="formData.status" placeholder="请选择状态">
|
||
<el-option
|
||
v-for="opt in statusOptions"
|
||
:key="opt.value"
|
||
:label="opt.label"
|
||
:value="opt.value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="备注" style="grid-column: 1 / -1">
|
||
<el-input v-model="formData.remark" type="textarea" :rows="2" placeholder="可选" />
|
||
</el-form-item>
|
||
</div>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="dialogVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button class="btn-main" type="primary" @click="handleSubmit" :loading="submitLoading">
|
||
<el-icon><Check /></el-icon>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<!-- 关联用户(布局对齐角色管理「数据权限」自定义范围) -->
|
||
<el-dialog
|
||
v-model="postBindDialogVisible"
|
||
title="关联用户"
|
||
width="1120px"
|
||
class="dialog-form-layout"
|
||
align-center
|
||
destroy-on-close
|
||
:close-on-click-modal="false"
|
||
>
|
||
<DialogFormBody :loading="postBindLoading" body-class="post-bind-dialog-body">
|
||
<p v-if="postBindPostName" class="post-bind-subtitle">
|
||
职位:<strong>{{ postBindPostName }}</strong>
|
||
</p>
|
||
<el-form label-position="top" class="data-scope-form">
|
||
<el-form-item label="绑定用户" class="data-scope-custom-form-item">
|
||
<div class="data-scope-custom-grid">
|
||
<div class="data-scope-custom-col">
|
||
<div>
|
||
<div class="data-scope-col-title">选择部门</div>
|
||
<div class="data-scope-col-tip">点击部门名称可按该部门及子部门筛选右侧用户</div>
|
||
</div>
|
||
<div class="data-scope-tree-wrap">
|
||
<el-tree
|
||
ref="postBindDeptTreeRef"
|
||
:data="postBindDeptTreeData"
|
||
highlight-current
|
||
node-key="id"
|
||
:props="{ label: 'deptName', children: 'children' }"
|
||
default-expand-all
|
||
:expand-on-click-node="false"
|
||
@node-click="handlePostBindDeptNodeClick"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div class="data-scope-custom-col">
|
||
<div class="data-scope-col-head-row">
|
||
<span class="data-scope-col-title">选择用户</span>
|
||
<el-button
|
||
v-if="postBindUserFilterDeptId"
|
||
class="btn-ghost"
|
||
size="small"
|
||
@click="clearPostBindUserDeptFilter"
|
||
>
|
||
清除部门筛选
|
||
</el-button>
|
||
</div>
|
||
<el-input
|
||
v-model="postBindUserKeyword"
|
||
class="data-scope-user-filter"
|
||
clearable
|
||
placeholder="按姓名、账号过滤"
|
||
/>
|
||
<div class="data-scope-user-list-wrap">
|
||
<el-checkbox-group
|
||
v-if="postBindFilteredUsers.length > 0"
|
||
v-model="postBindUserCheckedKeys"
|
||
>
|
||
<div
|
||
v-for="u in postBindFilteredUsers"
|
||
:key="u.id"
|
||
class="data-scope-user-row"
|
||
>
|
||
<el-checkbox :value="u.id">{{ postBindUserOptionLabel(u) }}</el-checkbox>
|
||
</div>
|
||
</el-checkbox-group>
|
||
<el-empty v-else :description="postBindUserEmptyDesc" :image-size="40" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="data-scope-selection-summary">
|
||
<template v-if="postBindSelectedUserSummaries.length > 0">
|
||
<div class="data-scope-summary-block">
|
||
<div class="data-scope-summary-heading">已选用户</div>
|
||
<ul class="data-scope-summary-list">
|
||
<li v-for="row in postBindSelectedUserSummaries" :key="'u-' + row.id">
|
||
{{ row.label }}
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
<p v-else class="data-scope-summary-empty">暂未勾选用户</p>
|
||
</div>
|
||
</el-form-item>
|
||
</el-form>
|
||
</DialogFormBody>
|
||
<template #footer>
|
||
<el-button class="btn-ghost" @click="postBindDialogVisible = false">
|
||
<el-icon><Close /></el-icon>
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
class="btn-main"
|
||
type="primary"
|
||
@click="handlePostBindSubmit"
|
||
:loading="postBindSubmitLoading"
|
||
>
|
||
<el-icon><Check /></el-icon>
|
||
确定
|
||
</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
|
||
<SystemViewDialog
|
||
v-model:visible="viewVisible"
|
||
:title="viewRow?.postName || '职位详情'"
|
||
:icon="Briefcase"
|
||
:meta="`职位编码:${viewRow?.postCode || ''}${viewRow?.createTime ? ' · ' + viewRow.createTime : ''}`"
|
||
:sections="viewSections"
|
||
/>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import {
|
||
Plus,
|
||
Close,
|
||
Check,
|
||
User,
|
||
View,
|
||
Edit,
|
||
Delete,
|
||
More,
|
||
Briefcase,
|
||
RefreshLeft,
|
||
Search
|
||
} from '@element-plus/icons-vue'
|
||
import DialogFormBody from '@/components/business/dialog-form-body/index.vue'
|
||
import SystemViewDialog from '@/components/business/system-view-dialog/index.vue'
|
||
import { MAX_ADMIN_LIST_PAGE_SIZE } from '@/utils/fetch-paged-list'
|
||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||
import { useDictStore } from '@/store/modules/dict'
|
||
import {
|
||
fetchPostList,
|
||
fetchCreatePost,
|
||
fetchUpdatePost,
|
||
fetchDeletePost,
|
||
fetchDictDataOptions,
|
||
fetchDeptTree,
|
||
fetchUserList,
|
||
fetchPostBoundUserIds,
|
||
fetchUpdatePostBoundUsers
|
||
} from '@/api/admin'
|
||
|
||
defineOptions({ name: 'SystemPost' })
|
||
|
||
const dictStore = useDictStore()
|
||
|
||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||
const tableData = ref<Api.Post.PostItem[]>([])
|
||
const selectedRows = ref<Api.Post.PostItem[]>([])
|
||
const loading = ref(false)
|
||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||
const dialogVisible = ref(false)
|
||
const dialogTitle = ref('新增职位')
|
||
const isEdit = ref(false)
|
||
const submitLoading = ref(false)
|
||
const formRef = ref()
|
||
const editingId = ref<string | null>(null)
|
||
|
||
const formData = reactive({
|
||
postName: '',
|
||
postCode: '',
|
||
status: '1',
|
||
orderNum: 0,
|
||
remark: ''
|
||
})
|
||
|
||
const formRules = {
|
||
postName: [{ required: true, message: '请输入职位名称', trigger: 'blur' }],
|
||
postCode: [{ required: true, message: '请输入职位编码', trigger: 'blur' }]
|
||
}
|
||
|
||
const statusOptions = ref<Api.Dict.DictDataOption[]>([])
|
||
|
||
// ---- 查看弹窗 ----
|
||
const viewVisible = ref(false)
|
||
const viewRow = ref<Api.Post.PostItem | null>(null)
|
||
const viewBoundUserNames = ref<string[]>([])
|
||
const viewSections = computed(() => {
|
||
const row = viewRow.value
|
||
if (!row) return []
|
||
const sections: any[] = [
|
||
{
|
||
title: '基本信息',
|
||
fieldLayout: 'table' as const,
|
||
tableMode: 'triple' as const,
|
||
fields: [
|
||
{ label: '职位名称', value: row.postName, long: false },
|
||
{ label: '职位编码', value: row.postCode, long: false },
|
||
{
|
||
label: '状态',
|
||
value: dictStore.getLabel('sys_status', row.status),
|
||
tagType: dictStore.getTagType('sys_status', row.status),
|
||
long: false
|
||
},
|
||
{ label: '创建时间', value: row.createTime, long: false },
|
||
{ label: '备注', value: row.remark, long: false }
|
||
]
|
||
}
|
||
]
|
||
if (viewBoundUserNames.value.length > 0) {
|
||
sections.push({
|
||
title: '关联用户',
|
||
fields: [{ label: '已关联用户', value: viewBoundUserNames.value.join('、'), span: true }]
|
||
})
|
||
}
|
||
return sections
|
||
})
|
||
async function handleView(row: Api.Post.PostItem) {
|
||
viewRow.value = row
|
||
viewBoundUserNames.value = []
|
||
viewVisible.value = true
|
||
try {
|
||
const [boundRes] = await Promise.all([fetchPostBoundUserIds(row.id), fetchDeptTree()])
|
||
const boundIds = boundRes?.userIds || []
|
||
if (boundIds.length === 0) return
|
||
const users = await fetchAllUsersForPostBind()
|
||
const idToName = new Map(users.map((u) => [u.id, u.nickName || u.userNo || u.id]))
|
||
viewBoundUserNames.value = boundIds.map((id: string) => idToName.get(id) || id)
|
||
} catch (e) {
|
||
console.error('加载关联用户失败:', e)
|
||
}
|
||
}
|
||
|
||
// ---- 关联用户弹窗(布局对齐角色管理「数据权限」自定义范围)----
|
||
const postBindDialogVisible = ref(false)
|
||
const postBindLoading = ref(false)
|
||
const postBindSubmitLoading = ref(false)
|
||
const postBindPostId = ref('')
|
||
const postBindPostName = ref('')
|
||
const postBindDeptTreeRef = ref()
|
||
const postBindDeptTreeData = ref<Api.Dept.DeptItem[]>([])
|
||
const postBindAllUsers = ref<
|
||
Array<{ id: string; nickName: string; userNo: string; deptId: string; deptName?: string }>
|
||
>([])
|
||
const postBindUserCheckedKeys = ref<string[]>([])
|
||
const postBindUserFilterDeptId = ref<string | null>(null)
|
||
const postBindUserKeyword = ref('')
|
||
|
||
function findPostBindDeptNodeById(
|
||
nodes: Api.Dept.DeptItem[],
|
||
id: string
|
||
): Api.Dept.DeptItem | null {
|
||
for (const n of nodes) {
|
||
if (n.id === id) return n
|
||
if (n.children?.length) {
|
||
const found = findPostBindDeptNodeById(n.children, id)
|
||
if (found) return found
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
function collectPostBindDeptSubtreeIds(node: Api.Dept.DeptItem): string[] {
|
||
const ids = [node.id]
|
||
for (const c of node.children || []) ids.push(...collectPostBindDeptSubtreeIds(c))
|
||
return ids
|
||
}
|
||
|
||
function collectAllPostBindDeptIds(nodes: Api.Dept.DeptItem[]): string[] {
|
||
const ids: string[] = []
|
||
const walk = (list: Api.Dept.DeptItem[]) => {
|
||
for (const n of list) {
|
||
ids.push(n.id)
|
||
if (n.children?.length) walk(n.children)
|
||
}
|
||
}
|
||
walk(nodes)
|
||
return ids
|
||
}
|
||
|
||
async function expandPostBindDeptTree() {
|
||
await nextTick()
|
||
const tree = postBindDeptTreeRef.value
|
||
const keys = collectAllPostBindDeptIds(postBindDeptTreeData.value)
|
||
if (tree && keys.length && typeof tree.setExpandedKeys === 'function') {
|
||
tree.setExpandedKeys(keys)
|
||
}
|
||
}
|
||
|
||
const postBindDeptFilterIdSet = computed(() => {
|
||
const id = postBindUserFilterDeptId.value
|
||
if (!id) return null
|
||
const node = findPostBindDeptNodeById(postBindDeptTreeData.value, id)
|
||
if (!node) return new Set<string>([id])
|
||
return new Set(collectPostBindDeptSubtreeIds(node))
|
||
})
|
||
|
||
const postBindFilteredUsers = computed(() => {
|
||
const kw = postBindUserKeyword.value.trim().toLowerCase()
|
||
const deptSet = postBindDeptFilterIdSet.value
|
||
return postBindAllUsers.value.filter((u) => {
|
||
if (deptSet && !deptSet.has(u.deptId)) return false
|
||
if (!kw) return true
|
||
const name = (u.nickName || '').toLowerCase()
|
||
const no = (u.userNo || '').toLowerCase()
|
||
return name.includes(kw) || no.includes(kw)
|
||
})
|
||
})
|
||
|
||
const postBindUserEmptyDesc = computed(() => {
|
||
if (postBindAllUsers.value.length === 0) return '暂无用户数据'
|
||
const hasDept = Boolean(postBindUserFilterDeptId.value)
|
||
const kw = postBindUserKeyword.value.trim()
|
||
if (hasDept && kw) return '该部门下无匹配用户'
|
||
if (hasDept) return '该部门下暂无用户'
|
||
if (kw) return '无匹配用户'
|
||
return '无匹配用户'
|
||
})
|
||
|
||
const postBindSelectedUserSummaries = computed(() =>
|
||
postBindUserCheckedKeys.value.map((id) => {
|
||
const u = postBindAllUsers.value.find((x) => x.id === id)
|
||
return { id, label: u ? postBindUserOptionLabel(u) : id }
|
||
})
|
||
)
|
||
|
||
function postBindUserOptionLabel(u: {
|
||
id: string
|
||
nickName: string
|
||
userNo: string
|
||
deptName?: string
|
||
}) {
|
||
const base = u.nickName || u.userNo || u.id
|
||
if (u.deptName) return `${base}(${u.deptName})`
|
||
return base
|
||
}
|
||
|
||
function handlePostBindDeptNodeClick(data: Api.Dept.DeptItem) {
|
||
postBindUserFilterDeptId.value = data.id
|
||
postBindDeptTreeRef.value?.setCurrentKey?.(data.id)
|
||
}
|
||
|
||
function clearPostBindUserDeptFilter() {
|
||
postBindUserFilterDeptId.value = null
|
||
postBindDeptTreeRef.value?.setCurrentKey?.(null)
|
||
}
|
||
|
||
async function fetchAllUsersForPostBind(): Promise<
|
||
Array<{ id: string; nickName: string; userNo: string; deptId: string; deptName?: string }>
|
||
> {
|
||
const pageSize = MAX_ADMIN_LIST_PAGE_SIZE
|
||
const raw: any[] = []
|
||
let page = 1
|
||
for (;;) {
|
||
const res = await fetchUserList({ page, pageSize })
|
||
const list = res?.list || []
|
||
raw.push(...list)
|
||
if (list.length < pageSize) break
|
||
if (typeof res?.total === 'number' && raw.length >= res.total) break
|
||
page += 1
|
||
}
|
||
return raw.map((u: any) => ({
|
||
id: u.id,
|
||
deptId: u.deptId || '',
|
||
nickName: u.nickName || u.realName || '',
|
||
userNo: u.userNo || '',
|
||
deptName: u.deptName
|
||
}))
|
||
}
|
||
|
||
async function handlePostBindUsers(row: Api.Post.PostItem) {
|
||
postBindPostId.value = row.id
|
||
postBindPostName.value = row.postName || row.postCode || ''
|
||
postBindUserCheckedKeys.value = []
|
||
postBindUserFilterDeptId.value = null
|
||
postBindUserKeyword.value = ''
|
||
postBindDialogVisible.value = true
|
||
postBindLoading.value = true
|
||
try {
|
||
const [boundRes, deptTree, users] = await Promise.all([
|
||
fetchPostBoundUserIds(row.id),
|
||
fetchDeptTree(),
|
||
fetchAllUsersForPostBind()
|
||
])
|
||
postBindUserCheckedKeys.value = [...(boundRes?.userIds || [])]
|
||
postBindDeptTreeData.value = deptTree || []
|
||
postBindAllUsers.value = users
|
||
await expandPostBindDeptTree()
|
||
} finally {
|
||
postBindLoading.value = false
|
||
}
|
||
}
|
||
|
||
async function handlePostBindSubmit() {
|
||
postBindSubmitLoading.value = true
|
||
try {
|
||
await fetchUpdatePostBoundUsers(postBindPostId.value, [...postBindUserCheckedKeys.value])
|
||
postBindDialogVisible.value = false
|
||
} finally {
|
||
postBindSubmitLoading.value = false
|
||
}
|
||
}
|
||
|
||
const loadDictOptions = async () => {
|
||
try {
|
||
const bmzt = await fetchDictDataOptions('bmzt')
|
||
statusOptions.value = bmzt || []
|
||
} catch (error) {
|
||
console.error('加载字典选项失败:', error)
|
||
}
|
||
}
|
||
|
||
const handleSelectionChange = (rows: Api.Post.PostItem[]) => {
|
||
selectedRows.value = rows
|
||
}
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const res = await fetchPostList({
|
||
page: pagination.page,
|
||
pageSize: pagination.pageSize,
|
||
keyword: searchForm.keyword || undefined
|
||
})
|
||
tableData.value = res.list || []
|
||
pagination.total = res.total ?? 0
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleSearch() {
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
function handleReset() {
|
||
searchForm.keyword = ''
|
||
pagination.page = 1
|
||
loadData()
|
||
}
|
||
|
||
function resetForm() {
|
||
formData.postName = ''
|
||
formData.postCode = ''
|
||
formData.status = '1'
|
||
formData.orderNum = 0
|
||
formData.remark = ''
|
||
editingId.value = null
|
||
}
|
||
|
||
function handleAdd() {
|
||
isEdit.value = false
|
||
dialogTitle.value = '新增职位'
|
||
resetForm()
|
||
dialogVisible.value = true
|
||
formRef.value?.clearValidate?.()
|
||
}
|
||
|
||
function handleEdit(row: Api.Post.PostItem) {
|
||
isEdit.value = true
|
||
dialogTitle.value = '编辑职位'
|
||
editingId.value = row.id
|
||
formData.postName = row.postName || ''
|
||
formData.postCode = row.postCode || ''
|
||
formData.status = row.status || '1'
|
||
formData.orderNum = row.orderNum ?? 0
|
||
formData.remark = row.remark || ''
|
||
dialogVisible.value = true
|
||
formRef.value?.clearValidate?.()
|
||
}
|
||
|
||
async function handleSubmit() {
|
||
await formRef.value?.validate?.()
|
||
submitLoading.value = true
|
||
try {
|
||
const payload = {
|
||
postName: formData.postName.trim(),
|
||
postCode: formData.postCode.trim(),
|
||
status: formData.status,
|
||
orderNum: formData.orderNum,
|
||
remark: formData.remark?.trim() || undefined
|
||
}
|
||
if (isEdit.value && editingId.value) {
|
||
await fetchUpdatePost(editingId.value, payload)
|
||
} else {
|
||
await fetchCreatePost(payload)
|
||
}
|
||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||
dialogVisible.value = false
|
||
await loadData()
|
||
} catch {
|
||
/* 校验或接口错误已由全局处理 */
|
||
} finally {
|
||
submitLoading.value = false
|
||
}
|
||
}
|
||
|
||
function handleDropdownCommand(command: string, row: any) {
|
||
switch (command) {
|
||
case 'view':
|
||
handleView(row)
|
||
break
|
||
case 'edit':
|
||
handleEdit(row)
|
||
break
|
||
case 'delete':
|
||
handleDelete(row)
|
||
break
|
||
}
|
||
}
|
||
|
||
async function handleDelete(row: Api.Post.PostItem) {
|
||
await ElMessageBox.confirm(`确定删除职位「${row.postName}」吗?`, '提示', { type: 'warning' })
|
||
await fetchDeletePost(row.id)
|
||
ElMessage.success('删除成功')
|
||
await loadData()
|
||
}
|
||
|
||
async function handleBatchDelete() {
|
||
const ids = selectedRows.value.map((r) => r.id)
|
||
if (!ids.length) return
|
||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个职位吗?`, '提示', {
|
||
type: 'warning'
|
||
})
|
||
await withFullscreenLoading(async () => {
|
||
for (const id of ids) {
|
||
await fetchDeletePost(id)
|
||
}
|
||
ElMessage.success('批量删除成功')
|
||
selectedRows.value = []
|
||
await loadData()
|
||
}, fullscreenLoadingText.batchDelete)
|
||
}
|
||
// 输入检索条件时自动搜索(防抖 300ms)
|
||
watchDebounced(
|
||
searchForm,
|
||
() => {
|
||
pagination.page = 1
|
||
loadData()
|
||
},
|
||
{ debounce: 300, deep: true }
|
||
)
|
||
|
||
onMounted(() => {
|
||
loadData()
|
||
loadDictOptions()
|
||
})
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.post-dialog-form {
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.post-bind-dialog-body {
|
||
min-height: 200px;
|
||
}
|
||
|
||
.post-bind-subtitle {
|
||
margin: 0 0 12px;
|
||
font-size: 13px;
|
||
line-height: 1.5;
|
||
color: var(--el-text-color-regular);
|
||
}
|
||
|
||
.data-scope-form {
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.data-scope-custom-form-item {
|
||
margin-top: 0;
|
||
|
||
:deep(.el-form-item__content) {
|
||
display: block;
|
||
}
|
||
}
|
||
|
||
.data-scope-custom-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 16px;
|
||
width: 100%;
|
||
}
|
||
|
||
.data-scope-custom-col {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
min-width: 0;
|
||
}
|
||
|
||
.data-scope-col-title {
|
||
padding-left: 10px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-regular);
|
||
border-left: 3px solid #10b981;
|
||
}
|
||
|
||
.data-scope-col-tip {
|
||
margin-top: 4px;
|
||
font-size: 11px;
|
||
line-height: 1.45;
|
||
color: var(--mint-text-muted, var(--el-text-color-secondary));
|
||
}
|
||
|
||
.data-scope-col-head-row {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 8px;
|
||
}
|
||
|
||
.data-scope-tree-wrap {
|
||
max-height: 280px;
|
||
padding: 8px;
|
||
overflow: auto;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.data-scope-user-filter {
|
||
width: 100%;
|
||
}
|
||
|
||
.data-scope-user-list-wrap {
|
||
max-height: 236px;
|
||
padding: 8px;
|
||
overflow: auto;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.data-scope-user-row {
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.data-scope-selection-summary {
|
||
margin-top: 12px;
|
||
padding: 10px 12px;
|
||
border: 1px solid var(--el-border-color-lighter);
|
||
border-radius: 8px;
|
||
background: var(--el-fill-color-blank);
|
||
}
|
||
|
||
.data-scope-summary-block + .data-scope-summary-block {
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
border-top: 1px solid var(--el-border-color-lighter);
|
||
}
|
||
|
||
.data-scope-summary-heading {
|
||
margin-bottom: 6px;
|
||
padding-left: 10px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--el-text-color-regular);
|
||
border-left: 3px solid #10b981;
|
||
}
|
||
|
||
.data-scope-summary-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||
gap: 8px 12px;
|
||
margin: 0;
|
||
padding: 0;
|
||
list-style: none;
|
||
font-size: 12px;
|
||
line-height: 1.45;
|
||
color: var(--el-text-color-primary);
|
||
}
|
||
|
||
.data-scope-summary-list li {
|
||
min-width: 0;
|
||
padding: 6px 8px;
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
border-radius: 6px;
|
||
background: var(--el-fill-color-light);
|
||
}
|
||
|
||
.data-scope-summary-empty {
|
||
margin: 0;
|
||
font-size: 12px;
|
||
line-height: 1.5;
|
||
color: var(--mint-text-muted, var(--el-text-color-secondary));
|
||
}
|
||
</style>
|