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

+1 -1
View File
@@ -78,7 +78,7 @@
const panelTitleClassList = panelTitleIconByText.map(([, cls]) => cls)
const panelTitleBarClass = 'title-bar-heading'
const panelTitleBarTextRules: RegExp[] = [
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域)/
/(数据概览|快捷菜单|分类列表|单位列表|商品列表|参数列表|用户列表|租户列表|区域列表|区域与询报价概览|报价管理|订单列表|最新订单|待结算订单|结算单记录|配送任务列表|预警列表|订单设置|客服设置|请求列表|近30日趋势|今日各区域|组织|组织管理|角色列表|菜单列表|字典类型|字典数据|站点列表|职位列表|快递公司列表)/
]
const refreshPanelTitleIcons = () => {
+327 -55
View File
@@ -79,17 +79,15 @@ export function fetchDeleteTenant(id: string) {
}
// ---------------------------------------------------------------------------
// 用户
// 用户管理
// ---------------------------------------------------------------------------
export function fetchUserList(params: {
export function fetchUserList(params?: {
page?: number
pageSize?: number
keyword?: string
role?: string
roleIn?: string
regionId?: string
isActive?: boolean
deptId?: string
status?: string
}) {
return request.get<{
list: Api.User.UserItem[]
@@ -104,84 +102,67 @@ export function fetchUserList(params: {
}
export function fetchUserDetail(id: string) {
return request.get<Api.User.UserDetail>({
return request.get<Api.User.UserItem>({
url: `/web/admin/users/${id}`
})
}
export function fetchCreateUser(data: {
username: string
userNo: string
nickName: string
password: string
realName: string
phone?: string
role: string
regionId?: string
regionIds?: string[]
avatarUrl?: string | null
deptId?: string
email?: string
phoneNo?: string
sex?: string
avatar?: string
status?: string
orderNum?: number
remark?: string
roleIds?: string[]
postIds?: string[]
}) {
return request.post({
url: '/web/admin/users',
data
data,
showSuccessMessage: true
})
}
export function fetchUpdateUser(
id: string,
data: Partial<{
realName: string
phone?: string
role: string
regionId?: string
regionIds?: string[]
isActive: boolean
avatarUrl?: string | null
nickName: string
deptId: string
email: string
phoneNo: string
sex: string
avatar: string
status: string
orderNum: number
remark: string
roleIds: string[]
postIds: string[]
}>
) {
return request.put({
url: `/web/admin/users/${id}`,
params: data
})
}
export function fetchAssignableMenuCodes() {
return request.get<{
list: Array<{
id: string
code: string
name: string
parentId?: string
sortOrder?: number
}>
}>({
url: '/web/admin/menus/assignable-codes'
})
}
export function fetchUserMenuScopes(userId: string) {
return request.get<{
userId: string
menuCodes: string[]
}>({
url: `/web/admin/users/${userId}/menu-scopes`
})
}
export function fetchUpdateUserMenuScopes(userId: string, menuCodes: string[]) {
return request.post({
url: `/web/admin/users/${userId}/menu-scopes`,
data: { menuCodes }
data,
showSuccessMessage: true
})
}
export function fetchDeleteUser(id: string) {
return request.del({
url: `/web/admin/users/${id}`
url: `/web/admin/users/${id}`,
showSuccessMessage: true
})
}
export function fetchResetPassword(id: string) {
return request.post({
url: `/web/admin/users/${id}/reset-password`
url: `/web/admin/users/${id}/reset-password`,
showSuccessMessage: true
})
}
@@ -572,3 +553,294 @@ export function fetchSaveProductParameterValues(
params: { productId, values }
})
}
// ---------------------------------------------------------------------------
// 字典类型
// ---------------------------------------------------------------------------
export function fetchDictTypeList(params?: {
page?: number
pageSize?: number
keyword?: string
}) {
return request.get<{
list: Api.Dict.DictTypeItem[]
total: number
page: number
pageSize: number
totalPages: number
}>({
url: '/web/admin/dict-types',
params
})
}
export function fetchCreateDictType(data: {
dictName: string
dictType: string
orderNum?: number
remark?: string
}) {
return request.post({ url: '/web/admin/dict-types', params: data })
}
export function fetchUpdateDictType(
id: string,
data: Partial<{
dictName: string
dictType: string
orderNum: number
remark: string
}>
) {
return request.put({ url: `/web/admin/dict-types/${id}`, params: data })
}
export function fetchDeleteDictType(id: string) {
return request.del({ url: `/web/admin/dict-types/${id}` })
}
// ---------------------------------------------------------------------------
// 字典数据
// ---------------------------------------------------------------------------
export function fetchDictDataList(params?: {
page?: number
pageSize?: number
dictType?: string
keyword?: string
}) {
return request.get<{
list: Api.Dict.DictDataItem[]
total: number
page: number
pageSize: number
totalPages: number
}>({
url: '/web/admin/dict-data',
params
})
}
export function fetchCreateDictData(data: {
dictLabel: string
dictValue: string
dictType: string
orderNum?: number
remark?: string
}) {
return request.post({ url: '/web/admin/dict-data', params: data })
}
export function fetchUpdateDictData(
id: string,
data: Partial<{
dictLabel: string
dictValue: string
orderNum: number
remark: string
}>
) {
return request.put({ url: `/web/admin/dict-data/${id}`, params: data })
}
export function fetchDeleteDictData(id: string) {
return request.del({ url: `/web/admin/dict-data/${id}` })
}
export function fetchDictDataOptions(dictType: string) {
return request.get<Api.Dict.DictDataOption[]>({
url: '/web/admin/dict-data/options',
params: { dictType }
})
}
// ---------------------------------------------------------------------------
// 组织管理
// ---------------------------------------------------------------------------
export function fetchDeptTree(params?: { keyword?: string }) {
return request.get<Api.Dept.DeptItem[]>({
url: '/web/admin/depts',
params
})
}
export function fetchCreateDept(data: {
parentId?: string
deptName: string
deptCategoryCode?: string
status?: string
leader?: string
leaderPhone?: string
orderNum?: number
remark?: string
}) {
return request.post({ url: '/web/admin/depts', params: data })
}
export function fetchUpdateDept(
id: string,
data: Partial<{
deptName: string
deptCategoryCode: string
status: string
leader: string
leaderPhone: string
orderNum: number
remark: string
}>
) {
return request.put({ url: `/web/admin/depts/${id}`, params: data })
}
export function fetchDeleteDept(id: string) {
return request.del({ url: `/web/admin/depts/${id}` })
}
// ---------------------------------------------------------------------------
// 站点管理
// ---------------------------------------------------------------------------
export function fetchSiteList(params?: { page?: number; pageSize?: number; keyword?: string }) {
return request.get<{ list: Api.Site.SiteItem[]; total: number; page: number; pageSize: number; totalPages: number }>({ url: '/web/admin/sites', params })
}
export function fetchCreateSite(data: { siteName: string; siteCode: string; siteType?: string; status?: string; address?: string; contactName?: string; contactPhone?: string; businessScope?: string; orderNum?: number; remark?: string }) {
return request.post({ url: '/web/admin/sites', params: data })
}
export function fetchUpdateSite(id: string, data: Partial<{ siteName: string; siteCode: string; siteType: string; status: string; address: string; contactName: string; contactPhone: string; businessScope: string; orderNum: number; remark: string }>) {
return request.put({ url: `/web/admin/sites/${id}`, params: data })
}
export function fetchDeleteSite(id: string) {
return request.del({ url: `/web/admin/sites/${id}` })
}
// ---------------------------------------------------------------------------
// 职位管理
// ---------------------------------------------------------------------------
export function fetchPostList(params?: { page?: number; pageSize?: number; keyword?: string }) {
return request.get<{ list: Api.Post.PostItem[]; total: number; page: number; pageSize: number; totalPages: number }>({ url: '/web/admin/posts', params })
}
export function fetchCreatePost(data: { postName: string; postCode: string; status?: string; orderNum?: number; remark?: string }) {
return request.post({ url: '/web/admin/posts', params: data })
}
export function fetchUpdatePost(id: string, data: Partial<{ postName: string; postCode: string; status: string; orderNum: number; remark: string }>) {
return request.put({ url: `/web/admin/posts/${id}`, params: data })
}
export function fetchDeletePost(id: string) {
return request.del({ url: `/web/admin/posts/${id}` })
}
// ---------------------------------------------------------------------------
// 物品单位管理
// ---------------------------------------------------------------------------
export function fetchItemUnitList(params?: { page?: number; pageSize?: number; keyword?: string }) {
return request.get<{ list: Api.ItemUnit.ItemUnitItem[]; total: number; page: number; pageSize: number; totalPages: number }>({ url: '/web/admin/item-units', params })
}
export function fetchCreateItemUnit(data: { unitCode: string; unitName: string; orderNum?: number; remark?: string }) {
return request.post({ url: '/web/admin/item-units', params: data })
}
export function fetchUpdateItemUnit(id: string, data: Partial<{ unitCode: string; unitName: string; orderNum: number; remark: string }>) {
return request.put({ url: `/web/admin/item-units/${id}`, params: data })
}
export function fetchDeleteItemUnit(id: string) {
return request.del({ url: `/web/admin/item-units/${id}` })
}
// ---------------------------------------------------------------------------
// 快递公司管理
// ---------------------------------------------------------------------------
export function fetchExpressCompanyList(params?: { page?: number; pageSize?: number; keyword?: string }) {
return request.get<{ list: Api.ExpressCompany.ExpressCompanyItem[]; total: number; page: number; pageSize: number; totalPages: number }>({ url: '/web/admin/express-companies', params })
}
export function fetchCreateExpressCompany(data: { expressCode: string; expressName: string; orderNum?: number; remark?: string }) {
return request.post({ url: '/web/admin/express-companies', params: data })
}
export function fetchUpdateExpressCompany(id: string, data: Partial<{ expressCode: string; expressName: string; orderNum: number; remark: string }>) {
return request.put({ url: `/web/admin/express-companies/${id}`, params: data })
}
export function fetchDeleteExpressCompany(id: string) {
return request.del({ url: `/web/admin/express-companies/${id}` })
}
// ---------------------------------------------------------------------------
// 菜单管理
// ---------------------------------------------------------------------------
export function fetchMenuTree(params?: { keyword?: string }) {
return request.get<Api.Menu.MenuItem[]>({
url: '/web/admin/menus',
params
})
}
export function fetchCreateMenu(data: {
menuName: string
parentId?: string
menuType?: string
path?: string
component?: string
perms?: string
isFrame?: number
visible?: string
status?: string
orderNum?: number
remark?: string
}) {
return request.post({ url: '/web/admin/menus', params: data })
}
export function fetchUpdateMenu(
id: string,
data: Partial<{
menuName: string
parentId: string
menuType: string
path: string
component: string
perms: string
isFrame: number
visible: string
status: string
orderNum: number
remark: string
}>
) {
return request.put({ url: `/web/admin/menus/${id}`, params: data })
}
export function fetchDeleteMenu(id: string) {
return request.del({ url: `/web/admin/menus/${id}` })
}
// ---------------------------------------------------------------------------
// 角色管理
// ---------------------------------------------------------------------------
export function fetchRoleList(params?: { page?: number; pageSize?: number; keyword?: string }) {
return request.get<{ list: Api.Role.RoleItem[]; total: number; page: number; pageSize: number; totalPages: number }>({ url: '/web/admin/role-management', params })
}
export function fetchCreateRole(data: { roleName: string; dataScope?: string; status?: string; orderNum?: number; remark?: string }) {
return request.post({ url: '/web/admin/role-management', params: data, showSuccessMessage: true })
}
export function fetchUpdateRole(id: string, data: Partial<{ roleName: string; dataScope: string; status: string; orderNum: number; remark: string }>) {
return request.put({ url: `/web/admin/role-management/${id}`, params: data, showSuccessMessage: true })
}
export function fetchDeleteRole(id: string) {
return request.del({ url: `/web/admin/role-management/${id}`, showSuccessMessage: true })
}
export function fetchRoleMenuIds(roleId: string) {
return request.get<{ menuIds: string[] }>({ url: `/web/admin/role-management/${roleId}/menus` })
}
export function fetchUpdateRoleMenus(roleId: string, menuIds: string[]) {
return request.put({ url: `/web/admin/role-management/${roleId}/menus`, params: { menuIds }, showSuccessMessage: true })
}
export function fetchRoleDataScope(roleId: string) {
return request.get<{ dataScope: string; scopeDetails: Array<{ scopeType: string; scopeValue: string }> }>({ url: `/web/admin/role-management/${roleId}/data-scope` })
}
export function fetchUpdateRoleDataScope(roleId: string, data: { dataScope: string; scopeDetails?: Array<{ scopeType: string; scopeValue: string }> }) {
return request.put({ url: `/web/admin/role-management/${roleId}/data-scope`, params: data, showSuccessMessage: true })
}
+83
View File
@@ -617,6 +617,89 @@ body {
}
}
/* 弹窗表单栅格(与 docs/web-admin-ui-spec-fresh-mint.md §9.3.1 / §10.3 一致) */
.form-grid-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 20px;
}
.form-grid-3 {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px 16px;
}
.form-grid-4 {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px 14px;
}
.form-grid-5 {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px 12px;
}
.form-col-span-2 {
grid-column: 1 / -1;
}
.sort-field {
display: flex;
align-items: center;
}
.form-tip {
margin-left: 8px;
color: var(--mint-text-muted);
font-size: 12px;
}
@media (max-width: 900px) {
.form-grid-3 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.form-grid-4,
.form-grid-5 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 560px) {
.form-grid-2,
.form-grid-3,
.form-grid-4,
.form-grid-5 {
grid-template-columns: 1fr;
}
}
/* 标准 CRUD 弹窗:相对视口上下各保留至少 50px 可视留白,正文区纵向滚动(不占满视口高度) */
.el-dialog.dialog-form-layout {
display: flex;
flex-direction: column;
max-height: calc(100vh - 100px);
overflow: hidden;
}
.el-dialog.dialog-form-layout .el-dialog__header {
flex-shrink: 0;
}
.el-dialog.dialog-form-layout .el-dialog__body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 18px 22px;
}
.el-dialog.dialog-form-layout .el-dialog__footer {
flex-shrink: 0;
}
#app-header > div {
background: var(--mint-surface) !important;
}
+1 -1
View File
@@ -624,7 +624,7 @@ body .el-table thead {
}
// 修复固定列与滚动列之间的边框
.el-table__fixed-column--right {
.el-table-fixed-column--right {
border-left: 1px solid var(--el-table-border-color) !important;
}
@@ -13,7 +13,7 @@
class="fixed right-10 bottom-15 size-9.5 flex-cc c-p border border-g-300 rounded-md tad-300 hover:bg-g-200"
@click="scrollToTop"
>
<ArtSvgIcon icon="ri:arrow-up-wide-line" class="text-g-500 text-lg" />
<ArtSvgIcon icon="arrow-up" class="text-g-500 text-lg" />
</div>
</Transition>
</template>
@@ -3,8 +3,8 @@
<div class="flex-cc">
<img
:style="logoStyle"
src="/logo1.jpg"
alt="陇耘鲜达"
src="/logo.png"
alt="中科兴皖"
class="block object-contain"
/>
</div>
@@ -1,24 +1,29 @@
<!-- 图标组件 -->
<!-- 图标组件:支持 Element Plus 图标(无 : 前缀)与 Iconify 图标(含 : 前缀) -->
<template>
<Icon v-if="icon" :icon="icon" v-bind="bindAttrs" class="art-svg-icon inline" />
<el-icon v-if="epIcon" v-bind="iconAttrs" class="art-svg-icon inline">
<component :is="epIcon" />
</el-icon>
<Icon v-else-if="icon" :icon="icon" v-bind="iconAttrs" class="art-svg-icon inline" />
</template>
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { resolveEpIcon } from '@/utils/ui/ep-icon-map'
defineOptions({ name: 'ArtSvgIcon', inheritAttrs: false })
interface Props {
/** Iconify icon name */
icon?: string
}
defineProps<Props>()
const props = defineProps<Props>()
const attrs = useAttrs()
const bindAttrs = computed<{ class: string; style: string }>(() => ({
const iconAttrs = computed<{ class: string; style: string }>(() => ({
class: (attrs.class as string) || '',
style: (attrs.style as string) || ''
}))
const epIcon = computed(() => resolveEpIcon(props.icon))
</script>
@@ -27,7 +27,7 @@
>
</div>
<div v-if="showArrow">
<ArtSvgIcon icon="ri:arrow-right-s-line" class="text-xl text-g-500" />
<ArtSvgIcon icon="arrow-right" class="text-xl text-g-500" />
</div>
</div>
</template>
@@ -2,7 +2,7 @@
<template>
<div>
<ElDropdown v-if="hasAnyAuthItem">
<ArtIconButton icon="ri:more-2-fill" class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm" />
<ArtIconButton icon="more-filled" class="!size-8 bg-g-200 dark:bg-g-300/45 text-sm" />
<template #dropdown>
<ElDropdownMenu>
<template v-for="item in list" :key="item.key">
@@ -36,11 +36,11 @@
// 默认按钮配置
const defaultButtons = {
add: { icon: 'ri:add-fill', class: 'bg-theme/12 text-theme' },
edit: { icon: 'ri:pencil-line', class: 'bg-secondary/12 text-secondary' },
delete: { icon: 'ri:delete-bin-5-line', class: 'bg-error/12 text-error' },
view: { icon: 'ri:eye-line', class: 'bg-info/12 text-info' },
more: { icon: 'ri:more-2-fill', class: '' }
add: { icon: 'plus', class: 'bg-theme/12 text-theme' },
edit: { icon: 'edit', class: 'bg-secondary/12 text-secondary' },
delete: { icon: 'delete', class: 'bg-error/12 text-error' },
view: { icon: 'view', class: 'bg-info/12 text-info' },
more: { icon: 'more-filled', class: '' }
} as const
// 获取图标内容
@@ -92,8 +92,8 @@
completedBg: '#57D187',
circle: false,
radius: 'calc(var(--custom-radius) / 3 + 2px)',
handlerIcon: 'solar:double-alt-arrow-right-linear',
successIcon: 'ri:check-fill',
handlerIcon: 'd-arrow-right',
successIcon: 'check',
handlerBg: '#fff',
textSize: '13px',
textColor: '#333'
@@ -77,8 +77,8 @@
</ElInput>
<div class="mt-3 flex-cb">
<div class="flex-c">
<ArtSvgIcon icon="ri:image-line" class="mr-5 c-p text-g-600 text-lg" />
<ArtSvgIcon icon="ri:emotion-happy-line" class="mr-5 c-p text-g-600 text-lg" />
<ArtSvgIcon icon="picture" class="mr-5 c-p text-g-600 text-lg" />
<ArtSvgIcon icon="smile" class="mr-5 c-p text-g-600 text-lg" />
</div>
<ElButton type="primary" @click="sendMessage" v-ripple class="min-w-20">发送</ElButton>
</div>
@@ -22,7 +22,7 @@
<div
class="h-4.5 flex-cc rounded border border-g-300 dark:!bg-g-200/50 !bg-box px-1.5 text-g-500"
>
<ArtSvgIcon icon="fluent:arrow-enter-left-20-filled" />
<ArtSvgIcon icon="select" />
</div>
</template>
</ElInput>
@@ -40,7 +40,7 @@
@mouseenter="highlightOnHover(index)"
>
{{ formatMenuTitle(item.meta.title) }}
<ArtSvgIcon v-show="isHighlighted(index)" icon="fluent:arrow-enter-left-20-filled" />
<ArtSvgIcon v-show="isHighlighted(index)" icon="select" />
</div>
</div>
</div>
@@ -65,7 +65,7 @@
class="size-5 selected-icon select-none rounded-full text-g-500 flex-cc c-p"
@click.stop="deleteHistory(index)"
>
<ArtSvgIcon icon="ri:close-large-fill" class="text-xs" />
<ArtSvgIcon icon="close-bold" class="text-xs" />
</div>
</div>
</div>
@@ -75,12 +75,12 @@
<template #footer>
<div class="dialog-footer box-border flex-c border-t-d pt-4.5 pb-1">
<div class="flex-cc">
<ArtSvgIcon icon="fluent:arrow-enter-left-20-filled" class="keyboard" />
<ArtSvgIcon icon="select" class="keyboard" />
<span class="mr-3.5 text-xs text-g-700">{{ $t('search.selectKeydown') }}</span>
</div>
<div class="flex-c">
<ArtSvgIcon icon="ri:arrow-up-wide-fill" class="keyboard" />
<ArtSvgIcon icon="ri:arrow-down-wide-fill" class="keyboard" />
<ArtSvgIcon icon="arrow-up" class="keyboard" />
<ArtSvgIcon icon="arrow-down" class="keyboard" />
<span class="mr-3.5 text-xs text-g-700">{{ $t('search.switchKeydown') }}</span>
</div>
<div class="flex-c">
@@ -29,7 +29,7 @@
<!-- 菜单按钮 -->
<ArtIconButton
v-if="isLeftMenu && shouldShowMenuButton"
icon="ri:menu-2-fill"
icon="fold"
class="ml-3 max-sm:ml-[7px]"
@click="visibleMenu"
/>
@@ -37,7 +37,7 @@
<!-- 刷新按钮 - 已隐藏 -->
<ArtIconButton
v-if="shouldShowRefreshButton"
icon="ri:refresh-line"
icon="refresh"
class="!ml-3 refresh-btn max-sm:!hidden"
:style="{ marginLeft: !isLeftMenu ? '10px' : '0' }"
@click="reload"
@@ -64,7 +64,7 @@
<!-- 全屏按钮 -->
<ArtIconButton
v-if="shouldShowFullscreen"
:icon="isFullscreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-fill'"
:icon="isFullscreen ? 'fullscreen' : 'fullscreen'"
:class="[!isFullscreen ? 'full-screen-btn' : 'exit-full-screen-btn', 'ml-3']"
class="max-md:!hidden"
@click="toggleFullScreen"
@@ -35,11 +35,11 @@
</div>
<ul class="py-4 mt-3 border-t border-g-300/80">
<li class="btn-item">
<ArtSvgIcon icon="ri:user-3-line" />
<ArtSvgIcon icon="user" />
<span>{{ userInfo.phone || '暂无电话' }}</span>
</li>
<li v-if="userInfo.regionName" class="btn-item">
<ArtSvgIcon icon="ri:map-pin-line" />
<ArtSvgIcon icon="location" />
<span>{{ userInfo.regionName }}</span>
</li>
<div class="w-full h-px my-2 bg-g-300/80"></div>
@@ -113,6 +113,9 @@
.user-avatar-wrapper {
cursor: pointer;
margin-right: 5px;
background-color: #fff;
border-radius: 50%;
padding: 2px;
&:hover {
opacity: 0.8;
@@ -54,7 +54,7 @@
<ArtIconButton
class="switch-btn size-10"
icon="ri:arrow-left-right-fill"
icon="sort"
@click="toggleDualMenuMode"
/>
</div>
@@ -83,9 +83,9 @@
opacity: 1
}"
>
<template v-if="AppConfig.systemInfo.name === '陇耘鲜达'">
<span class="system-name-main">陇耘</span>
<span class="system-name-accent">鲜达</span>
<template v-if="AppConfig.systemInfo.name === '中科兴皖'">
<span class="system-name-main">中科</span>
<span class="system-name-accent">兴皖</span>
</template>
<template v-else>
{{ AppConfig.systemInfo.name }}
@@ -119,7 +119,7 @@
<div class="dual-menu-collapse-btn" v-if="isDualMenu" @click="toggleMenuVisibility">
<ArtSvgIcon
class="text-g-500/70"
:icon="menuOpen ? 'ri:arrow-left-wide-fill' : 'ri:arrow-right-wide-fill'"
:icon="menuOpen ? 'arrow-left' : 'arrow-right'"
/>
</div>
@@ -84,7 +84,7 @@
v-show="currentTabIsEmpty"
class="relative top-25 h-full text-g-500 text-center !bg-transparent"
>
<ArtSvgIcon icon="system-uicons:inbox" class="text-5xl" />
<ArtSvgIcon icon="box" class="text-5xl" />
<p class="mt-3.5 text-xs !bg-transparent"
>{{ $t('notice.text[0]') }}{{ barList[barActiveIndex].name }}</p
>
@@ -271,30 +271,30 @@
const useNotificationStyles = () => {
const noticeStyleMap: Record<NoticeType, NoticeStyle> = {
email: {
icon: 'ri:mail-line',
icon: 'message',
iconClass: 'bg-warning/12 text-warning'
},
message: {
icon: 'ri:volume-down-line',
icon: 'notification',
iconClass: 'bg-success/12 text-success'
},
collection: {
icon: 'ri:heart-3-line',
icon: 'star-filled',
iconClass: 'bg-danger/12 text-danger'
},
user: {
icon: 'ri:volume-down-line',
icon: 'notification',
iconClass: 'bg-info/12 text-info'
},
notice: {
icon: 'ri:notification-3-line',
icon: 'bell',
iconClass: 'bg-theme/12 text-theme'
}
}
const getNoticeStyle = (type: NoticeType): NoticeStyle => {
const defaultStyle: NoticeStyle = {
icon: 'ri:arrow-right-circle-line',
icon: 'arrow-right',
iconClass: 'bg-theme/12 text-theme'
}
@@ -11,7 +11,7 @@
@click="colorHandlers.selectColor(color)"
>
<ArtSvgIcon
icon="ri:check-fill"
icon="check"
class="text-base !text-white"
v-show="color === systemThemeColor"
/>
@@ -5,7 +5,7 @@
@click="$emit('close')"
class="flex-cc c-p size-7.5 !transition-all duration-200 rounded hover:bg-g-300/80"
>
<ArtSvgIcon icon="ri:close-fill" class="block text-xl text-g-600" />
<ArtSvgIcon icon="close" class="block text-xl text-g-600" />
</div>
</div>
</div>
@@ -36,7 +36,7 @@
class="worktab-close inline-flex flex-cc relative ml-0.5 p-1 rounded-full tad-200"
@click.stop="closeWorktab('current', item.path)"
>
<ArtSvgIcon icon="ri:close-large-fill" class="text-[10px] text-inherit" />
<ArtSvgIcon icon="close-bold" class="text-[10px] text-inherit" />
</span>
</li>
</ul>
@@ -47,7 +47,7 @@
class="worktab-menu-btn flex-cc relative top-0 size-8 leading-8 text-center c-p tad-200"
@click="(e) => showMenu(e, activeTab)"
>
<ArtSvgIcon icon="iconamoon:arrow-down-2-thin" class="text-2xl text-g-700" />
<ArtSvgIcon icon="arrow-down" class="text-2xl text-g-700" />
</div>
</div>
@@ -161,38 +161,38 @@
{
key: 'refresh',
label: t('worktab.btn.refresh'),
icon: 'ri:refresh-line',
icon: 'refresh',
disabled: !isCurrentTab
},
{
key: 'fixed',
label: currentTab?.fixedTab ? t('worktab.btn.unfixed') : t('worktab.btn.fixed'),
icon: 'ri:pushpin-2-line',
icon: 'position',
disabled: false,
showLine: true
},
{
key: 'left',
label: t('worktab.btn.closeLeft'),
icon: 'ri:arrow-left-s-line',
icon: 'arrow-left',
disabled: clickedIndex === 0 || fixedStatus.areAllLeftTabsFixed
},
{
key: 'right',
label: t('worktab.btn.closeRight'),
icon: 'ri:arrow-right-s-line',
icon: 'arrow-right',
disabled: isLastTab || fixedStatus.areAllRightTabsFixed
},
{
key: 'other',
label: t('worktab.btn.closeOther'),
icon: 'ri:close-fill',
icon: 'close',
disabled: isOneTab || fixedStatus.areAllOtherTabsFixed
},
{
key: 'all',
label: t('worktab.btn.closeAll'),
icon: 'ri:close-circle-line',
icon: 'circle-close',
disabled: isOneTab || fixedStatus.areAllTabsFixed
}
]
@@ -45,7 +45,7 @@
>{{ item.label }}</span
>
<ArtSvgIcon
icon="ri:arrow-right-s-line"
icon="arrow-right"
class="ubmenu-arrow ml-auto mr-0 text-base text-g-500 transition-transform duration-150"
/>
</div>
@@ -12,7 +12,7 @@
@click="search"
:class="showSearchBar ? 'active !bg-theme hover:!bg-theme/80' : ''"
>
<ArtSvgIcon icon="ri:search-line" :class="showSearchBar ? 'text-white' : 'text-g-700'" />
<ArtSvgIcon icon="search" :class="showSearchBar ? 'text-white' : 'text-g-700'" />
</div>
<div
v-if="shouldShow('refresh')"
@@ -21,14 +21,14 @@
:class="{ loading: loading && isManualRefresh }"
>
<ArtSvgIcon
icon="ri:refresh-line"
icon="refresh"
:class="loading && isManualRefresh ? 'animate-spin text-g-600' : ''"
/>
</div>
<ElDropdown v-if="shouldShow('size')" @command="handleTableSizeChange">
<div class="button">
<ArtSvgIcon icon="ri:arrow-up-down-fill" />
<ArtSvgIcon icon="sort" />
</div>
<template #dropdown>
<ElDropdownMenu>
@@ -50,14 +50,14 @@
</ElDropdown>
<div v-if="shouldShow('fullscreen')" class="button" @click="toggleFullScreen">
<ArtSvgIcon :icon="isFullScreen ? 'ri:fullscreen-exit-line' : 'ri:fullscreen-line'" />
<ArtSvgIcon :icon="isFullScreen ? 'fullscreen' : 'fullscreen'" />
</div>
<!-- 列设置 -->
<ElPopover v-if="shouldShow('columns')" placement="bottom" trigger="click">
<template #reference>
<div class="button">
<ArtSvgIcon icon="ri:align-right" />
<ArtSvgIcon icon="operation" />
</div>
</template>
<div>
@@ -80,7 +80,7 @@
:class="item.fixed ? 'cursor-default text-g-300' : 'cursor-move'"
>
<ArtSvgIcon
:icon="item.fixed ? 'ri:unpin-line' : 'ri:drag-move-2-fill'"
:icon="item.fixed ? 'close' : 'rank'"
class="text-base"
/>
</div>
@@ -102,7 +102,7 @@
<ElPopover v-if="shouldShow('settings')" placement="bottom" trigger="click">
<template #reference>
<div class="button">
<ArtSvgIcon icon="ri:settings-line" />
<ArtSvgIcon icon="setting" />
</div>
</template>
<div>
@@ -7,7 +7,7 @@
:style="containerStyle"
>
<div class="flex-cc absolute left-0 h-full w-9 z-10" :style="{ backgroundColor: bgColor }">
<ArtSvgIcon icon="ri:volume-down-line" class="text-lg" />
<ArtSvgIcon icon="notification" class="text-lg" />
</div>
<div
@@ -37,7 +37,7 @@
:style="{ backgroundColor: bgColor }"
@click="handleClose"
>
<ArtSvgIcon icon="ri:close-fill" class="text-lg" />
<ArtSvgIcon icon="close" class="text-lg" />
</div>
</div>
</template>
@@ -21,12 +21,12 @@
:style="{ background: color, '--index': index }"
@click="changeThemeColor(color)"
>
<ArtSvgIcon v-if="color === systemThemeColor" icon="ri:check-fill" class="text-white" />
<ArtSvgIcon v-if="color === systemThemeColor" icon="check" class="text-white" />
</div>
</div>
<div class="btn palette-btn relative z-[2] h-8 w-8 c-p flex-cc tad-300">
<ArtSvgIcon
icon="ri:palette-line"
icon="brush"
class="text-xl text-g-800 transition-colors duration-300"
/>
</div>
@@ -50,7 +50,7 @@
:class="{ 'is-selected': locale === lang.value }"
>
<span class="menu-txt">{{ lang.label }}</span>
<ArtSvgIcon icon="ri:check-fill" class="text-base" v-if="locale === lang.value" />
<ArtSvgIcon icon="check" class="text-base" v-if="locale === lang.value" />
</ElDropdownItem>
</div>
</ElDropdownMenu>
@@ -62,7 +62,7 @@
@click="themeAnimation"
>
<ArtSvgIcon
:icon="isDark ? 'ri:sun-fill' : 'ri:moon-line'"
:icon="isDark ? 'sunny' : 'moon'"
class="text-xl text-g-800 transition-colors duration-300"
/>
</div>
+1 -1
View File
@@ -38,7 +38,7 @@ import { headerBarConfig } from './modules/headerBar'
const appConfig: SystemConfig = {
// 系统信息
systemInfo: {
name: '陇耘鲜达' // 系统名称
name: '中科兴皖' // 系统名称
},
// 系统主题
systemThemeStyles: {
+2 -2
View File
@@ -10,7 +10,7 @@ const fastEnterConfig: FastEnterConfig = {
{
name: '工作台',
description: '入口与参考页跳转',
icon: 'ri:pie-chart-line',
icon: 'data-analysis',
iconColor: '#10b981',
enabled: true,
order: 1,
@@ -19,7 +19,7 @@ const fastEnterConfig: FastEnterConfig = {
{
name: '官方文档',
description: '使用指南与开发文档',
icon: 'ri:bill-line',
icon: 'tickets',
iconColor: '#ffb100',
enabled: true,
order: 2,
+1 -1
View File
@@ -6,7 +6,7 @@ export const exceptionRoutes: AppRouteRecord = {
component: '/index/index',
meta: {
title: 'menus.exception.title',
icon: 'ri:error-warning-line'
icon: 'warning'
},
children: [
{
+1 -1
View File
@@ -3,6 +3,6 @@ import { sqxpAdminRoutes } from './sqxp'
/**
* 动态菜单与路由(前端权限模式 VITE_ACCESS_MODE=frontend 时使用)
* 陇耘鲜达业务菜单,与 backend/app/api/menu.py 中 get_admin_menu_list 对齐
* 中科兴皖业务菜单,与 backend/app/api/menu.py 中 get_admin_menu_list 对齐
*/
export const routeModules: AppRouteRecord[] = [...sqxpAdminRoutes]
+73 -17
View File
@@ -12,7 +12,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
component: '/index/index',
meta: {
title: 'Demo',
icon: 'ri:layout-grid-line',
icon: 'grid',
keepAlive: false,
roles: ['admin']
},
@@ -24,7 +24,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
component: '/dashboard/console/index',
meta: {
title: '工作台',
icon: 'ri:home-smile-2-line',
icon: 'home-filled',
keepAlive: true,
fixedTab: true,
menuCode: 'dashboard'
@@ -57,20 +57,6 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'category',
component: '/system/category/index',
meta: { title: '商品分类', keepAlive: true }
},
{
id: '10000000000000000000000000000007',
name: 'User',
path: 'user',
component: '/system/user/index',
meta: { title: '用户管理', keepAlive: true }
},
{
id: '10000000000000000000000000000008',
name: 'UserDetail',
path: 'user/detail',
component: '/system/user/detail',
meta: { title: '用户详情', isHide: true, keepAlive: true }
}
]
},
@@ -81,7 +67,7 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
component: '/index/index',
meta: {
title: '系统管理',
icon: 'ri:settings-3-line',
icon: 'setting',
keepAlive: false,
roles: ['admin']
},
@@ -92,6 +78,76 @@ export const sqxpAdminRoutes: AppRouteRecord[] = [
path: 'tenant',
component: '/system/tenant/index',
meta: { title: '租户管理', keepAlive: true }
},
{
id: 'user-management-route',
name: 'SystemUser',
path: 'user',
component: '/system/user/index',
meta: { title: '用户管理', keepAlive: true }
},
{
id: 'user-detail-route',
name: 'SystemUserDetail',
path: 'user/detail',
component: '/system/user/detail',
meta: { title: '用户详情', isHide: true, keepAlive: true }
},
{
id: 'role-management-route',
name: 'SystemRole',
path: 'role',
component: '/system/role/index',
meta: { title: '角色管理', keepAlive: true }
},
{
id: 'menu-management-route',
name: 'SystemMenu',
path: 'menu',
component: '/system/menu/index',
meta: { title: '菜单管理', keepAlive: true }
},
{
id: 'dept-management-route',
name: 'SystemDept',
path: 'dept',
component: '/system/dept/index',
meta: { title: '组织管理', keepAlive: true }
},
{
id: 'dict-management-route',
name: 'SystemDict',
path: 'dict',
component: '/system/dict/index',
meta: { title: '字典管理', keepAlive: true }
},
{
id: 'site-management-route',
name: 'SystemSite',
path: 'site',
component: '/system/site/index',
meta: { title: '站点管理', keepAlive: true }
},
{
id: 'post-management-route',
name: 'SystemPost',
path: 'post',
component: '/system/post/index',
meta: { title: '职位管理', keepAlive: true }
},
{
id: 'unit-management-route',
name: 'SystemUnit',
path: 'unit',
component: '/system/unit/index',
meta: { title: '物品单位', keepAlive: true }
},
{
id: 'express-management-route',
name: 'SystemExpress',
path: 'express',
component: '/system/express/index',
meta: { title: '快递公司管理', keepAlive: true }
}
]
}
+157 -16
View File
@@ -14,7 +14,7 @@ declare namespace Api {
regionName?: string
email?: string
roles?: string[]
/** 自定义头像 URL,空则前端使用叁强 logo */
/** 自定义头像 URL,空则前端使用中科兴皖 logo */
avatarUrl?: string | null
}
@@ -34,21 +34,22 @@ declare namespace Api {
namespace User {
interface UserItem {
id: string
username: string
realName: string
phone: string
avatarUrl?: string | null
role: string
regionId?: string
regionName?: string
isActive: boolean
createAt: string
createName: string
}
interface UserDetail extends UserItem {
updateAt: string
updateBy: string
deptId: string
userNo: string
nickName: string
email: string
phoneNo: string
sex: string
avatar: string
status: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
deptName?: string
roleIds?: string[]
postIds?: string[]
}
}
@@ -385,6 +386,146 @@ declare namespace Api {
status: string
}
}
namespace Dept {
interface DeptItem {
id: string
parentId: string
deptName: string
deptCategoryCode: string
status: string
leader: string
leaderPhone: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
children?: DeptItem[]
}
}
namespace Dict {
interface DictTypeItem {
id: string
dictName: string
dictType: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
}
interface DictDataItem {
id: string
dictLabel: string
dictValue: string
dictType: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
}
interface DictDataOption {
label: string
value: string
}
}
namespace Site {
interface SiteItem {
id: string
siteName: string
siteCode: string
siteType: string
status: string
address: string
contactName: string
contactPhone: string
businessScope: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
}
}
namespace Post {
interface PostItem {
id: string
postName: string
postCode: string
status: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
}
}
namespace ItemUnit {
interface ItemUnitItem {
id: string
unitCode: string
unitName: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
}
}
namespace Menu {
interface MenuItem {
id: string
menuName: string
parentId: string
menuType: string
path: string
component: string
perms: string
isFrame: number
visible: string
status: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
children?: MenuItem[]
}
}
namespace ExpressCompany {
interface ExpressCompanyItem {
id: string
expressCode: string
expressName: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
}
}
namespace Role {
interface RoleItem {
id: string
roleName: string
dataScope: string
status: string
tenantNo: string
orderNum: number
remark: string
createTime: string
updateTime: string
}
}
}
declare module '*.vue' {
+118
View File
@@ -0,0 +1,118 @@
import type { Component } from 'vue'
import {
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
Back,
Bell,
Box,
Brush,
ChatDotRound,
Check,
CircleClose,
Close,
CloseBold,
DArrowRight,
DataAnalysis,
Delete,
Document,
Edit,
Expand,
Fold,
FullScreen,
Grid,
HomeFilled,
Location,
Message,
Moon,
MoreFilled,
Notification,
Operation,
Picture,
Plus,
Position,
Rank,
Refresh,
RefreshLeft,
Right,
Search,
Select,
Setting,
Sort,
StarFilled,
Sunny,
Tickets,
Upload,
User,
View,
Warning
} from '@element-plus/icons-vue'
/**
* Element Plus 图标名 → 组件映射
*
* 约定:icon 名不含 `:` 前缀 → 查此映射走 el-icon 渲染;
* 含 `:` → 走 Iconify(向后兼容)。
*/
export const epIconMap: Record<string, Component> = {
// Navigation / arrows
'arrow-down': ArrowDown,
'arrow-left': ArrowLeft,
'arrow-right': ArrowRight,
'arrow-up': ArrowUp,
back: Back,
right: Right,
'd-arrow-right': DArrowRight,
select: Select,
sort: Sort,
// Actions
search: Search,
setting: Setting,
refresh: Refresh,
'refresh-left': RefreshLeft,
close: Close,
'close-bold': CloseBold,
'circle-close': CircleClose,
check: Check,
plus: Plus,
delete: Delete,
edit: Edit,
view: View,
'more-filled': MoreFilled,
copy: Document,
upload: Upload,
// Content
grid: Grid,
'home-filled': HomeFilled,
document: Document,
picture: Picture,
message: Message,
bell: Bell,
chat: ChatDotRound,
box: Box,
tickets: Tickets,
'data-analysis': DataAnalysis,
// UI
fullscreen: FullScreen,
fold: Fold,
expand: Expand,
sunny: Sunny,
moon: Moon,
user: User,
location: Location,
position: Position,
notification: Notification,
rank: Rank,
operation: Operation,
'star-filled': StarFilled,
smile: ChatDotRound,
brush: Brush,
// Status
warning: Warning
}
export function resolveEpIcon(name: string | undefined): Component | undefined {
if (!name || name.includes(':')) return undefined
return epIconMap[name]
}
+3 -3
View File
@@ -1,12 +1,12 @@
/** 未设置头像时统一使用叁强 logo(与登录页、侧栏 ArtLogo 一致) */
export const DEFAULT_USER_AVATAR_URL = '/logo1.jpg'
/** 未设置头像时统一使用中科兴皖 logo(与登录页、侧栏 ArtLogo 一致) */
export const DEFAULT_USER_AVATAR_URL = '/logo.png'
/** @deprecated 各角色默认已统一为 logo,保留参数仅为兼容旧调用 */
export function defaultAvatarPathByRole(_role: string | undefined): string {
return DEFAULT_USER_AVATAR_URL
}
/** 展示用头像:有自定义 URL 用自定义,否则用叁强 logo */
/** 展示用头像:有自定义 URL 用自定义,否则用中科兴皖 logo */
export function resolveUserAvatarUrl(avatarUrl: string | null | undefined, role: string | undefined): string {
const u = avatarUrl != null ? String(avatarUrl).trim() : ''
if (u) return u
+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