docs: add cell storage protocol modules design for 2.2.6–2.2.10
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
058811ae77
commit
246ad2228e
153 files changed
+20578
-2040
No files matched your search
@@ -5,20 +5,20 @@
|
||||
<div class="login-left">
|
||||
<div class="brand-content">
|
||||
<div class="logo">
|
||||
<img class="logo-img" src="/logo1.jpg" alt="陇耘鲜达" />
|
||||
<img class="logo-img" src="/logo.png" alt="中科兴皖" />
|
||||
</div>
|
||||
<h1 class="slogan">专业生鲜配送管理平台</h1>
|
||||
<h1 class="slogan">一体化管理门户</h1>
|
||||
<p class="description">
|
||||
高效 · 智能 · 可信赖
|
||||
安全 · 高效 · 可扩展
|
||||
</p>
|
||||
<div class="features">
|
||||
<div class="feature-item">
|
||||
<i class="el-icon-present"></i>
|
||||
<span>订单管理</span>
|
||||
<i class="el-icon-setting"></i>
|
||||
<span>系统管理</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="el-icon-truck"></i>
|
||||
<span>配送调度</span>
|
||||
<i class="el-icon-user"></i>
|
||||
<span>权限配置</span>
|
||||
</div>
|
||||
<div class="feature-item">
|
||||
<i class="el-icon-data-analysis"></i>
|
||||
@@ -78,7 +78,7 @@
|
||||
</el-form>
|
||||
|
||||
<div class="form-footer">
|
||||
<span class="copyright">陇耘鲜达 © {{ currentYear }}</span>
|
||||
<span class="copyright">中科兴皖 © {{ currentYear }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,7 +166,7 @@
|
||||
|
||||
.login-left {
|
||||
flex: 1;
|
||||
background: linear-gradient(135deg, #1989fa 0%, #07c160 100%);
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -195,6 +195,8 @@
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
padding: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
@@ -288,7 +290,7 @@
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(135deg, #1989fa 0%, #07c160 100%);
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
<!-- 组织管理 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>组织管理</h1>
|
||||
<p class="page-desc">管理公司组织架构,支持多级组织。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">组织管理</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</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" @click="toggleExpand">
|
||||
{{ isExpandAll ? '全部折叠' : '全部展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="isExpandAll"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="55"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="deptName"
|
||||
label="组织名称"
|
||||
min-width="200"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="deptCategoryCode"
|
||||
label="组织类别"
|
||||
width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ getDeptCategoryLabel(row.deptCategoryCode) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="leader"
|
||||
label="负责人"
|
||||
width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="leaderPhone"
|
||||
label="联系电话"
|
||||
width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderNum"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="320"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleAddChild(row)">
|
||||
新增子组织
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="dept-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="组织名称" prop="deptName" style="grid-column: 1 / -1">
|
||||
<el-input v-model="formData.deptName" placeholder="请输入组织名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组织类别">
|
||||
<el-select
|
||||
v-model="formData.deptCategoryCode"
|
||||
placeholder="请选择组织类别"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in deptCategoryOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</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="负责人">
|
||||
<el-input v-model="formData.leader" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="formData.leaderPhone" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchDeptTree,
|
||||
fetchCreateDept,
|
||||
fetchUpdateDept,
|
||||
fetchDeleteDept,
|
||||
fetchDictDataOptions
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemDept' })
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.Dept.DeptItem[]>([])
|
||||
const selectedRows = ref<Api.Dept.DeptItem[]>([])
|
||||
const loading = ref(false)
|
||||
const isExpandAll = ref(true)
|
||||
const tableRef = ref()
|
||||
|
||||
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({
|
||||
parentId: '',
|
||||
deptName: '',
|
||||
deptCategoryCode: '',
|
||||
status: '1',
|
||||
leader: '',
|
||||
leaderPhone: '',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
deptName: [{ required: true, message: '请输入组织名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const deptCategoryOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
const statusOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
|
||||
const loadDictOptions = async () => {
|
||||
try {
|
||||
const [zzlbbm, bmzt] = await Promise.all([
|
||||
fetchDictDataOptions('zzlbbm'),
|
||||
fetchDictDataOptions('bmzt')
|
||||
])
|
||||
deptCategoryOptions.value = zzlbbm || []
|
||||
statusOptions.value = bmzt || []
|
||||
} catch (error) {
|
||||
console.error('加载字典选项失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getDeptCategoryLabel = (code: string) => {
|
||||
if (!code) return '-'
|
||||
const opt = deptCategoryOptions.value.find((o) => o.value === code)
|
||||
return opt ? opt.label : code
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.Dept.DeptItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchDeptTree({
|
||||
keyword: searchForm.keyword || undefined
|
||||
})
|
||||
tableData.value = res || []
|
||||
} catch (error) {
|
||||
console.error('加载组织数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
loadData()
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
tableData.value = [...tableData.value]
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.parentId = ''
|
||||
formData.deptName = ''
|
||||
formData.deptCategoryCode = ''
|
||||
formData.status = '1'
|
||||
formData.leader = ''
|
||||
formData.leaderPhone = ''
|
||||
formData.orderNum = 0
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增组织'
|
||||
resetForm()
|
||||
formData.parentId = '0'
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleAddChild(row: Api.Dept.DeptItem) {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增子组织'
|
||||
resetForm()
|
||||
formData.parentId = row.id
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleEdit(row: Api.Dept.DeptItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑组织'
|
||||
editingId.value = row.id
|
||||
formData.parentId = row.parentId || ''
|
||||
formData.deptName = row.deptName || ''
|
||||
formData.deptCategoryCode = row.deptCategoryCode || ''
|
||||
formData.status = row.status || '1'
|
||||
formData.leader = row.leader || ''
|
||||
formData.leaderPhone = row.leaderPhone || ''
|
||||
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 = {
|
||||
parentId: formData.parentId || undefined,
|
||||
deptName: formData.deptName.trim(),
|
||||
deptCategoryCode: formData.deptCategoryCode || undefined,
|
||||
status: formData.status,
|
||||
leader: formData.leader?.trim() || undefined,
|
||||
leaderPhone: formData.leaderPhone?.trim() || undefined,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateDept(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateDept(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Dept.DeptItem) {
|
||||
try {
|
||||
const childCount = row.children?.length || 0
|
||||
const confirmText = childCount
|
||||
? `该组织下有 ${childCount} 个子组织,删除将一并删除全部子组织。确定继续吗?`
|
||||
: '确定要删除该组织吗?'
|
||||
|
||||
await ElMessageBox.confirm(confirmText, '提示', { type: 'warning' })
|
||||
await fetchDeleteDept(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
resetForm()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dept-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,657 @@
|
||||
<!-- 字典管理 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>字典管理</h1>
|
||||
<p class="page-desc">管理系统中所有字典类型及字典数据</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">字典类型</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">
|
||||
搜索
|
||||
</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"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="55"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="dictName"
|
||||
label="字典名称"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="dictType"
|
||||
label="字典类型编码"
|
||||
min-width="150"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderNum"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="320"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
class="btn-op-item primary"
|
||||
@click="handleDictData(row)"
|
||||
>
|
||||
字典数据配置
|
||||
</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</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="dict-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="字典名称" prop="dictName">
|
||||
<el-input v-model="formData.dictName" placeholder="字典名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型编码" prop="dictType">
|
||||
<el-input
|
||||
v-model="formData.dictType"
|
||||
placeholder="字典类型编码"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
<el-dialog
|
||||
v-model="dictDataDialogVisible"
|
||||
:title="`${currentDictType.dictName} - 字典数据配置`"
|
||||
width="900px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div style="margin-bottom: 12px">
|
||||
<el-button class="btn-main" type="primary" @click="handleAddDictData">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增数据
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
:data="dictDataTableData"
|
||||
v-loading="dictDataLoading"
|
||||
stripe
|
||||
row-key="id"
|
||||
>
|
||||
<el-table-column type="index" label="#" width="50" align="center" />
|
||||
<el-table-column
|
||||
prop="dictLabel"
|
||||
label="数据标签"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="dictValue"
|
||||
label="数据值"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderNum"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="remark"
|
||||
label="备注"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="160"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
class="btn-op-item primary"
|
||||
@click="handleEditDictData(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDeleteDictData(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="pagination list-pager-bar" style="margin-top: 12px">
|
||||
<el-pagination
|
||||
:current-page="dictDataPagination.page"
|
||||
:page-size="dictDataPagination.pageSize"
|
||||
:total="dictDataPagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next"
|
||||
@size-change="
|
||||
(val) => (
|
||||
(dictDataPagination.page = 1),
|
||||
(dictDataPagination.pageSize = val),
|
||||
loadDictData()
|
||||
)
|
||||
"
|
||||
@current-change="
|
||||
(val) => ((dictDataPagination.page = val), loadDictData())
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dictDataFormVisible"
|
||||
:title="dictDataFormTitle"
|
||||
width="480px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
ref="dictDataFormRef"
|
||||
:model="dictDataFormData"
|
||||
:rules="dictDataFormRules"
|
||||
label-position="top"
|
||||
class="dict-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="数据标签" prop="dictLabel">
|
||||
<el-input v-model="dictDataFormData.dictLabel" placeholder="数据标签" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据值" prop="dictValue">
|
||||
<el-input v-model="dictDataFormData.dictValue" placeholder="数据值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number
|
||||
v-model="dictDataFormData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" style="grid-column: 1 / -1">
|
||||
<el-input
|
||||
v-model="dictDataFormData.remark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="可选"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dictDataFormVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button
|
||||
class="btn-main"
|
||||
type="primary"
|
||||
@click="handleSubmitDictData"
|
||||
:loading="dictDataSubmitLoading"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchDictTypeList,
|
||||
fetchCreateDictType,
|
||||
fetchUpdateDictType,
|
||||
fetchDeleteDictType,
|
||||
fetchDictDataList,
|
||||
fetchCreateDictData,
|
||||
fetchUpdateDictData,
|
||||
fetchDeleteDictData
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemDict' })
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.Dict.DictTypeItem[]>([])
|
||||
const selectedRows = ref<Api.Dict.DictTypeItem[]>([])
|
||||
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({
|
||||
dictName: '',
|
||||
dictType: '',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
dictName: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: '请输入字典类型编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.Dict.DictTypeItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchDictTypeList({
|
||||
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.dictName = ''
|
||||
formData.dictType = ''
|
||||
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.Dict.DictTypeItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑字典类型'
|
||||
editingId.value = row.id
|
||||
formData.dictName = row.dictName
|
||||
formData.dictType = row.dictType
|
||||
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 = {
|
||||
dictName: formData.dictName.trim(),
|
||||
dictType: formData.dictType.trim(),
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateDictType(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateDictType(payload)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Dict.DictTypeItem) {
|
||||
await ElMessageBox.confirm(`确定删除字典类型「${row.dictName}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
await fetchDeleteDictType(row.id)
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请选择要删除的字典类型')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个字典类型吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
for (const id of ids) {
|
||||
await fetchDeleteDictType(id)
|
||||
}
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
const dictDataDialogVisible = ref(false)
|
||||
const dictDataTableData = ref<Api.Dict.DictDataItem[]>([])
|
||||
const dictDataLoading = ref(false)
|
||||
const dictDataPagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const currentDictType = reactive({ dictName: '', dictType: '' })
|
||||
const dictDataFormVisible = ref(false)
|
||||
const dictDataFormTitle = ref('新增字典数据')
|
||||
const dictDataIsEdit = ref(false)
|
||||
const dictDataSubmitLoading = ref(false)
|
||||
const dictDataFormRef = ref()
|
||||
const dictDataEditingId = ref<string | null>(null)
|
||||
|
||||
const dictDataFormData = reactive({
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const dictDataFormRules = {
|
||||
dictLabel: [{ required: true, message: '请输入数据标签', trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: '请输入数据值', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
async function loadDictData() {
|
||||
dictDataLoading.value = true
|
||||
try {
|
||||
const res = await fetchDictDataList({
|
||||
page: dictDataPagination.page,
|
||||
pageSize: dictDataPagination.pageSize,
|
||||
dictType: currentDictType.dictType || undefined
|
||||
})
|
||||
dictDataTableData.value = res.list || []
|
||||
dictDataPagination.total = res.total ?? 0
|
||||
} finally {
|
||||
dictDataLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleDictData(row: Api.Dict.DictTypeItem) {
|
||||
currentDictType.dictName = row.dictName
|
||||
currentDictType.dictType = row.dictType
|
||||
dictDataPagination.page = 1
|
||||
dictDataDialogVisible.value = true
|
||||
loadDictData()
|
||||
}
|
||||
|
||||
function resetDictDataForm() {
|
||||
dictDataFormData.dictLabel = ''
|
||||
dictDataFormData.dictValue = ''
|
||||
dictDataFormData.orderNum = 0
|
||||
dictDataFormData.remark = ''
|
||||
dictDataEditingId.value = null
|
||||
}
|
||||
|
||||
function handleAddDictData() {
|
||||
dictDataIsEdit.value = false
|
||||
dictDataFormTitle.value = '新增字典数据'
|
||||
resetDictDataForm()
|
||||
dictDataFormVisible.value = true
|
||||
dictDataFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleEditDictData(row: Api.Dict.DictDataItem) {
|
||||
dictDataIsEdit.value = true
|
||||
dictDataFormTitle.value = '编辑字典数据'
|
||||
dictDataEditingId.value = row.id
|
||||
dictDataFormData.dictLabel = row.dictLabel
|
||||
dictDataFormData.dictValue = row.dictValue
|
||||
dictDataFormData.orderNum = row.orderNum ?? 0
|
||||
dictDataFormData.remark = row.remark || ''
|
||||
dictDataFormVisible.value = true
|
||||
dictDataFormRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
async function handleSubmitDictData() {
|
||||
await dictDataFormRef.value?.validate?.()
|
||||
dictDataSubmitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
dictLabel: dictDataFormData.dictLabel.trim(),
|
||||
dictValue: dictDataFormData.dictValue.trim(),
|
||||
dictType: currentDictType.dictType,
|
||||
orderNum: dictDataFormData.orderNum,
|
||||
remark: dictDataFormData.remark?.trim() || undefined
|
||||
}
|
||||
if (dictDataIsEdit.value && dictDataEditingId.value) {
|
||||
await fetchUpdateDictData(dictDataEditingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateDictData(payload)
|
||||
}
|
||||
dictDataFormVisible.value = false
|
||||
await loadDictData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
dictDataSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteDictData(row: Api.Dict.DictDataItem) {
|
||||
await ElMessageBox.confirm(
|
||||
`确定删除字典数据「${row.dictLabel}」吗?`,
|
||||
'提示',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
await fetchDeleteDictData(row.id)
|
||||
await loadDictData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dict-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,307 @@
|
||||
<!-- 快递公司管理 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>快递公司管理</h1>
|
||||
<p class="page-desc">维护快递公司编码与名称。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">快递公司列表</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</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"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="expressCode" label="快递编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="expressName" label="快递名称" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</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="express-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="快递编码" prop="expressCode">
|
||||
<el-input v-model="formData.expressCode" placeholder="请输入快递编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="快递名称" prop="expressName">
|
||||
<el-input v-model="formData.expressName" placeholder="请输入快递名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="formData.remark" 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>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchExpressCompanyList,
|
||||
fetchCreateExpressCompany,
|
||||
fetchUpdateExpressCompany,
|
||||
fetchDeleteExpressCompany
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemExpress' })
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
|
||||
const selectedRows = ref<Api.ExpressCompany.ExpressCompanyItem[]>([])
|
||||
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({
|
||||
expressCode: '',
|
||||
expressName: '',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
expressCode: [{ required: true, message: '请输入快递编码', trigger: 'blur' }],
|
||||
expressName: [{ required: true, message: '请输入快递名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.ExpressCompany.ExpressCompanyItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchExpressCompanyList({
|
||||
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.expressCode = ''
|
||||
formData.expressName = ''
|
||||
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.ExpressCompany.ExpressCompanyItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑快递公司'
|
||||
editingId.value = row.id
|
||||
formData.expressCode = row.expressCode || ''
|
||||
formData.expressName = row.expressName || ''
|
||||
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 = {
|
||||
expressCode: formData.expressCode.trim(),
|
||||
expressName: formData.expressName.trim(),
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateExpressCompany(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateExpressCompany(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.ExpressCompany.ExpressCompanyItem) {
|
||||
await ElMessageBox.confirm(`确定删除快递公司「${row.expressName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteExpressCompany(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' })
|
||||
for (const id of ids) {
|
||||
await fetchDeleteExpressCompany(id)
|
||||
}
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.express-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,494 @@
|
||||
<!-- 菜单管理 -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>菜单管理</h1>
|
||||
<p class="page-desc">管理系统菜单树,支持目录、菜单、按钮三种类型。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">菜单列表</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</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" @click="toggleExpand">
|
||||
{{ isExpandAll ? '全部折叠' : '全部展开' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="isExpandAll"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="55"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="menuName"
|
||||
label="菜单名称"
|
||||
min-width="180"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="menuType"
|
||||
label="类型"
|
||||
width="90"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="menuTypeTagMap[row.menuType]?.type || 'info'"
|
||||
size="small"
|
||||
>
|
||||
{{ menuTypeTagMap[row.menuType]?.label || row.menuType }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="path"
|
||||
label="路由路径"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.path || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="component"
|
||||
label="组件路径"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.component || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="perms"
|
||||
label="权限标识"
|
||||
min-width="140"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ row.perms || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="visible"
|
||||
label="可见"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.visible === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.visible === '1' ? '显示' : '隐藏' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
label="状态"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="orderNum"
|
||||
label="排序"
|
||||
width="70"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="320"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleAddChild(row)">
|
||||
新增子菜单
|
||||
</el-button>
|
||||
<el-button class="btn-op-item" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="560px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="menu-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item
|
||||
v-if="formData.parentId && formData.parentId !== '0'"
|
||||
label="上级菜单"
|
||||
style="grid-column: 1 / -1"
|
||||
>
|
||||
<el-input :model-value="parentMenuName" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单名称" prop="menuName" style="grid-column: 1 / -1">
|
||||
<el-input v-model="formData.menuName" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单类型" prop="menuType">
|
||||
<el-select v-model="formData.menuType" placeholder="请选择菜单类型">
|
||||
<el-option label="目录" value="M" />
|
||||
<el-option label="菜单" value="C" />
|
||||
<el-option label="按钮" value="F" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否外链">
|
||||
<el-select v-model="formData.isFrame" placeholder="请选择">
|
||||
<el-option label="否" :value="1" />
|
||||
<el-option label="是" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="路由路径">
|
||||
<el-input v-model="formData.path" placeholder="请输入路由路径" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.menuType === 'C'" label="组件路径">
|
||||
<el-input v-model="formData.component" placeholder="请输入组件路径" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.menuType === 'F'" label="权限标识">
|
||||
<el-input v-model="formData.perms" placeholder="如 system:menu:list" />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示状态">
|
||||
<el-select v-model="formData.visible" placeholder="请选择">
|
||||
<el-option label="显示" value="1" />
|
||||
<el-option label="隐藏" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="formData.status" placeholder="请选择状态">
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchMenuTree,
|
||||
fetchCreateMenu,
|
||||
fetchUpdateMenu,
|
||||
fetchDeleteMenu
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemMenu' })
|
||||
|
||||
const menuTypeTagMap: Record<string, { label: string; type: string }> = {
|
||||
M: { label: '目录', type: '' },
|
||||
C: { label: '菜单', type: 'success' },
|
||||
F: { label: '按钮', type: 'warning' }
|
||||
}
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.Menu.MenuItem[]>([])
|
||||
const selectedRows = ref<Api.Menu.MenuItem[]>([])
|
||||
const loading = ref(false)
|
||||
const isExpandAll = ref(true)
|
||||
const tableRef = ref()
|
||||
|
||||
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({
|
||||
parentId: '0',
|
||||
menuName: '',
|
||||
menuType: 'M',
|
||||
path: '',
|
||||
component: '',
|
||||
perms: '',
|
||||
isFrame: 1,
|
||||
visible: '1',
|
||||
status: '1',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
menuName: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
|
||||
menuType: [{ required: true, message: '请选择菜单类型', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const parentMenuName = computed(() => {
|
||||
if (!formData.parentId || formData.parentId === '0') return ''
|
||||
const findName = (items: Api.Menu.MenuItem[]): string => {
|
||||
for (const item of items) {
|
||||
if (item.id === formData.parentId) return item.menuName
|
||||
if (item.children?.length) {
|
||||
const found = findName(item.children)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return findName(tableData.value)
|
||||
})
|
||||
|
||||
const handleSelectionChange = (rows: Api.Menu.MenuItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchMenuTree({
|
||||
keyword: searchForm.keyword || undefined
|
||||
})
|
||||
tableData.value = Array.isArray(res) ? res : []
|
||||
} catch (error) {
|
||||
console.error('加载菜单数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
loadData()
|
||||
}
|
||||
|
||||
function toggleExpand() {
|
||||
isExpandAll.value = !isExpandAll.value
|
||||
tableData.value = [...tableData.value]
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.parentId = '0'
|
||||
formData.menuName = ''
|
||||
formData.menuType = 'M'
|
||||
formData.path = ''
|
||||
formData.component = ''
|
||||
formData.perms = ''
|
||||
formData.isFrame = 1
|
||||
formData.visible = '1'
|
||||
formData.status = '1'
|
||||
formData.orderNum = 0
|
||||
formData.remark = ''
|
||||
editingId.value = null
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增菜单'
|
||||
resetForm()
|
||||
formData.parentId = '0'
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleAddChild(row: Api.Menu.MenuItem) {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '新增子菜单'
|
||||
resetForm()
|
||||
formData.parentId = row.id
|
||||
dialogVisible.value = true
|
||||
formRef.value?.clearValidate?.()
|
||||
}
|
||||
|
||||
function handleEdit(row: Api.Menu.MenuItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑菜单'
|
||||
editingId.value = row.id
|
||||
formData.parentId = row.parentId || '0'
|
||||
formData.menuName = row.menuName || ''
|
||||
formData.menuType = row.menuType || 'M'
|
||||
formData.path = row.path || ''
|
||||
formData.component = row.component || ''
|
||||
formData.perms = row.perms || ''
|
||||
formData.isFrame = row.isFrame ?? 1
|
||||
formData.visible = row.visible || '1'
|
||||
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 = {
|
||||
parentId: formData.parentId || undefined,
|
||||
menuName: formData.menuName.trim(),
|
||||
menuType: formData.menuType,
|
||||
path: formData.path?.trim() || undefined,
|
||||
component: formData.menuType === 'C' ? formData.component?.trim() || undefined : undefined,
|
||||
perms: formData.menuType === 'F' ? formData.perms?.trim() || undefined : undefined,
|
||||
isFrame: formData.isFrame,
|
||||
visible: formData.visible,
|
||||
status: formData.status,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateMenu(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateMenu(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Menu.MenuItem) {
|
||||
try {
|
||||
const childCount = row.children?.length || 0
|
||||
const confirmText = childCount
|
||||
? `该菜单下有 ${childCount} 个子菜单,删除将一并删除全部子菜单。确定继续吗?`
|
||||
: '确定要删除该菜单吗?'
|
||||
|
||||
await ElMessageBox.confirm(confirmText, '提示', { type: 'warning' })
|
||||
await fetchDeleteMenu(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') console.error('删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDialogClose() {
|
||||
resetForm()
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.menu-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,341 @@
|
||||
<!-- 职位管理 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>职位管理</h1>
|
||||
<p class="page-desc">维护职位编码与名称。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">职位列表</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</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"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
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="row.status === '1' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</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="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchPostList,
|
||||
fetchCreatePost,
|
||||
fetchUpdatePost,
|
||||
fetchDeletePost,
|
||||
fetchDictDataOptions
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemPost' })
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
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' })
|
||||
for (const id of ids) {
|
||||
await fetchDeletePost(id)
|
||||
}
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.post-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,539 @@
|
||||
<!-- 角色管理(系统管理) -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>角色管理</h1>
|
||||
<p class="page-desc">管理运营端角色,配置菜单权限与数据权限范围。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">角色列表</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</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"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index: number) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="roleName" label="角色名称" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="dataScope" label="数据权限" min-width="120" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ dataScopeLabel(row.dataScope) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="400" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleMenuConfig(row)">菜单配置</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleDataScope(row)">数据权限</el-button>
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</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: number) => ((pagination.page = 1), (pagination.pageSize = val), loadData())"
|
||||
@current-change="(val: number) => ((pagination.page = val), loadData())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑角色弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
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="role-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="formData.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="formData.status" style="width: 100%">
|
||||
<el-option label="正常" value="1" />
|
||||
<el-option label="停用" value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number v-model="formData.orderNum" :min="0" :max="9999" controls-position="right" />
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</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="menuDialogVisible"
|
||||
title="菜单配置"
|
||||
width="700px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<div v-loading="menuDialogLoading" style="min-height: 200px">
|
||||
<el-tree
|
||||
ref="menuTreeRef"
|
||||
:data="menuTreeData"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:props="{ label: 'menuName', children: 'children' }"
|
||||
:default-checked-keys="menuCheckedKeys"
|
||||
check-strictly
|
||||
style="max-height: 500px; overflow-y: auto"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="menuDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleMenuSubmit" :loading="menuSubmitLoading">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 数据权限配置弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dataScopeDialogVisible"
|
||||
title="数据权限配置"
|
||||
width="700px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form label-position="top" class="data-scope-form">
|
||||
<el-form-item label="数据权限范围">
|
||||
<el-select v-model="dataScopeForm.dataScope" style="width: 100%">
|
||||
<el-option label="全部数据" value="1" />
|
||||
<el-option label="本部门数据" value="2" />
|
||||
<el-option label="本部门及以下数据" value="3" />
|
||||
<el-option label="仅本人数据" value="4" />
|
||||
<el-option label="自定义" value="5" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="dataScopeForm.dataScope === '5'">
|
||||
<el-form-item label="选择部门">
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
:data="deptTreeData"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:props="{ label: 'deptName', children: 'children' }"
|
||||
:default-checked-keys="scopeDeptCheckedKeys"
|
||||
check-strictly
|
||||
style="max-height: 260px; overflow-y: auto; border: 1px solid var(--el-border-color-lighter); border-radius: 4px; padding: 8px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择用户">
|
||||
<div style="border: 1px solid var(--el-border-color-lighter); border-radius: 4px; padding: 8px; max-height: 260px; overflow-y: auto; width: 100%">
|
||||
<el-checkbox-group v-model="scopeUserCheckedKeys">
|
||||
<div v-for="u in allUsers" :key="u.id" style="padding: 4px 0">
|
||||
<el-checkbox :value="u.id">{{ u.nickName || u.userNo }}</el-checkbox>
|
||||
</div>
|
||||
</el-checkbox-group>
|
||||
<el-empty v-if="allUsers.length === 0" description="暂无用户数据" :image-size="40" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button class="btn-ghost" @click="dataScopeDialogVisible = false">
|
||||
<el-icon><Close /></el-icon>
|
||||
取消
|
||||
</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleDataScopeSubmit" :loading="dataScopeSubmitLoading">
|
||||
<el-icon><Check /></el-icon>
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchRoleList,
|
||||
fetchCreateRole,
|
||||
fetchUpdateRole,
|
||||
fetchDeleteRole,
|
||||
fetchRoleMenuIds,
|
||||
fetchUpdateRoleMenus,
|
||||
fetchRoleDataScope,
|
||||
fetchUpdateRoleDataScope,
|
||||
fetchMenuTree,
|
||||
fetchDeptTree,
|
||||
fetchUserList
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemRole' })
|
||||
|
||||
const DATA_SCOPE_MAP: Record<string, string> = {
|
||||
'1': '全部数据',
|
||||
'2': '本部门数据',
|
||||
'3': '本部门及以下',
|
||||
'4': '仅本人',
|
||||
'5': '自定义'
|
||||
}
|
||||
|
||||
function dataScopeLabel(val: string) {
|
||||
return DATA_SCOPE_MAP[val] || '未知'
|
||||
}
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.Role.RoleItem[]>([])
|
||||
const selectedRows = ref<Api.Role.RoleItem[]>([])
|
||||
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({
|
||||
roleName: '',
|
||||
status: '1',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
roleName: [{ required: true, message: '请输入角色名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
// ---- 菜单配置弹窗 ----
|
||||
const menuDialogVisible = ref(false)
|
||||
const menuDialogLoading = ref(false)
|
||||
const menuSubmitLoading = ref(false)
|
||||
const menuTreeRef = ref()
|
||||
const menuTreeData = ref<Api.Menu.MenuItem[]>([])
|
||||
const menuCheckedKeys = ref<string[]>([])
|
||||
const menuEditingRoleId = ref<string>('')
|
||||
|
||||
// ---- 数据权限弹窗 ----
|
||||
const dataScopeDialogVisible = ref(false)
|
||||
const dataScopeSubmitLoading = ref(false)
|
||||
const dataScopeEditingRoleId = ref<string>('')
|
||||
const deptTreeRef = ref()
|
||||
const deptTreeData = ref<Api.Dept.DeptItem[]>([])
|
||||
const allUsers = ref<Array<{ id: string; nickName: string; userNo: string }>>([])
|
||||
const scopeDeptCheckedKeys = ref<string[]>([])
|
||||
const scopeUserCheckedKeys = ref<string[]>([])
|
||||
const dataScopeForm = reactive({
|
||||
dataScope: '1'
|
||||
})
|
||||
|
||||
const handleSelectionChange = (rows: Api.Role.RoleItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchRoleList({
|
||||
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.roleName = ''
|
||||
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.Role.RoleItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑角色'
|
||||
editingId.value = row.id
|
||||
formData.roleName = row.roleName
|
||||
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 = {
|
||||
roleName: formData.roleName.trim(),
|
||||
status: formData.status,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateRole(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateRole(payload)
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* handled globally */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Role.RoleItem) {
|
||||
await ElMessageBox.confirm(`确定删除角色「${row.roleName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteRole(row.id)
|
||||
await loadData()
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
if (!ids.length) {
|
||||
ElMessage.warning('请至少选择一项')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个角色吗?`, '提示', { type: 'warning' })
|
||||
for (const id of ids) {
|
||||
await fetchDeleteRole(id)
|
||||
}
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
// ---- 菜单配置 ----
|
||||
async function handleMenuConfig(row: Api.Role.RoleItem) {
|
||||
menuEditingRoleId.value = row.id
|
||||
menuDialogVisible.value = true
|
||||
menuDialogLoading.value = true
|
||||
try {
|
||||
const [tree, res] = await Promise.all([fetchMenuTree(), fetchRoleMenuIds(row.id)])
|
||||
menuTreeData.value = tree || []
|
||||
menuCheckedKeys.value = res?.menuIds || []
|
||||
} finally {
|
||||
menuDialogLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMenuSubmit() {
|
||||
menuSubmitLoading.value = true
|
||||
try {
|
||||
const tree = menuTreeRef.value
|
||||
const checkedKeys: string[] = tree?.getCheckedKeys() || []
|
||||
const halfCheckedKeys: string[] = tree?.getHalfCheckedKeys() || []
|
||||
const allKeys = [...checkedKeys, ...halfCheckedKeys]
|
||||
await fetchUpdateRoleMenus(menuEditingRoleId.value, allKeys)
|
||||
menuDialogVisible.value = false
|
||||
} finally {
|
||||
menuSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 数据权限配置 ----
|
||||
async function handleDataScope(row: Api.Role.RoleItem) {
|
||||
dataScopeEditingRoleId.value = row.id
|
||||
dataScopeForm.dataScope = row.dataScope || '1'
|
||||
scopeDeptCheckedKeys.value = []
|
||||
scopeUserCheckedKeys.value = []
|
||||
dataScopeDialogVisible.value = true
|
||||
|
||||
const [, deptTree, userRes] = await Promise.all([
|
||||
fetchRoleDataScope(row.id).then((res) => {
|
||||
if (res?.scopeDetails) {
|
||||
scopeDeptCheckedKeys.value = res.scopeDetails
|
||||
.filter((d) => d.scopeType === 'dept')
|
||||
.map((d) => d.scopeValue)
|
||||
scopeUserCheckedKeys.value = res.scopeDetails
|
||||
.filter((d) => d.scopeType === 'user')
|
||||
.map((d) => d.scopeValue)
|
||||
}
|
||||
dataScopeForm.dataScope = res?.dataScope || row.dataScope || '1'
|
||||
}),
|
||||
fetchDeptTree(),
|
||||
fetchUserList({ pageSize: 200 })
|
||||
])
|
||||
|
||||
deptTreeData.value = deptTree || []
|
||||
allUsers.value = (userRes?.list || []).map((u: any) => ({
|
||||
id: u.id,
|
||||
nickName: u.nickName || u.realName || '',
|
||||
userNo: u.userNo || ''
|
||||
}))
|
||||
}
|
||||
|
||||
async function handleDataScopeSubmit() {
|
||||
dataScopeSubmitLoading.value = true
|
||||
try {
|
||||
const details: Array<{ scopeType: string; scopeValue: string }> = []
|
||||
|
||||
if (dataScopeForm.dataScope === '5') {
|
||||
const deptKeys: string[] = deptTreeRef.value?.getCheckedKeys() || []
|
||||
deptKeys.forEach((id: string) => details.push({ scopeType: 'dept', scopeValue: id }))
|
||||
scopeUserCheckedKeys.value.forEach((id: string) =>
|
||||
details.push({ scopeType: 'user', scopeValue: id })
|
||||
)
|
||||
}
|
||||
|
||||
await fetchUpdateRoleDataScope(dataScopeEditingRoleId.value, {
|
||||
dataScope: dataScopeForm.dataScope,
|
||||
scopeDetails: details.length > 0 ? details : undefined
|
||||
})
|
||||
dataScopeDialogVisible.value = false
|
||||
await loadData()
|
||||
} finally {
|
||||
dataScopeSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.role-dialog-form,
|
||||
.data-scope-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,417 @@
|
||||
<!-- 站点管理 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>站点管理</h1>
|
||||
<p class="page-desc">维护站点信息、联系人与经营范围。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<div class="panel-top">
|
||||
<h2 class="title-bar-heading">站点列表</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</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"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="siteName" label="站点名称" min-width="140" align="center" header-align="center" />
|
||||
<el-table-column prop="siteCode" label="站点编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="siteType" label="站点类型" min-width="100" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ getSiteTypeLabel(row.siteType) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" min-width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'danger'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="address" label="地址" min-width="160" align="center" header-align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" header-align="center" />
|
||||
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</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="720px"
|
||||
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="site-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="站点名称" prop="siteName">
|
||||
<el-input v-model="formData.siteName" placeholder="请输入站点名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="站点编码" prop="siteCode">
|
||||
<el-input v-model="formData.siteCode" placeholder="请输入站点编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="站点类型">
|
||||
<el-select v-model="formData.siteType" placeholder="请选择站点类型" clearable>
|
||||
<el-option
|
||||
v-for="opt in siteTypeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</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.address" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人">
|
||||
<el-input v-model="formData.contactName" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话">
|
||||
<el-input v-model="formData.contactPhone" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="经营范围" style="grid-column: 1 / -1">
|
||||
<el-select
|
||||
v-model="formData.businessScopeList"
|
||||
multiple
|
||||
placeholder="请选择经营范围"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in businessScopeOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</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>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
fetchSiteList,
|
||||
fetchCreateSite,
|
||||
fetchUpdateSite,
|
||||
fetchDeleteSite,
|
||||
fetchDictDataOptions
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemSite' })
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.Site.SiteItem[]>([])
|
||||
const selectedRows = ref<Api.Site.SiteItem[]>([])
|
||||
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({
|
||||
siteName: '',
|
||||
siteCode: '',
|
||||
siteType: '',
|
||||
status: '1',
|
||||
address: '',
|
||||
contactName: '',
|
||||
contactPhone: '',
|
||||
businessScopeList: [] as string[],
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
siteName: [{ required: true, message: '请输入站点名称', trigger: 'blur' }],
|
||||
siteCode: [{ required: true, message: '请输入站点编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const siteTypeOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
const statusOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
const businessScopeOptions = ref<Api.Dict.DictDataOption[]>([])
|
||||
|
||||
const loadDictOptions = async () => {
|
||||
try {
|
||||
const [zdlx, bmzt, zdywfw] = await Promise.all([
|
||||
fetchDictDataOptions('zdlx'),
|
||||
fetchDictDataOptions('bmzt'),
|
||||
fetchDictDataOptions('zdywfw')
|
||||
])
|
||||
siteTypeOptions.value = zdlx || []
|
||||
statusOptions.value = bmzt || []
|
||||
businessScopeOptions.value = zdywfw || []
|
||||
} catch (error) {
|
||||
console.error('加载字典选项失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const getSiteTypeLabel = (code: string) => {
|
||||
if (!code) return '-'
|
||||
const opt = siteTypeOptions.value.find((o) => o.value === code)
|
||||
return opt ? opt.label : code
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.Site.SiteItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchSiteList({
|
||||
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.siteName = ''
|
||||
formData.siteCode = ''
|
||||
formData.siteType = ''
|
||||
formData.status = '1'
|
||||
formData.address = ''
|
||||
formData.contactName = ''
|
||||
formData.contactPhone = ''
|
||||
formData.businessScopeList = []
|
||||
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.Site.SiteItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑站点'
|
||||
editingId.value = row.id
|
||||
formData.siteName = row.siteName || ''
|
||||
formData.siteCode = row.siteCode || ''
|
||||
formData.siteType = row.siteType || ''
|
||||
formData.status = row.status || '1'
|
||||
formData.address = row.address || ''
|
||||
formData.contactName = row.contactName || ''
|
||||
formData.contactPhone = row.contactPhone || ''
|
||||
formData.businessScopeList = row.businessScope ? row.businessScope.split(',') : []
|
||||
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 = {
|
||||
siteName: formData.siteName.trim(),
|
||||
siteCode: formData.siteCode.trim(),
|
||||
siteType: formData.siteType || undefined,
|
||||
status: formData.status,
|
||||
address: formData.address?.trim() || undefined,
|
||||
contactName: formData.contactName?.trim() || undefined,
|
||||
contactPhone: formData.contactPhone?.trim() || undefined,
|
||||
businessScope: formData.businessScopeList.length ? formData.businessScopeList.join(',') : undefined,
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateSite(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateSite(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: Api.Site.SiteItem) {
|
||||
await ElMessageBox.confirm(`确定删除站点「${row.siteName}」吗?`, '提示', { type: 'warning' })
|
||||
await fetchDeleteSite(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' })
|
||||
for (const id of ids) {
|
||||
await fetchDeleteSite(id)
|
||||
}
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadDictOptions()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.site-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,11 @@
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>租户管理</h1>
|
||||
<p class="page-desc">维护多租户编号与名称;编号「{{ DEFAULT_TENANT_NO }}」为平台默认租户,不可删除且不可改编号。</p>
|
||||
<p class="page-desc"
|
||||
>维护多租户编号与名称;编号「{{
|
||||
DEFAULT_TENANT_NO
|
||||
}}」为平台默认租户,不可删除且不可改编号。</p
|
||||
>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
@@ -58,7 +62,13 @@
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" :selectable="rowSelectable" />
|
||||
<el-table-column
|
||||
type="selection"
|
||||
width="55"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:selectable="rowSelectable"
|
||||
/>
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
@@ -67,21 +77,76 @@
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="tenantNo" label="租户编号" min-width="120" align="center" header-align="center">
|
||||
<el-table-column
|
||||
prop="tenantNo"
|
||||
label="租户编号"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.tenantNo }}</span>
|
||||
<el-tag v-if="isDefaultTenantRow(row)" class="tag muted" size="small" style="margin-left: 8px">默认</el-tag>
|
||||
<el-tag
|
||||
v-if="isDefaultTenantRow(row)"
|
||||
class="tag muted"
|
||||
size="small"
|
||||
style="margin-left: 8px"
|
||||
>
|
||||
默认
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="tenantName" label="租户名称" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column prop="contactName" label="联系人" min-width="100" align="center" header-align="center" />
|
||||
<el-table-column prop="contactPhone" label="联系电话" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<el-table-column
|
||||
prop="tenantName"
|
||||
label="租户名称"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="contactName"
|
||||
label="联系人"
|
||||
min-width="100"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="contactPhone"
|
||||
label="联系电话"
|
||||
min-width="120"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="orderNum"
|
||||
label="排序"
|
||||
width="80"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="createTime"
|
||||
label="创建时间"
|
||||
min-width="160"
|
||||
align="center"
|
||||
header-align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
width="260"
|
||||
fixed="right"
|
||||
align="center"
|
||||
header-align="center"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" :disabled="isDefaultTenantRow(row)" @click="handleDelete(row)">删除</el-button>
|
||||
<el-button
|
||||
class="btn-op-item"
|
||||
:disabled="isDefaultTenantRow(row)"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -98,7 +163,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="560px">
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="720px"
|
||||
class="dialog-form-layout"
|
||||
align-center
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
@@ -106,6 +179,7 @@
|
||||
label-position="top"
|
||||
class="tenant-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="租户编号" prop="tenantNo">
|
||||
@@ -126,7 +200,12 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number v-model="formData.orderNum" :min="0" :max="9999" controls-position="right" />
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
@@ -154,7 +233,12 @@
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import { fetchTenantList, fetchCreateTenant, fetchUpdateTenant, fetchDeleteTenant } from '@/api/admin'
|
||||
import {
|
||||
fetchTenantList,
|
||||
fetchCreateTenant,
|
||||
fetchUpdateTenant,
|
||||
fetchDeleteTenant
|
||||
} from '@/api/admin'
|
||||
|
||||
/** 与后端 `settings.DEFAULT_TENANT_NO` 一致 */
|
||||
const DEFAULT_TENANT_NO = 'zkxw'
|
||||
@@ -187,7 +271,9 @@
|
||||
tenantName: [{ required: true, message: '请输入租户名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const deletableSelection = computed(() => selectedRows.value.filter((r) => !isDefaultTenantRow(r)))
|
||||
const deletableSelection = computed(() =>
|
||||
selectedRows.value.filter((r) => !isDefaultTenantRow(r))
|
||||
)
|
||||
|
||||
function isDefaultTenantNo(no: string) {
|
||||
return (no || '').trim() === DEFAULT_TENANT_NO
|
||||
@@ -305,7 +391,9 @@
|
||||
ElMessage.warning('所选中含默认租户或无可删项,请重新选择')
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个租户吗?`, '提示', { type: 'warning' })
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个租户吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
for (const id of ids) {
|
||||
await fetchDeleteTenant(id)
|
||||
}
|
||||
@@ -321,7 +409,15 @@
|
||||
<style scoped lang="scss">
|
||||
.tenant-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,10 +1,10 @@
|
||||
<!-- 商品单位管理 -->
|
||||
<!-- 物品单位管理 -->
|
||||
<!-- eslint-disable vue/no-v-model-argument -->
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<header class="page-head">
|
||||
<h1>单位管理</h1>
|
||||
<p class="page-desc">统一维护商品计量单位与排序规则。</p>
|
||||
<p class="page-desc">统一维护物品计量单位与排序规则。</p>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
@@ -12,160 +12,157 @@
|
||||
<h2 class="title-bar-heading">单位列表</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>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchForm.isActive" placeholder="请选择状态" clearable>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="启用" :value="true" />
|
||||
<el-option label="禁用" :value="false" />
|
||||
</el-select>
|
||||
</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-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
|
||||
<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>
|
||||
</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>
|
||||
|
||||
<ArtExcelExport
|
||||
:data="batchExportData"
|
||||
:list-total="pagination.total"
|
||||
:load-all-data="loadAllUnitsForExport"
|
||||
:filename="unitExportFilename"
|
||||
sheetName="单位数据"
|
||||
:columns="unitExportColumns"
|
||||
>
|
||||
批量导出
|
||||
</ArtExcelExport>
|
||||
|
||||
<ArtExcelExport
|
||||
:data="unitImportTemplateData"
|
||||
:filename="unitImportTemplateFilename"
|
||||
sheetName="单位导入模板"
|
||||
:columns="unitImportTemplateColumns"
|
||||
:showSuccessMessage="false"
|
||||
:showErrorMessage="false"
|
||||
>
|
||||
下载导入模板
|
||||
</ArtExcelExport>
|
||||
|
||||
<ArtExcelImport @import-success="handleUnitImportSuccess" @import-error="handleExcelImportError">
|
||||
<el-button class="btn-ghost">批量导入</el-button>
|
||||
</ArtExcelImport>
|
||||
</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">
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="index => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="name" label="单位名称" min-width="150" align="center" header-align="center" />
|
||||
<el-table-column prop="code" label="单位编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="isActive" label="状态" min-width="80" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.isActive ? 'success' : 'info'" size="small">{{ row.isActive ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序号" width="90" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.sortOrder ?? row.sort_order ?? 0 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</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">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-position="top"
|
||||
class="unit-dialog-form"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="单位名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="如:斤、kg、个" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位编码" prop="code">
|
||||
<el-input v-model="formData.code" placeholder="如:jin、kg、ge" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number v-model="formData.sortOrder" :min="0" :max="9999" controls-position="right" />
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
<div class="filter-actions-row">
|
||||
<span class="filter-hint">按名称、编码模糊查询。</span>
|
||||
<div class="filter-actions-end">
|
||||
<el-button class="btn-ghost" @click="handleReset">重置</el-button>
|
||||
<el-button class="btn-main" type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" v-if="isEdit"><el-switch v-model="formData.isActive" /></el-form-item>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="list-body list-wrap">
|
||||
<el-table
|
||||
class="list-table"
|
||||
:data="tableData"
|
||||
v-loading="loading"
|
||||
stripe
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" header-align="center" />
|
||||
<el-table-column
|
||||
type="index"
|
||||
label="序号"
|
||||
width="68"
|
||||
align="center"
|
||||
header-align="center"
|
||||
:index="(index) => (pagination.page - 1) * pagination.pageSize + index + 1"
|
||||
/>
|
||||
<el-table-column prop="unitName" label="单位名称" min-width="150" align="center" header-align="center" />
|
||||
<el-table-column prop="unitCode" label="单位编码" min-width="120" align="center" header-align="center" />
|
||||
<el-table-column prop="orderNum" label="排序" width="80" align="center" header-align="center" />
|
||||
<el-table-column prop="createTime" label="创建时间" min-width="160" align="center" header-align="center" />
|
||||
<el-table-column label="操作" width="260" fixed="right" align="center" header-align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button class="btn-op-item primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button class="btn-op-item" @click="handleDelete(row)">删除</el-button>
|
||||
</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="unit-dialog-form"
|
||||
scroll-to-error
|
||||
:scroll-into-view-options="{ behavior: 'smooth', block: 'center' }"
|
||||
>
|
||||
<div class="form-grid-2">
|
||||
<el-form-item label="单位名称" prop="unitName">
|
||||
<el-input v-model="formData.unitName" placeholder="如:斤、kg、个" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位编码" prop="unitCode">
|
||||
<el-input v-model="formData.unitCode" placeholder="如:jin、kg、ge" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<div class="sort-field">
|
||||
<el-input-number
|
||||
v-model="formData.orderNum"
|
||||
:min="0"
|
||||
:max="9999"
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="form-tip">数值越小越靠前</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="formData.remark" 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>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { withFullscreenLoading, fullscreenLoadingText } from '@/utils/fullscreen-loading'
|
||||
import { Plus, Close, Check } from '@element-plus/icons-vue'
|
||||
import { fetchUnitList, fetchCreateUnit, fetchUpdateUnit, fetchDeleteUnit } from '../../../api/admin'
|
||||
import { fetchNextSortOrder } from '../../../utils/sort-order'
|
||||
import { fetchAllPagedRecords } from '../../../utils/fetch-paged-list'
|
||||
defineOptions({ name: 'ProductUnit' })
|
||||
const searchForm = reactive<{ keyword: string; isActive: boolean | '' }>({ keyword: '', isActive: '' })
|
||||
const tableData = ref<any[]>([])
|
||||
const selectedRows = ref<any[]>([])
|
||||
import {
|
||||
fetchItemUnitList,
|
||||
fetchCreateItemUnit,
|
||||
fetchUpdateItemUnit,
|
||||
fetchDeleteItemUnit
|
||||
} from '@/api/admin'
|
||||
|
||||
defineOptions({ name: 'SystemItemUnit' })
|
||||
|
||||
const searchForm = reactive<{ keyword: string }>({ keyword: '' })
|
||||
const tableData = ref<Api.ItemUnit.ItemUnitItem[]>([])
|
||||
const selectedRows = ref<Api.ItemUnit.ItemUnitItem[]>([])
|
||||
const loading = ref(false)
|
||||
const pagination = reactive({ page: 1, pageSize: 10, total: 0 })
|
||||
const dialogVisible = ref(false)
|
||||
@@ -173,262 +170,138 @@
|
||||
const isEdit = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref()
|
||||
const handleSelectionChange = (rows: any[]) => {
|
||||
const editingId = ref<string | null>(null)
|
||||
|
||||
const formData = reactive({
|
||||
unitName: '',
|
||||
unitCode: '',
|
||||
orderNum: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
unitName: [{ required: true, message: '请输入单位名称', trigger: 'blur' }],
|
||||
unitCode: [{ required: true, message: '请输入单位编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows: Api.ItemUnit.ItemUnitItem[]) => {
|
||||
selectedRows.value = rows
|
||||
}
|
||||
|
||||
const unitExportFilename = 'base_unit_batch_export'
|
||||
const unitImportTemplateFilename = 'base_unit_import_template'
|
||||
|
||||
const unitExportColumns = {
|
||||
id: { title: 'id' },
|
||||
name: { title: '单位名称' },
|
||||
code: { title: '单位编码' },
|
||||
sortOrder: { title: '排序' },
|
||||
isActive: { title: '状态' }
|
||||
} as const
|
||||
|
||||
const unitImportTemplateColumns = {
|
||||
id: { title: 'id' },
|
||||
name: { title: '单位名称' },
|
||||
code: { title: '单位编码' },
|
||||
sortOrder: { title: '排序' },
|
||||
isActive: { title: '状态' }
|
||||
} as const
|
||||
|
||||
const unitImportTemplateData = [
|
||||
{
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
sortOrder: 0,
|
||||
isActive: true
|
||||
}
|
||||
]
|
||||
|
||||
const mapUnitExportRow = (row: any) => ({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
sortOrder: row.sortOrder ?? row.sort_order ?? 0,
|
||||
isActive: row.isActive
|
||||
})
|
||||
|
||||
const batchExportData = computed(() => {
|
||||
const sel = selectedRows.value || []
|
||||
if (sel.length === 0) return []
|
||||
return sel.map(mapUnitExportRow)
|
||||
})
|
||||
|
||||
const loadAllUnitsForExport = async () => {
|
||||
const all = await fetchAllPagedRecords((page, pageSize) =>
|
||||
fetchUnitList({
|
||||
page,
|
||||
pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
isActive: searchForm.isActive === '' ? undefined : searchForm.isActive
|
||||
})
|
||||
)
|
||||
return all.map(mapUnitExportRow)
|
||||
}
|
||||
|
||||
// ======================
|
||||
// 批量删除
|
||||
// ======================
|
||||
const handleBatchDelete = async () => {
|
||||
if (selectedRows.value.length === 0) return
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 条单位吗?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
await withFullscreenLoading(async () => {
|
||||
for (const row of selectedRows.value) {
|
||||
await fetchDeleteUnit(row.id)
|
||||
}
|
||||
ElMessage.success('批量删除成功')
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
}, fullscreenLoadingText.batchDelete)
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') console.error('批量删除失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// ======================
|
||||
// Excel 导入
|
||||
// ======================
|
||||
const importLoading = ref(false)
|
||||
|
||||
const parseNumber = (v: any, defaultValue: number) => {
|
||||
if (v === null || v === undefined || v === '') return defaultValue
|
||||
const n = Number(v)
|
||||
return Number.isFinite(n) ? n : defaultValue
|
||||
}
|
||||
|
||||
const parseBool = (v: any, defaultValue: boolean) => {
|
||||
if (v === null || v === undefined || v === '') return defaultValue
|
||||
if (typeof v === 'boolean') return v
|
||||
const s = String(v).trim().toLowerCase()
|
||||
if (['1', 'true', 'yes', '启用', 'enabled'].includes(s)) return true
|
||||
if (['0', 'false', 'no', '禁用', 'disabled'].includes(s)) return false
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
const getExcelValue = (row: any, keys: string[]) => {
|
||||
for (const k of keys) {
|
||||
const v = row?.[k]
|
||||
if (v !== undefined && v !== null && String(v).trim() !== '') return v
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const handleExcelImportError = (error: Error) => {
|
||||
ElMessage.error(`导入失败:${error.message}`)
|
||||
}
|
||||
|
||||
const handleUnitImportSuccess = async (rows: Array<Record<string, unknown>>) => {
|
||||
if (importLoading.value) return
|
||||
importLoading.value = true
|
||||
|
||||
try {
|
||||
if (!Array.isArray(rows) || rows.length === 0) {
|
||||
ElMessage.warning('没有可导入的数据')
|
||||
return
|
||||
}
|
||||
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
|
||||
for (const raw of rows) {
|
||||
const id = String(getExcelValue(raw, ['id', 'ID']) ?? '').trim()
|
||||
const name = String(getExcelValue(raw, ['name', '单位名称']) ?? '').trim()
|
||||
const code = String(getExcelValue(raw, ['code', '单位编码']) ?? '').trim()
|
||||
const sortOrder = parseNumber(getExcelValue(raw, ['sortOrder', '排序', 'sort_order']) as any, 0)
|
||||
const isActive = parseBool(getExcelValue(raw, ['isActive', '状态', 'is_active']) as any, true)
|
||||
|
||||
if (!name || !code) {
|
||||
failCount++
|
||||
continue
|
||||
}
|
||||
|
||||
const payload: any = { name, code, sortOrder, isActive }
|
||||
|
||||
if (id) {
|
||||
await fetchUpdateUnit(id, payload)
|
||||
} else {
|
||||
await fetchCreateUnit(payload)
|
||||
}
|
||||
|
||||
successCount++
|
||||
}
|
||||
|
||||
ElMessage.success(`导入完成:成功 ${successCount} 条,失败 ${failCount} 条`)
|
||||
selectedRows.value = []
|
||||
loadData()
|
||||
} catch (error: any) {
|
||||
console.error('导入单位失败:', error)
|
||||
ElMessage.error(`导入单位失败:${error?.message || error}`)
|
||||
} finally {
|
||||
importLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formData = reactive({ id: '', name: '', code: '', isActive: true, sortOrder: 0 })
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入单位名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入单位编码', trigger: 'blur' }]
|
||||
}
|
||||
const loadData = async () => {
|
||||
selectedRows.value = []
|
||||
async function loadData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchUnitList({
|
||||
const res = await fetchItemUnitList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: searchForm.keyword || undefined,
|
||||
isActive: searchForm.isActive === '' ? undefined : searchForm.isActive
|
||||
keyword: searchForm.keyword || undefined
|
||||
})
|
||||
const list = res.list || []
|
||||
tableData.value = list.map((item: any) => ({
|
||||
...item,
|
||||
sortOrder: item.sortOrder ?? item.sort_order ?? 0
|
||||
}))
|
||||
pagination.total = res.total
|
||||
} catch (error) { console.error('加载数据失败:', error) } finally { loading.value = false }
|
||||
}
|
||||
const handleSearch = () => { pagination.page = 1; loadData() }
|
||||
const handleReset = () => { Object.assign(searchForm, { keyword: '', isActive: '' }); loadData() }
|
||||
const handleAdd = async () => {
|
||||
dialogTitle.value = '新增单位'
|
||||
isEdit.value = false
|
||||
const next = await fetchNextSortOrder(params =>
|
||||
fetchUnitList({ ...params, keyword: undefined, isActive: undefined })
|
||||
)
|
||||
Object.assign(formData, { id: '', name: '', code: '', isActive: true, sortOrder: next })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const handleEdit = (row: any) => { dialogTitle.value = '编辑单位'; isEdit.value = true; Object.assign(formData, row); dialogVisible.value = true }
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该单位吗?', '提示', { type: 'warning' })
|
||||
await withFullscreenLoading(async () => {
|
||||
await fetchDeleteUnit(row.id)
|
||||
}, fullscreenLoadingText.delete)
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} catch (error: any) {
|
||||
if (error !== 'cancel') console.error('删除失败:', error)
|
||||
tableData.value = res.list || []
|
||||
pagination.total = res.total ?? 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
function handleSearch() {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.unitName = ''
|
||||
formData.unitCode = ''
|
||||
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.ItemUnit.ItemUnitItem) {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑单位'
|
||||
editingId.value = row.id
|
||||
formData.unitName = row.unitName || ''
|
||||
formData.unitCode = row.unitCode || ''
|
||||
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 {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
await withFullscreenLoading(async () => {
|
||||
if (isEdit.value) {
|
||||
const updateData: any = { name: formData.name, code: formData.code, sort_order: formData.sortOrder }
|
||||
updateData.is_active = formData.isActive
|
||||
await fetchUpdateUnit(formData.id, updateData)
|
||||
} else {
|
||||
await fetchCreateUnit({ name: formData.name, code: formData.code, sortOrder: formData.sortOrder, isActive: formData.isActive } as any)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
}, fullscreenLoadingText.save)
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
const payload = {
|
||||
unitName: formData.unitName.trim(),
|
||||
unitCode: formData.unitCode.trim(),
|
||||
orderNum: formData.orderNum,
|
||||
remark: formData.remark?.trim() || undefined
|
||||
}
|
||||
if (isEdit.value && editingId.value) {
|
||||
await fetchUpdateItemUnit(editingId.value, payload)
|
||||
} else {
|
||||
await fetchCreateItemUnit(payload)
|
||||
}
|
||||
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
await loadData()
|
||||
} catch {
|
||||
/* 校验或接口错误已由全局处理 */
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
onMounted(() => loadData())
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.unit-dialog-form {
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
|
||||
async function handleDelete(row: Api.ItemUnit.ItemUnitItem) {
|
||||
await ElMessageBox.confirm('确定要删除该单位吗?', '提示', { type: 'warning' })
|
||||
await fetchDeleteItemUnit(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await loadData()
|
||||
}
|
||||
}
|
||||
|
||||
.form-grid-2 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px 20px;
|
||||
}
|
||||
async function handleBatchDelete() {
|
||||
const ids = selectedRows.value.map((r) => r.id)
|
||||
if (!ids.length) return
|
||||
await ElMessageBox.confirm(`确定删除选中的 ${ids.length} 个单位吗?`, '提示', { type: 'warning' })
|
||||
for (const id of ids) {
|
||||
await fetchDeleteItemUnit(id)
|
||||
}
|
||||
selectedRows.value = []
|
||||
await loadData()
|
||||
}
|
||||
|
||||
.sort-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
.form-tip { margin-left: 8px; color: #909399; font-size: 12px; }
|
||||
<style scoped lang="scss">
|
||||
.unit-dialog-form {
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--mint-text-muted);
|
||||
padding: 0 0 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user