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:
zlwuandCursor committed 2026-05-13 17:48:22 +08:00
1 parent 058811ae77
commit 246ad2228e
153 files changed
+20578 -2040

No files matched your search

+12 -10
View File
@@ -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 {
+437
View File
@@ -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>
+657
View File
@@ -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>
+494
View File
@@ -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>
+341
View File
@@ -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>
+539
View File
@@ -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>
+417
View File
@@ -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>
+113 -17
View File
@@ -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>
+259 -386
View File
@@ -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