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