Commit 0cb0b180 by qinjianhui

Merge branch 'dev_user_permission' into 'dev'

Dev user permission

See merge request !256
parents 0d769efe 76653b9a
...@@ -21,6 +21,9 @@ export function changePasswordApi(data: { oldPwd: string; newPwd: string }) { ...@@ -21,6 +21,9 @@ export function changePasswordApi(data: { oldPwd: string; newPwd: string }) {
data, data,
) )
} }
export function logoutApi() {
return axios.get<never, BaseRespData<never>>('factory/logout')
}
// 用户列表 // 用户列表
export function getUserList( export function getUserList(
data: userSearchForm, data: userSearchForm,
......
import axios from './axios'
import { BaseRespData } from '@/types/api'
import {
RoleItem,
RoleFormPayload,
RolePermsData,
RoleUser,
MenuTreeNode,
} from '@/types/api/role'
// 角色列表
export function getRoleListApi() {
return axios.get<never, BaseRespData<RoleItem[]>>('/factoryRole/list')
}
// 新增角色
export function addRoleApi(form: RoleFormPayload) {
return axios.post<never, BaseRespData<never>>('/factoryRole/add', {
...form,
type: 'FUNCTION_ROLE',
})
}
// 编辑角色
export function updateRoleApi(form: RoleFormPayload) {
return axios.post<never, BaseRespData<never>>('/factoryRole/update', {
...form,
type: 'FUNCTION_ROLE',
})
}
// 删除角色
export function deleteRoleApi(ids: number | string) {
return axios.get<never, BaseRespData<never>>('/factoryRole/delete', {
params: { ids },
})
}
// 复制角色
export function copyRoleApi(sysRoleId: number | string) {
return axios.post<never, BaseRespData<RoleItem>>(
`/factoryRole/copy-role/${sysRoleId}`,
)
}
// 角色下的用户
export function getRoleUsersApi(id: number | string) {
return axios.get<never, BaseRespData<RoleUser[]>>(
'/factoryRole/get-user-by-role-id',
{
params: { id },
},
)
}
// 保存角色权限
export function saveRoleAuthApi(data: {
roleId: number | string
menuIds: number[]
}) {
return axios.post<never, BaseRespData<never>>(
'/factoryRole/add-role-all',
data,
)
}
// 获取角色已有权限
export function getRolePermsApi(roleId: number | string) {
return axios.get<never, BaseRespData<RolePermsData>>(
'/factoryRole/get-role-perms',
{
params: { roleId },
},
)
}
// 系统菜单树
export function getSysMenusTreeApi() {
return axios.get<never, BaseRespData<MenuTreeNode[]>>(
'factoryMenu/get-factory-menus-tree',
)
}
// 按钮 + 左边栏权限:每个页面把自己的 route 传进来,
// 后端返回当前页可见的 buttons列表
export function getSysPermissionApi(reqMenu: string) {
return axios.get<never, BaseRespData<{ buttons: string[] }>>(
'factoryMenu/request-permission',
{
params: { reqMenu },
},
)
}
...@@ -167,42 +167,56 @@ import { ...@@ -167,42 +167,56 @@ import {
onUnmounted, onUnmounted,
} from 'vue' } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import Menu, { type MenuItem } from '@/router/menu' import { transformMenuTree, type MenuItem } from '@/router/menu'
import userUserStore from '@/store/user' import userUserStore from '@/store/user'
import useMenuTreeStore from '@/store/menuTree'
import type { FormRules } from 'element-plus' import type { FormRules } from 'element-plus'
import { useValue } from '@/utils/hooks/useValue' import { useValue } from '@/utils/hooks/useValue'
import { changePasswordApi } from '@/api/auth' import { changePasswordApi, logoutApi } from '@/api/auth'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import NavMenuItem from './NavMenuItem.vue' import NavMenuItem from './NavMenuItem.vue'
const removeMenuItemByIndex = (items: MenuItem[], indexToRemove: string) => {
for (let i = items.length - 1; i >= 0; i--) {
if (items[i].index === indexToRemove) {
items.splice(i, 1)
} else if (items[i].children?.length) {
removeMenuItemByIndex(items[i].children!, indexToRemove)
}
}
}
interface PasswordForm { interface PasswordForm {
oldPwd: string oldPwd: string
newPwd: string newPwd: string
confirmPwd?: string confirmPwd?: string
} }
const route = useRoute() const route = useRoute()
const menuList = reactive(Menu) const menuTreeStore = useMenuTreeStore()
const userStore = userUserStore()
const userInfo = userStore.user
const DASHBOARD_PATH = '/dashboard' const DASHBOARD_PATH = '/dashboard'
const DASHBOARD_TITLE = '概览' const DASHBOARD_TITLE = '概览'
const displayMenuList = computed(() => // 工厂类型 !== 2 时,不显示 /system/customers 菜单入口(路由仍可访问)
menuList.filter((item) => item.index !== DASHBOARD_PATH), const isShowCustomerManagement = computed(() => userInfo?.factory?.type === 2)
)
const removeMenuItemByIndex = (
items: MenuItem[],
indexToRemove: string,
): MenuItem[] => {
return items
.filter((item) => item.index !== indexToRemove)
.map((item) =>
item.children?.length
? {
...item,
children: removeMenuItemByIndex(item.children, indexToRemove),
}
: item,
)
}
const userStore = userUserStore() const displayMenuList = computed<MenuItem[]>(() => {
const userInfo = userStore.user let list = transformMenuTree(menuTreeStore.menusTreeNoBtn)
if (!isShowCustomerManagement.value) {
list = removeMenuItemByIndex(list, '/system/customers')
}
return list.filter((item) => item.index !== DASHBOARD_PATH)
})
const dialogVisible = ref(false) const dialogVisible = ref(false)
// 密码form // 密码form
const [passwordForm, resetPasswordForm] = useValue<PasswordForm>( const [passwordForm, resetPasswordForm] = useValue<PasswordForm>(
...@@ -240,9 +254,12 @@ const rules = reactive<FormRules<PasswordForm>>({ ...@@ -240,9 +254,12 @@ const rules = reactive<FormRules<PasswordForm>>({
], ],
}) })
const logout = () => { const logout = async () => {
await logoutApi()
userStore.logout() userStore.logout()
window.location.reload() setTimeout(() => {
window.location.reload()
}, 600)
} }
const resetPassword = () => { const resetPassword = () => {
dialogVisible.value = true dialogVisible.value = true
...@@ -412,21 +429,6 @@ watch(activeTab, (newTab) => { ...@@ -412,21 +429,6 @@ watch(activeTab, (newTab) => {
}) })
watch(scrollableTabs, checkScrollArrows) watch(scrollableTabs, checkScrollArrows)
onMounted(() => { onMounted(() => {
let showCustomerManagement = false
const userJson = localStorage.getItem('user')
if (userJson) {
try {
const user = JSON.parse(userJson)
if (user && user.factory && user.factory.type === 2) {
showCustomerManagement = true
}
} catch (e) {
// ignore
}
}
if (!showCustomerManagement) {
removeMenuItemByIndex(menuList, '/system/customers')
}
// 初始检查 // 初始检查
checkScrollArrows() checkScrollArrows()
......
...@@ -77,7 +77,8 @@ import { ...@@ -77,7 +77,8 @@ import {
} from 'vue' } from 'vue'
import type { CustomColumn } from '@/types/table' import type { CustomColumn } from '@/types/table'
import RenderColumn from './RenderColumn.vue' import RenderColumn from './RenderColumn.vue'
import { ElTable } from 'element-plus' // 此处必须注释掉,不然使用该组件的表格样式会错乱
// import { ElTable } from 'element-plus'
import type { TableInstance } from 'element-plus' import type { TableInstance } from 'element-plus'
const tableRef = shallowRef<TableInstance>() const tableRef = shallowRef<TableInstance>()
......
<template>
<div class="page_main h-100">
<!-- 头部插槽 -->
<div v-if="$slots.header" class="page_header">
<slot name="header" />
</div>
<div class="layout_wrap" :style="wrapStyle">
<!-- 左侧插槽 -->
<div
v-if="$slots.page_left"
class="page_left"
:class="[isLeft ? 'page_left_true' : 'page_left_false']"
:style="leftStyle"
>
<slot name="page_left" />
</div>
<!-- 分隔栏 / 折叠按钮 -->
<div v-if="props.isleftshow" class="separation">
<span
class="left_toggle"
:title="isLeft ? '点击收起' : '点击展开'"
@click="leftChange"
>
<i :class="isLeft ? 'el-icon-caret-left' : 'el-icon-caret-right'" />
</span>
</div>
<!-- 右侧插槽 -->
<div v-if="$slots.page_right" class="page_right" :style="rightStyle">
<slot name="page_right" />
</div>
<!-- 默认插槽 -->
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, useAttrs } from 'vue'
import type { CSSProperties } from 'vue'
const props = defineProps({
isleftshow: {
type: Boolean,
default: true,
},
})
// 获取透传的 attrs,并定义类型
interface CustomAttrs {
leftStyle?: CSSProperties
rightStyle?: CSSProperties
style?: CSSProperties
}
const attrs = useAttrs() as CustomAttrs
const isLeft = ref(true)
const leftChange = () => {
isLeft.value = !isLeft.value
}
const leftStyle = computed<CSSProperties>(() => {
const { leftStyle = {} } = attrs
const { width = '200px', ...rest } = leftStyle
return {
...rest,
width: isLeft.value ? width : '1px',
}
})
const rightStyle = computed<CSSProperties>(() => attrs.rightStyle || {})
const wrapStyle = computed<CSSProperties>(() => attrs.style || {})
</script>
<style scoped>
.page_main {
box-sizing: border-box;
display: flex;
flex-direction: column;
margin: 0;
overflow: hidden;
position: relative;
}
.page_main .layout_wrap {
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
position: relative;
}
/* .page_main:not(.mobile) {
margin-right: 54px;
} */
.page_main.row .layout_wrap {
flex-direction: row;
background: #fff;
}
.page_main.wrap .layout_wrap {
background: #fff;
box-sizing: border-box;
border-radius: 10px;
padding-top: 10px;
}
.page_main.wrap .layout_wrap :deep(form:nth-of-type(1)) {
margin-left: 10px;
}
.page_main.mobile {
height: calc(100% - 9.333vw);
}
.page_header {
background: #fff;
padding-top: 10px;
margin-bottom: 10px;
}
.page_left {
position: relative;
display: flex;
flex-direction: column;
width: 200px;
height: 100%;
background: var(--background-color);
box-sizing: border-box;
border-radius: 5px 5px 0 0;
overflow: auto;
border-right: 1px solid #ddd;
}
.separation {
display: none;
position: relative;
width: 1px;
}
.row .separation {
display: block;
}
.separation .left_toggle {
position: absolute;
right: -16px;
width: 17px;
height: 24px;
background: #dedede;
z-index: 1000;
top: 50%;
transform: translateY(-50%);
cursor: pointer;
border-top-right-radius: 5px;
border-bottom-right-radius: 5px;
}
.separation .left_toggle::before {
position: absolute;
width: 0px;
height: 0px;
content: '';
top: 10px;
left: -12px;
pointer-events: none;
transform: rotate(45deg);
border-width: 12px;
border-style: solid;
border-color: rgba(0, 0, 0, 0) #dedede rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
border-radius: 5px;
}
.separation .left_toggle::after {
position: absolute;
width: 0px;
height: 0px;
top: -10px;
left: -12px;
pointer-events: none;
content: '';
transform: rotate(-45deg);
border-width: 12px;
border-style: solid;
border-color: rgba(0, 0, 0, 0) #dedede rgba(0, 0, 0, 0) rgba(0, 0, 0, 0);
border-radius: 5px;
}
.separation .left_toggle i {
display: block;
margin-top: 3px;
pointer-events: none;
}
.separation:hover .left_toggle {
display: block;
}
.page_right {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 10px;
box-sizing: border-box;
overflow: hidden;
position: relative;
}
.page_main:not(.drag) .page_right {
background: var(--background-color);
border-radius: 5px 5px 0 0;
padding-top: 10px;
box-sizing: border-box;
}
/* 深度选择器 */
.page_main .page_right :deep(form:nth-of-type(1)),
.page_main .page_left :deep(form),
.page_main.drag :deep(form) {
padding-left: 10px;
}
.page_main.drag .page_right {
background: transparent;
}
</style>
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router, { registerDynamicRoutes } from './router'
import store from './store' import store from './store'
import './styles/index.scss' import './styles/index.scss'
import roleShow from './utils/roleShow'
import VxeUITable from 'vxe-table' import VxeUITable from 'vxe-table'
import 'vxe-table/lib/style.css' import 'vxe-table/lib/style.css'
import { isPermissionBtn } from '@/utils/index' import { isPermissionBtn } from '@/utils/index'
import { getToken } from '@/api/axios'
import { buildDynamicRoutes } from '@/router/dynamicRoutes'
import { MenuTreeNode } from '@/types/api/role'
// 确保在渲染用户提供的HTML内容时,不会执行任何潜在的恶意脚本,从而提高应用的安全性 // 确保在渲染用户提供的HTML内容时,不会执行任何潜在的恶意脚本,从而提高应用的安全性
import vueDomPurifyHTMLPlugin from 'vue-dompurify-html' import vueDomPurifyHTMLPlugin from 'vue-dompurify-html'
/**
* 应用挂载前还原动态路由:用户已登录时把 localStorage 里的 sysMenus
* 重新挂到 / (Home) 下。必须先于 app.mount(),否则首次路由解析时
* 业务路由还没注册,会出现刷新即 404。
*/
function restoreDynamicRoutes() {
if (!getToken()) return
try {
const raw = localStorage.getItem('sysMenus')
if (!raw) return
const parsed = JSON.parse(raw) as MenuTreeNode[]
if (Array.isArray(parsed) && parsed.length) {
registerDynamicRoutes(buildDynamicRoutes(parsed))
}
} catch {
// ignore
}
}
restoreDynamicRoutes()
const app = createApp(App) const app = createApp(App)
app app
.use(vueDomPurifyHTMLPlugin) .use(vueDomPurifyHTMLPlugin)
.use(router) .use(router)
.use(store) .use(store)
.use(roleShow)
.use(VxeUITable) .use(VxeUITable)
.mount('#app') .mount('#app')
app.config.globalProperties.$isPermissionBtn = isPermissionBtn app.config.globalProperties.$isPermissionBtn = isPermissionBtn
import type { RouteRecordRaw } from 'vue-router'
import type { MenuTreeNode } from '@/types/api/role'
/**
* 动态路由注册:根据登录接口返回的 sysMenus 树生成路由记录,
* 并挂到 / (Home) 之下。
*
* - 叶子节点(path 以 / 开头)查表得到 component,register 到 / 下
* - /system/downloadClient 是特例:保留原 beforeEnter 下载逻辑
* - 找不到 component 的路径直接跳过(防御后端乱返)
*/
type RouteComponent = RouteRecordRaw['component']
type LazyComponent = () => Promise<{ default: RouteComponent } | RouteComponent>
// path -> lazy import。Dashboard 保留为同步导入以便静态路由共用
import Dashboard from '@/views/Dashboard.vue'
export const dynamicComponentMap: Record<string, LazyComponent> = {
'/order/list': () => import('@/views/order/index.vue'),
'/pod-order/list': () => import('@/views/order/pod/index.vue'),
'/pod-cn-order/list': () => import('@/views/order/podCN/index.vue'),
'/pod-us-order/list': () => import('@/views/order/podUs/index.vue'),
'/pod-us-order/orderTracking': () =>
import('@/views/order/orderTracking/index.vue'),
'/pod-us-order/podUsSchedulingRules': () =>
import('@/views/order/podUsSchedulingRules/index.vue'),
'/order/factory-order-new': () =>
import('@/views/order/factoryOrderNew/index.vue'),
'/order/cancel-order-process': () =>
import('@/views/order/cancelOrderProcess/index.vue'),
'/order/allocation-sorting-rule': () =>
import('@/views/order/allocationSortingRule/index.vue'),
'/pod-cn-order/orderTracking': () =>
import('@/views/order/orderTracking/indexcn.vue'),
'/pod-delivery-note/list': () =>
import('@/views/order/pod/deliveryOrderList.vue'),
'/production/complete': () => import('@/views/production/complete.vue'),
'/system/user': () => import('@/views/system/UserPage.vue'),
'/system/role-manage': () => import('@/views/system/RoleManagementPage.vue'),
'/system/external-authorisation': () =>
import('@/views/system/externalAuthorisationPage.vue'),
'/system/customers': () => import('@/views/system/CustomersPage.vue'),
'/supply/out-of-stock-statistics': () =>
import('@/views/supply/OutOfStockStatistics.vue'),
'/supply/stocking-order': () =>
import('@/views/supply/stockingOrder/index.vue'),
'/system/delivery-note': () => import('@/views/DeliveryNotePage.vue'),
'/account/statement-note': () => import('@/views/AccountStatementNote.vue'),
'/account/pod-bill-order': () => import('@/views/podBillOrder/index.vue'),
'/account/pod-us-bill-order': () =>
import('@/views/podUsBillOrder/index.vue'),
'/typesetting-management/list': () =>
import('@/views/typesetting/TypesettingManagement.vue'),
'/logistics/logisticsMethod': () =>
import('@/views/logistics/logisticsMethod.vue'),
'/logistics/logisticsCompany': () =>
import('@/views/logistics/logisticsCompany.vue'),
'/logistics/shippingAddress': () =>
import('@/views/logistics/shippingAddress.vue'),
'/logistics/logisticsQuotation': () =>
import('@/views/logistics/logisticsQuotation.vue'),
'/logistics/declarationRule': () =>
import('@/views/logistics/declarationRule.vue'),
'/logistics/logisticsPartition': () =>
import('@/views/logistics/logisticsPartition.vue'),
'/logistics/logisticsCalculate': () =>
import('@/views/logistics/logisticsCalculate.vue'),
'/logistics/sortingConfiguration': () =>
import('@/views/logistics/sortingConfiguration.vue'),
'/logistics/logisticsTracking': () =>
import('@/views/logistics/logisticsTracking.vue'),
'/warehouse/manage': () => import('@/views/warehouse/manage.vue'),
'/warehouse/receipt-doc': () => import('@/views/warehouse/receiptDoc.vue'),
'/warehouse/issue-doc': () => import('@/views/warehouse/issueDoc.vue'),
'/warehouse/stocking-plan': () =>
import('@/views/warehouse/stockingPlan.vue'),
'/warehouse/stocking-application': () =>
import('@/views/warehouse/stockingApplicationOrder/index.vue'),
'/warehouse/warning': () => import('@/views/warehouse/warning.vue'),
'/warehouse/position': () => import('@/views/warehouse/position.vue'),
'/setting/settingIndex': () => import('@/views/setting/settingIndex.vue'),
'/MyDownloads': () => import('@/views/MyDownloads.vue'),
'/supply/supplierManagement': () =>
import('@/views/supply/supplierManagement/index.vue'),
}
/** dashboard 是登录后的默认页,保留为静态路由 */
export { Dashboard }
/**
* 把后端返回的叶子节点规范化为 router 用的完整路径。
* 优先级:route > path > 顶层名+叶子名/name
* - route 已带 / → 原样返回
* - path 含 _ → 把 _ 替换成 /,并补 / 前缀
* - 都没有 → 用顶层 name + 叶子名兜底
*/
export function buildLeafPath(
leaf: MenuTreeNode,
topName: string = '',
): string {
const r = leaf.route
if (r?.startsWith('/')) return r
if (r) return '/' + r.replace(/_/g, '/')
const p = leaf.path
if (p?.startsWith('/')) return p
if (p?.includes('_')) {
return '/' + p.replace(/_/g, '/')
}
const leafPart = p || leaf.name
if (!leafPart) return ''
if (topName) {
return `/${topName}/${leafPart}`
}
return `/${leafPart}`
}
/** 把所有叶子 path 从 sysMenus 树里展平为路由可用的完整路径 */
export function flattenLeafPaths(nodes: MenuTreeNode[] = []): string[] {
return flattenLeafNodes(nodes).map((n) => n.path)
}
/**
* 展平 sysMenus 的所有叶子节点,输出 { path, title },
* 供 buildDynamicRoutes 注册路由时写入 meta.title。
*/
export function flattenLeafNodes(
nodes: MenuTreeNode[] = [],
): Array<{ path: string; title: string }> {
const out: Array<{ path: string; title: string }> = []
for (const top of nodes) {
const topName = top.name
const walk = (arr: MenuTreeNode[]) => {
for (const n of arr) {
if (n.children?.length) {
walk(n.children)
} else {
const p = buildLeafPath(n, topName)
if (p) out.push({ path: p, title: n.name || n.title })
}
}
}
if (top.children?.length) {
walk(top.children)
} else {
const p = buildLeafPath(top, topName)
if (p) out.push({ path: p, title: top.name || top.title })
}
}
return out
}
/**
* 把 sysMenus 转换成可注册到 router 的 RouteRecordRaw 列表。
*/
export function buildDynamicRoutes(
menus: MenuTreeNode[] = [],
): RouteRecordRaw[] {
const leaves = flattenLeafNodes(menus)
const records: RouteRecordRaw[] = []
for (const { path: p, title } of leaves) {
if (p === '/system/downloadClient') {
// 特殊路由:触发 exe 下载,无组件
records.push({
path: p,
component: { render: () => null } as unknown as RouteComponent,
beforeEnter() {
const a = document.createElement('a')
a.href = '/exeFiles/JomallProductionAssistantSetup.exe'
a.download = '生产客户端.exe'
a.style.display = 'none'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
},
meta: { title: '下载生产客户端' },
} as RouteRecordRaw)
continue
}
const loader = dynamicComponentMap[p]
if (!loader) continue
records.push({
path: p,
name: p,
// 使用异步加载的 component
component: loader as unknown as RouteComponent,
meta: { title },
} as RouteRecordRaw)
}
return records
}
...@@ -3,41 +3,27 @@ import { ...@@ -3,41 +3,27 @@ import {
createWebHistory, createWebHistory,
RouteLocationNormalized, RouteLocationNormalized,
NavigationGuardNext, NavigationGuardNext,
type RouteRecordRaw,
} from 'vue-router' } from 'vue-router'
import { cleanupImagePreview } from '@/utils/hooks/useImagePreview.' import { cleanupImagePreview } from '@/utils/hooks/useImagePreview.'
import Login from '@/views/Login.vue' import Login from '@/views/Login.vue'
import Reset from '@/views/Reset.vue' import Reset from '@/views/Reset.vue'
import Home from '@/views/Home.vue' import Home from '@/views/Home.vue'
import Dashboard from '@/views/Dashboard.vue'
import Error from '@/views/error/404.vue' import Error from '@/views/error/404.vue'
import OrderList from '@/views/order/index.vue' import { Dashboard } from '@/router/dynamicRoutes'
import ProductionComplete from '@/views/production/complete.vue'
import { getToken } from '@/api/axios' import { getToken } from '@/api/axios'
import UserPage from '@/views/system/UserPage.vue' import { getSysPermissionApi } from '@/api/role'
import DeliveryNotePage from '@/views/DeliveryNotePage.vue' import useMenuTreeStore from '@/store/menuTree'
import AccountStatementNote from '@/views/AccountStatementNote.vue'
// import Product from '@/views/product/index.vue' export const HOME_ROUTE_NAME = 'HomeShell'
import PodBillOrder from '@/views/podBillOrder/index.vue'
import PodUsBillOrder from '@/views/podUsBillOrder/index.vue'
import TypeseetingManagement from '@/views/typesetting/TypesettingManagement.vue'
import PodOrderList from '@/views/order/pod/index.vue'
import PodDeliveryNoteList from '@/views/order/pod/deliveryOrderList.vue'
import PodUsOrderList from '@/views/order/podUs/index.vue'
import WarehouseManage from '@/views/warehouse/manage.vue'
import WarehouseWarning from '@/views/warehouse/warning.vue'
import WarehousePosition from '@/views/warehouse/position.vue'
import receiptDoc from '@/views/warehouse/receiptDoc.vue'
import issueDoc from '@/views/warehouse/issueDoc.vue'
import ExternalAuthorisationPage from '@/views/system/externalAuthorisationPage.vue'
import CustomersPage from '@/views/system/CustomersPage.vue'
import stockingPlan from '@/views/warehouse/stockingPlan.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
routes: [ routes: [
{ {
path: '/', path: '/',
name: HOME_ROUTE_NAME,
redirect: '/user/login', redirect: '/user/login',
component: Home, component: Home,
children: [ children: [
...@@ -48,328 +34,6 @@ const router = createRouter({ ...@@ -48,328 +34,6 @@ const router = createRouter({
}, },
component: Dashboard, component: Dashboard,
}, },
// {
// path: '/product',
// meta: {
// title: '商品',
// },
// component: Product,
// },
{
path: '/order/list',
meta: {
title: '定制订单',
},
component: OrderList,
},
{
path: '/pod-order/list',
meta: {
title: 'POD订单',
},
component: PodOrderList,
},
{
path: '/pod-cn-order/list',
meta: {
title: 'POD订单(CN)',
},
component: () => import('@/views/order/podCN/index.vue'),
},
{
path: '/pod-us-order/list',
meta: {
title: 'POD订单(US)',
},
component: PodUsOrderList,
},
{
path: '/pod-us-order/orderTracking',
meta: {
title: 'POD(US)订单跟踪',
},
component: () => import('@/views/order/orderTracking/index.vue'),
},
{
path: '/pod-us-order/podUsSchedulingRules',
meta: {
title: 'POD(US)排单规则',
},
component: () =>
import('@/views/order/podUsSchedulingRules/index.vue'),
},
{
path: '/order/factory-order-new',
meta: {
title: '工厂订单(NEW)',
},
component: () => import('@/views/order/factoryOrderNew/index.vue'),
},
{
path: '/order/cancel-order-process',
meta: {
title: '取消后订单处理',
},
component: () => import('@/views/order/cancelOrderProcess/index.vue'),
},
{
path: '/order/allocation-sorting-rule',
meta: {
title: '配货分拣规则',
},
component: () =>
import('@/views/order/allocationSortingRule/index.vue'),
},
{
path: '/pod-cn-order/orderTracking',
meta: {
title: 'POD(CN)订单跟踪',
},
component: () => import('@/views/order/orderTracking/indexcn.vue'),
},
{
path: '/pod-delivery-note/list',
meta: {
title: 'POD发货单',
},
component: PodDeliveryNoteList,
},
{
path: '/production/complete',
meta: {
title: '生产完成',
},
component: ProductionComplete,
},
{
path: '/system/user',
meta: {
title: '用户管理',
},
component: UserPage,
},
{
path: '/system/downloadClient',
meta: {
title: '下载生产客户端',
},
component: () => {},
beforeEnter() {
// 假设你的下载链接是这个
const downloadLink = '/exeFiles/JomallProductionAssistantSetup.exe'
// 创建一个 a 标签来触发下载
const a = document.createElement('a')
a.href = downloadLink
a.download = '生产客户端.exe' // 你可以指定文件名,或者保持为空来自动使用原始文件名
// 不显示 a 标签,直接触发下载
a.style.display = 'none'
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
},
},
{
path: '/system/external-authorisation',
meta: {
title: '外部授权',
},
component: ExternalAuthorisationPage,
},
{
path: '/system/customers',
meta: {
title: '客户管理',
},
component: CustomersPage,
},
{
path: '/supply/out-of-stock-statistics',
meta: {
title: '缺货统计',
},
component: () => import('@/views/supply/OutOfStockStatistics.vue'),
},
{
path: '/supply/stocking-order',
meta: {
title: '备货订单',
},
component: () => import('@/views/supply/stockingOrder/index.vue'),
},
{
path: '/system/delivery-note',
meta: {
title: '定制发货单',
},
component: DeliveryNotePage,
},
{
path: '/account/statement-note',
meta: {
title: '定制对账单',
},
component: AccountStatementNote,
},
{
path: '/account/pod-bill-order',
meta: {
title: 'POD(CN)对账单',
},
component: PodBillOrder,
},
{
path: '/account/pod-us-bill-order',
meta: {
title: 'POD(US)对账单',
},
component: PodUsBillOrder,
},
{
path: '/typesetting-management/list',
meta: {
title: '打版管理',
},
component: TypeseetingManagement,
},
{
path: '/logistics/logisticsMethod',
meta: {
title: '物流方式',
},
component: () => import('@/views/logistics/logisticsMethod.vue'),
},
{
path: '/logistics/logisticsCompany',
meta: {
title: '物流公司',
},
component: () => import('@/views/logistics/logisticsCompany.vue'),
},
{
path: '/logistics/shippingAddress',
meta: {
title: '发货地址',
},
component: () => import('@/views/logistics/shippingAddress.vue'),
},
{
path: '/logistics/logisticsQuotation',
meta: {
title: '物流报价',
},
component: () => import('@/views/logistics/logisticsQuotation.vue'),
},
{
path: '/logistics/declarationRule',
meta: {
title: '申报规则',
},
component: () => import('@/views/logistics/declarationRule.vue'),
},
{
path: '/logistics/logisticsPartition',
meta: {
title: '物流分区',
},
component: () => import('@/views/logistics/logisticsPartition.vue'),
},
{
path: '/logistics/logisticsCalculate',
meta: {
title: '运费试算',
},
component: () => import('@/views/logistics/logisticsCalculate.vue'),
},
{
path: '/logistics/sortingConfiguration',
meta: {
title: '分拣配置',
},
component: () => import('@/views/logistics/sortingConfiguration.vue'),
},
{
path: '/logistics/logisticsTracking',
meta: {
title: '物流跟踪',
},
component: () => import('@/views/logistics/logisticsTracking.vue'),
},
{
path: '/warehouse/manage',
meta: {
title: '仓库管理',
},
component: WarehouseManage,
},
{
path: '/warehouse/receipt-doc',
meta: {
title: '入库单',
},
component: receiptDoc,
},
{
path: '/warehouse/issue-doc',
meta: {
title: '出库单',
},
component: issueDoc,
},
{
path: '/warehouse/stocking-plan',
meta: {
title: '备货计划',
},
component: stockingPlan,
},
{
path: '/warehouse/stocking-application',
meta: {
title: '入库申请单',
},
component: () =>
import('@/views/warehouse/stockingApplicationOrder/index.vue'),
},
{
path: '/warehouse/warning',
meta: {
title: '仓库预警',
},
component: WarehouseWarning,
},
{
path: '/warehouse/position',
meta: {
title: '库位管理',
},
component: WarehousePosition,
},
{
path: '/setting/settingIndex',
meta: {
title: '工厂设置',
},
component: () => import('@/views/setting/settingIndex.vue'),
},
{
path: '/MyDownloads',
meta: {
title: '我的下载',
},
name: 'MyDownloads',
component: () => import('@/views/MyDownloads.vue'),
},
{
path: '/supply/supplierManagement',
meta: {
title: '供应商管理',
},
name: 'supplierManagement',
component: () =>
import('@/views//supply/supplierManagement/index.vue'),
},
], ],
}, },
// 登录 // 登录
...@@ -389,8 +53,26 @@ const router = createRouter({ ...@@ -389,8 +53,26 @@ const router = createRouter({
}, },
], ],
}) })
/**
* 把 sysMenus 对应的业务路由全部挂到 / (Home) 下。
* - 静态路由(login/reset/404/dashboard)永远存在
* - 业务路由由后端菜单决定
* - 重复注册前先 removeRoute(path) 防止堆叠
*/
export function registerDynamicRoutes(records: RouteRecordRaw[]) {
for (const r of records) {
if (router.hasRoute(r.path)) {
router.removeRoute(r.path)
}
router.addRoute(HOME_ROUTE_NAME, r)
}
}
let pendingReqMenu = ''
router.beforeEach( router.beforeEach(
( async (
to: RouteLocationNormalized, to: RouteLocationNormalized,
_from: RouteLocationNormalized, _from: RouteLocationNormalized,
next: NavigationGuardNext, next: NavigationGuardNext,
...@@ -405,7 +87,31 @@ router.beforeEach( ...@@ -405,7 +87,31 @@ router.beforeEach(
next({ path: '/user/login' }) next({ path: '/user/login' })
return return
} }
// 把 getSysPermissionApi 放到 beforeEach 并 await:
// 进入新页面前先把 buttons 清空,避免上一页按钮残留。
if (token && to.path !== '/user/login') {
const reqMenu = (to.meta?.permissionRoute as string) || to.path
if (reqMenu && reqMenu !== pendingReqMenu) {
pendingReqMenu = reqMenu
const menuTreeStore = useMenuTreeStore()
menuTreeStore.setReqMenu({ buttons: [] })
try {
const res = await getSysPermissionApi(reqMenu)
if (pendingReqMenu === reqMenu && res?.data) {
menuTreeStore.setReqMenu(res.data)
}
} catch {
if (pendingReqMenu === reqMenu) {
pendingReqMenu = ''
}
// 静默失败:roleShow 在 buttons 为空时直接返回 false
}
}
}
next() next()
}, },
) )
export default router export default router
import type { MenuTreeNode } from '@/types/api/role'
export interface MenuItem { export interface MenuItem {
index: string index: string
id: number id: number
...@@ -5,6 +7,29 @@ export interface MenuItem { ...@@ -5,6 +7,29 @@ export interface MenuItem {
children?: MenuItem[] children?: MenuItem[]
} }
/**
* 把后端返回的 MenuTreeNode 树转换为导航菜单使用的 MenuItem 树。
*/
export function transformMenuTree(
nodes: MenuTreeNode[] = [],
topName: string = '/',
): MenuItem[] {
return nodes.map((n) => {
// 只有"无 children 且 route 是真实路径"才是可点的叶子
const hasRealRoute = Boolean(n.route) && n.route !== '/'
const isLeaf = !n.children?.length && hasRealRoute
return {
id: n.id,
label: n.name || n.title,
// 叶子:直接用 route;非叶子:
index: isLeaf ? n.route! : topName,
children: n.children?.length
? transformMenuTree(n.children, topName)
: undefined,
}
})
}
const menu: MenuItem[] = [ const menu: MenuItem[] = [
{ {
index: '/dashboard', index: '/dashboard',
...@@ -251,6 +276,7 @@ const menu: MenuItem[] = [ ...@@ -251,6 +276,7 @@ const menu: MenuItem[] = [
children: [ children: [
{ index: '/setting/settingIndex', id: 821, label: '工厂设置' }, { index: '/setting/settingIndex', id: 821, label: '工厂设置' },
{ index: '/system/downloadClient', id: 822, label: '下载生产客户端' }, { index: '/system/downloadClient', id: 822, label: '下载生产客户端' },
{ index: '/system/role-manage', id: 823, label: '功能角色管理' },
], ],
}, },
], ],
......
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { MenuTreeNode } from '@/types/api/role'
const STORAGE_KEY = 'sysMenus'
function readFromStorage(): MenuTreeNode[] {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return []
const parsed = JSON.parse(raw)
return Array.isArray(parsed) ? (parsed as MenuTreeNode[]) : []
} catch {
return []
}
}
function writeToStorage(data: MenuTreeNode[]) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data))
} catch {
// ignore quota errors
}
}
const useMenuTreeStore = defineStore('menuTree', () => {
const menusTree = ref<MenuTreeNode[]>(readFromStorage()) // 功能按钮里的菜单(带左边栏和按钮)
const menusTreeNoBtn = ref<MenuTreeNode[]>(readFromStorage()) // 专门渲染三级菜单(带左边栏和按钮)
const reqMenu = ref<{ buttons: string[] }>({
buttons: [],
})
const setMenuTree = (data: MenuTreeNode[]) => {
menusTree.value = data
// writeToStorage(data) // 这里不写入本地存储,避免覆盖 menusTreeNoBtn
}
const clearMenuTree = () => {
menusTree.value = []
menusTreeNoBtn.value = []
reqMenu.value = { buttons: [] }
localStorage.removeItem(STORAGE_KEY)
}
const setMenuTreeNoBtn = (data: MenuTreeNode[]) => {
menusTreeNoBtn.value = data
writeToStorage(data)
}
const setReqMenu = (data: { buttons: string[] }) => {
reqMenu.value = data
}
return {
menusTree,
menusTreeNoBtn,
reqMenu,
setMenuTree,
clearMenuTree,
setReqMenu,
setMenuTreeNoBtn,
}
})
export default useMenuTreeStore
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ElMessage } from 'element-plus'
import { LoginReq, SysUser } from '@/types/api/auth' import { LoginReq, SysUser } from '@/types/api/auth'
import { loginApi } from '@/api/auth' import { loginApi } from '@/api/auth'
import { setToken } from '@/api/axios' import { setToken } from '@/api/axios'
import useMenuTreeStore from '@/store/menuTree'
import { buildDynamicRoutes } from '@/router/dynamicRoutes'
import { registerDynamicRoutes } from '@/router'
import router from '@/router' import router from '@/router'
export interface AppStoreState { export interface AppStoreState {
...@@ -30,7 +34,16 @@ const useUserStore = defineStore('user', { ...@@ -30,7 +34,16 @@ const useUserStore = defineStore('user', {
async login(data: LoginReq) { async login(data: LoginReq) {
try { try {
const resp = await loginApi(data) const resp = await loginApi(data)
console.log(33, resp) const { sysMenus } = resp.data
if (!sysMenus || sysMenus.length === 0) {
ElMessage.error('未分配菜单权限,请联系管理员')
this.logout()
return
}
// 菜单树
useMenuTreeStore().setMenuTreeNoBtn(sysMenus)
// 根据 sysMenus 动态注册路由到 / (Home) 下
registerDynamicRoutes(buildDynamicRoutes(sysMenus))
this.setUser(resp.data.sysUser) this.setUser(resp.data.sysUser)
setToken(resp.data.token) setToken(resp.data.token)
...@@ -47,6 +60,11 @@ const useUserStore = defineStore('user', { ...@@ -47,6 +60,11 @@ const useUserStore = defineStore('user', {
async logout() { async logout() {
this.setUser(undefined) this.setUser(undefined)
setToken('') setToken('')
// 清空动态菜单和路由
useMenuTreeStore().clearMenuTree()
// 清掉登录时写入的其他鉴权相关缓存
localStorage.removeItem('baseExternalAccountTypes')
// localStorage.removeItem('factory_code')
}, },
}, },
}) })
......
.el-table thead { .el-table thead {
color: #333;
font-size: 14px; font-size: 14px;
height: 50px; height: 50px;
min-height: 50px; min-height: 50px;
} }
.el-table .cell {
white-space: nowrap;
}
.el-table thead th.el-table__cell { .el-table thead th.el-table__cell {
color: #333;
background-color: #fafafa; background-color: #fafafa;
} }
.el-table .cell {
white-space: nowrap;
}
.el-table__row { .el-table__row {
height: 40px; height: 40px;
min-height: 40px; min-height: 40px;
......
import type { MenuTreeNode } from './role'
export interface LoginReq { export interface LoginReq {
factoryCode: string factoryCode: string
account: string account: string
...@@ -13,6 +15,7 @@ export interface LoginResp { ...@@ -13,6 +15,7 @@ export interface LoginResp {
export interface Root { export interface Root {
sysUser: SysUser sysUser: SysUser
token: string token: string
sysMenus: MenuTreeNode[]
} }
export interface SysUser { export interface SysUser {
...@@ -32,6 +35,7 @@ export interface Factory { ...@@ -32,6 +35,7 @@ export interface Factory {
bianma: string bianma: string
title: string title: string
typeId: number typeId: number
type?: number
name: string name: string
mobile: string mobile: string
goodsNumber: number goodsNumber: number
......
export interface RoleItem {
id: number;
roleGroup: string;
name: string;
type: string;
createTime: string;
}
export interface RoleFormPayload {
id?: number | string
name: string
roleGroup?: string
[key: string]: unknown
}
export interface RoleGroup {
label: string
list: RoleItem[]
}
export interface RoleUser {
id: number
account: string
}
export interface RolePermsData {
dataRoleIds?: number[]
menuIds?: number[]
[key: string]: unknown
}
export interface MenuTreeNode {
id: number;
name: string;
title: string;
path: string;
url: string;
route: string;
pid: number;
type: number;
sortNo: number;
enable: boolean;
indeter: boolean;
includeNamespace: string;
excludeNamespace: string;
children: MenuTreeNode[];
}
\ No newline at end of file
...@@ -7,6 +7,7 @@ export interface userData { ...@@ -7,6 +7,7 @@ export interface userData {
factoryCode?: string factoryCode?: string
status?: number status?: number
createTime?: string createTime?: string
firstSupperMark?: boolean
} }
export interface userSearchForm { export interface userSearchForm {
...@@ -19,6 +20,8 @@ export interface UserEditForm { ...@@ -19,6 +20,8 @@ export interface UserEditForm {
supperMark: string | number supperMark: string | number
account: string account: string
status: string | number status: string | number
roleIds: number[] | string
firstSupperMark?: boolean
} }
export interface customSearchForm { export interface customSearchForm {
sku?: string sku?: string
......
...@@ -73,7 +73,7 @@ export interface ExportInWarehouseInfo extends warehouseSearchForm { ...@@ -73,7 +73,7 @@ export interface ExportInWarehouseInfo extends warehouseSearchForm {
} }
export interface InterWarehouseTree { export interface InterWarehouseTree {
id?: number id?: number
code?: string | null code: string | null
name?: string name?: string
countQuantity?: string | null countQuantity?: string | null
children: InterWarehouseTree[] | null children: InterWarehouseTree[] | null
......
...@@ -64,4 +64,4 @@ export function downloadByUrl(url: string, filename?: string) { ...@@ -64,4 +64,4 @@ export function downloadByUrl(url: string, filename?: string) {
document.body.appendChild(a) document.body.appendChild(a)
a.click() a.click()
document.body.removeChild(a) document.body.removeChild(a)
} }
\ No newline at end of file
import { App } from 'vue'
import useMenuTreeStore from '@/store/menuTree'
/**
* 判断当前页面是否有指定按钮 / 权限 code。
* 既能在 <template> 里直接用(通过 app.config.globalProperties),
* 也能在 <script setup> 里 import 使用。
*/
export function roleShow(name?: string | string[] | null): boolean {
// 必须在 install() 里调用 useMenuTreeStore(),
// 不能在模块顶层调用 —— 否则 main.ts 在 app.use(store) 安装 pinia 之前
// import 这文件就会触发 "getActivePinia()" was called but there was no active Pinia
if (!name) return false
const menuTreeStore = useMenuTreeStore()
const roles = menuTreeStore.reqMenu.buttons
if (!roles) return false
if (Array.isArray(name)) {
return name.some((key) => roles.includes(key))
}
return roles.includes(name)
}
export default {
// 已经在 main.ts 中注册了 roleShow
// 其中左边栏里的 statusCode 是后端返回的,在sass-manage里添加时要和后端返回的statusCode一致
install(app: App) {
app.config.globalProperties.roleShow = roleShow
},
}
<template> <template>
<div class="home"> <div class="home">
<!-- 顶部导航栏 --> <!-- 顶部导航栏:key 随用户变化,账号切换时强制重挂载,菜单名称才会更新 -->
<NavMenu /> <NavMenu :key="navMenuKey" />
<div class="container"> <div class="container">
<RouterView /> <!-- key 随登录用户变化,强制重渲染避免切换账号后页面残留旧数据 -->
<RouterView :key="routerViewKey" />
</div> </div>
<SideBar /> <SideBar />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'
import NavMenu from '@/components/NavMenu.vue' import NavMenu from '@/components/NavMenu.vue'
import SideBar from '@/components/SideBar.vue' import SideBar from '@/components/SideBar.vue'
import useUserStore from '@/store/user'
const userStore = useUserStore()
const userKey = computed(() => userStore.user?.id ?? 'guest')
const routerViewKey = userKey
const navMenuKey = userKey
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.home { .home {
......
<template>
<div class="user-page flex-column card h-100 overflow-hidden">
<div class="header-filter-form">
<ElForm :model="searchForm" inline>
<ElFormItem label="用户名" prop="account">
<ElInput
v-model="searchForm.account"
placeholder="请输入用户名"
clearable
style="width: 200px"
/>
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElSelect
v-model="searchForm.status"
clearable
style="width: 200px"
placeholder="请选择状态"
>
<ElOption label="启用" :value="1"></ElOption>
<ElOption label="禁用" :value="0"></ElOption>
</ElSelect>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" @click="search">查询</ElButton>
</ElFormItem>
<ElFormItem>
<ElButton @click="resetSearchForm">重置</ElButton>
</ElFormItem>
<ElFormItem>
<ElButton type="success" @click="addUser">新增</ElButton>
</ElFormItem>
<ElFormItem>
<ElButton type="danger" @click="deleteUser">删除</ElButton>
</ElFormItem>
</ElForm>
</div>
<div class="user-content flex-1 flex-column overflow-hidden">
<div class="user-list flex-1 overflow-hidden">
<ElTable
:data="tableData"
default-expand-all
style="width: 100%; height: 100%"
@selection-change="handleSelectionChange"
>
<ElTableColumn
type="selection"
header-align="center"
align="center"
width="55"
/>
<ElTableColumn
type="index"
header-align="center"
align="center"
label="序号"
width="55"
/>
<ElTableColumn prop="account" header-align="center" label="用户名">
<template #default="scope">
<div>
<span
>{{ scope.row.account }}
<ElTooltip
v-if="scope.row.supperMark === 1"
content="超级管理员"
placement="top"
>
<Icon
v-if="scope.row.supperMark === 1"
name="zu54"
style="vertical-align: super; cursor: pointer"
>
</Icon>
</ElTooltip>
</span>
</div>
</template>
</ElTableColumn>
<ElTableColumn
prop="factoryCode"
header-align="center"
label="工厂"
align="center"
/>
<ElTableColumn
prop="status"
header-align="center"
align="center"
label="状态"
>
<template #default="scope">
<el-tooltip
:content="scope.row.status ? '启用' : '禁用'"
placement="top"
>
<ElSwitch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
@change="(e:number) => onChangeStatus(e, scope.row)"
></ElSwitch>
</el-tooltip>
</template>
</ElTableColumn>
<ElTableColumn
label="操作"
width="130"
header-align="center"
align="center"
>
<template #default="scope">
<el-icon
size="24"
title="编辑"
color="#EF6C00"
style="cursor: pointer; vertical-align: middle"
@click="editUser(scope.row)"
>
<Edit />
</el-icon>
<el-icon
size="24"
title="重置密码"
color="#67C23A"
style="cursor: pointer; vertical-align: middle"
@click.stop="resetPwd(scope.row)"
><RefreshLeft
/></el-icon>
</template>
</ElTableColumn>
</ElTable>
</div>
<ElPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[100, 200, 300, 400, 500]"
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
style="margin: 10px auto 0; text-align: right"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
></ElPagination>
</div>
</div>
<ElDialog
v-model="dialogVisible"
:title="editId ? '编辑用户' : '新增用户'"
width="600px"
:close-on-click-modal="false"
@opened="onOpenedUserForm"
>
<div class="dialog-form">
<ElForm
ref="editFormRef"
:model="editForm"
:rules="rules"
label-width="90px"
>
<ElFormItem label="用户名" prop="account">
<ElInput
v-model="editForm.account"
placeholder="请输入用户名"
clearable
/>
</ElFormItem>
<ElFormItem v-if="!editId" label="密码" prop="password">
<ElInput
v-model="editForm.password"
placeholder="请输入密码"
clearable
type="password"
autocomplete="off"
:show-password="true"
/>
</ElFormItem>
<ElFormItem label="状态" prop="status">
<ElCheckbox
v-model="editForm.status"
label="启用"
true-value="1"
false-value="0"
></ElCheckbox>
</ElFormItem>
<ElFormItem label="角色" prop="supperMark">
<ElCheckbox
v-model="editForm.supperMark"
label="超级管理员"
true-value="1"
false-value="0"
></ElCheckbox>
</ElFormItem>
</ElForm>
</div>
<template #footer>
<div class="dialog-footer">
<ElButton @click="dialogVisible = false"> 取消 </ElButton>
<ElButton type="primary" @click="save">保存</ElButton>
</div>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import {
getUserList,
addUserApi,
updateUserApi,
deleteUserApi,
getDetailsByIdApi,
changeUserStatusApi,
resetPasswordApi,
} from '@/api/auth'
import Icon from '@/components/Icon.vue'
import { UserEditForm, userData, userSearchForm } from '@/types/api/user'
import usePageList from '@/utils/hooks/usePageList'
import { useValue } from '@/utils/hooks/useValue'
import { showConfirm } from '@/utils/ui'
import { Edit, RefreshLeft } from '@element-plus/icons-vue'
import type { FormRules } from 'element-plus'
import { reactive, ref } from 'vue'
const [searchForm, resetSearchForm] = useValue<userSearchForm>({})
const [editForm, resetEditForm] = useValue<UserEditForm>({
account: '',
password: '',
supperMark: '0',
status: '1',
})
const {
currentPage,
pageSize,
total,
data: tableData,
refresh: search,
onCurrentPageChange: handleCurrentChange,
onPageSizeChange: handleSizeChange,
} = usePageList({
query: (page, pageSize) =>
getUserList(searchForm.value, page, pageSize).then((res) => res.data),
})
const dialogVisible = ref(false)
const editFormRef = ref()
const selection = ref<userData[]>([])
const rules = reactive<FormRules<UserEditForm>>({
account: [
{
required: true,
message: '请输入用户名',
},
],
password: [
{
required: true,
message: '请输入密码',
},
],
})
const resetPwd = async (row: userData) => {
try {
await showConfirm(`确认要重置用户${row.account}的密码?`, {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await resetPasswordApi(row.id)
await ElMessageBox.alert('重置成功\n\r 新密码为:' + res.data.passWord, {
confirmButtonText: '确定',
type: 'warning',
})
search()
} catch (e) {
search()
}
}
const editId = ref<number | undefined>(undefined)
const addUser = () => {
editId.value = undefined
dialogVisible.value = true
resetEditForm()
}
const deleteUser = async () => {
if (!selection.value.length) {
return ElMessage({
message: '请选择用户',
type: 'warning',
offset: window.innerHeight / 2,
})
}
try {
await showConfirm('是否删除用户', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const ids = selection.value.map((item) => item.id).join(',')
await deleteUserApi(ids)
ElMessage({
message: '删除成功',
type: 'success',
offset: window.innerHeight / 2,
})
search()
} catch (e) {
search()
// showError(e)
}
}
const editUser = async (item: userData) => {
editId.value = item.id
try {
const res = await getDetailsByIdApi(item.id)
res.data.supperMark = res.data.supperMark + ''
res.data.status = res.data.status + ''
editForm.value = res.data
dialogVisible.value = true
} catch (e) {
//showError(e)
}
}
const save = async () => {
try {
await editFormRef.value.validate()
} catch {
return
}
try {
if (!editId.value) {
await addUserApi({
...editForm.value,
supperMark: Number(editForm.value.supperMark),
status: Number(editForm.value.status),
})
} else {
await updateUserApi({
...editForm.value,
supperMark: Number(editForm.value.supperMark),
status: Number(editForm.value.status),
})
}
ElMessage({
message: '保存成功',
type: 'success',
offset: window.innerHeight / 2,
})
dialogVisible.value = false
search()
} catch (e) {
return
}
}
const onOpenedUserForm = async () => {
editFormRef.value?.clearValidate()
}
const handleSelectionChange = (s: userData[]) => {
selection.value = s
}
const onChangeStatus = async (value: number, item: userData) => {
try {
const res = await changeUserStatusApi(value, item.id)
ElMessage({
message: res.message,
type: 'success',
offset: window.innerHeight / 2,
})
search()
} catch (e) {
//showError(e)
}
}
</script>
<style lang="scss" scoped>
.header-filter-form {
margin-bottom: 20px;
:deep(.el-form-item) {
margin-right: 14px;
margin-bottom: 10px;
}
}
.user-operate-btn {
margin-bottom: 10px;
}
.dialog-footer {
text-align: center;
}
</style>
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
} from '@/api/factoryOrderNew' } from '@/api/factoryOrderNew'
import type { StatusTreeNode } from '@/types/api/order/factoryOrderNew' import type { StatusTreeNode } from '@/types/api/order/factoryOrderNew'
import type { SearchForm } from '@/types/api/factoryOrderNew' import type { SearchForm } from '@/types/api/factoryOrderNew'
import { roleShow } from '@/utils/roleShow'
interface UseOrderStatusTreeOptions { interface UseOrderStatusTreeOptions {
getQueryPayload: () => Record<string, unknown> getQueryPayload: () => Record<string, unknown>
...@@ -15,11 +16,7 @@ interface UseOrderStatusTreeOptions { ...@@ -15,11 +16,7 @@ interface UseOrderStatusTreeOptions {
} }
export function useOrderStatusTree(options: UseOrderStatusTreeOptions) { export function useOrderStatusTree(options: UseOrderStatusTreeOptions) {
const { const { getQueryPayload, currentPage, pageSize } = options
getQueryPayload,
currentPage,
pageSize,
} = options
const cardLayoutStatuses = [ const cardLayoutStatuses = [
'PENDING_SCHEDULE', 'PENDING_SCHEDULE',
...@@ -44,7 +41,12 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) { ...@@ -44,7 +41,12 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) {
acceptedOutOfStockCount: 0, acceptedOutOfStockCount: 0,
}) })
const suspendedTabs = ref([ const suspendedTabs = ref([
{ label: '客户拦截-取消订单', key: 'customerInterceptCount', value: 1, count: 0 }, {
label: '客户拦截-取消订单',
key: 'customerInterceptCount',
value: 1,
count: 0,
},
{ label: '地址异常', key: 'addressExceptionCount', value: 2, count: 0 }, { label: '地址异常', key: 'addressExceptionCount', value: 2, count: 0 },
{ label: '素材异常', key: 'materialExceptionCount', value: 3, count: 0 }, { label: '素材异常', key: 'materialExceptionCount', value: 3, count: 0 },
{ label: '其他', key: 'otherReasonCount', value: 4, count: 0 }, { label: '其他', key: 'otherReasonCount', value: 4, count: 0 },
...@@ -130,12 +132,37 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) { ...@@ -130,12 +132,37 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) {
console.error(e) console.error(e)
} }
} }
const loadStatusTreeCounts = async () => { const loadStatusTreeCounts = async () => {
try { try {
const res = await getPodOrderStateGroupListApi() const res = await getPodOrderStateGroupListApi()
const GROUP_MARK = new Set(['PICKING']) // 所有分组容器
const filterTree = (nodes: StatusTreeNode[]): StatusTreeNode[] => {
const result: StatusTreeNode[] = []
for (const node of nodes) {
let filteredChildren: StatusTreeNode[] = []
if (node.children) {
filteredChildren = filterTree(node.children)
}
const hasPermission = roleShow(node.status)
const isGroupContainer = GROUP_MARK.has(node.status)
if (
hasPermission ||
(isGroupContainer && filteredChildren.length > 0)
) {
result.push({ ...node, children: filteredChildren })
}
}
return result
}
const filteredData = filterTree(res.data)
statusTree.value = [ statusTree.value = [
{ status: 'ALL', statusName: '全部', quantity: 0, children: res.data }, {
status: 'ALL',
statusName: '全部',
quantity: 0,
children: filteredData,
},
] ]
nextTick(() => { nextTick(() => {
treeRef.value?.setCurrentKey(status.value, true) treeRef.value?.setCurrentKey(status.value, true)
...@@ -145,7 +172,6 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) { ...@@ -145,7 +172,6 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) {
} }
} }
const handlePendingAcceptTabClick = ( const handlePendingAcceptTabClick = (
tab: 'PENDING_RECEIVE' | 'ACCEPT_FAIL_OUT_OF_STOCK', tab: 'PENDING_RECEIVE' | 'ACCEPT_FAIL_OUT_OF_STOCK',
refreshTableList: () => void, refreshTableList: () => void,
...@@ -180,6 +206,6 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) { ...@@ -180,6 +206,6 @@ export function useOrderStatusTree(options: UseOrderStatusTreeOptions) {
getCancelledCounts, getCancelledCounts,
loadStatusTreeCounts, loadStatusTreeCounts,
handlePendingAcceptTabClick, handlePendingAcceptTabClick,
toggleExpand toggleExpand,
} }
} }
<template>
<layout class="row" :right-style="{ paddingTop: '0px' }" :isleftshow="false">
<template #page_left>
<div style="padding: 10px">
<ElInput
v-model="roleName"
placeholder="角色名称"
size="small"
clearable
@change="searchRole"
></ElInput>
</div>
<ElScrollbar style="height: calc(100% - 40px)">
<ElCollapse v-model="activeNames">
<ElCollapseItem
v-for="(group, key) in showGroups"
:key="key"
:name="`${key + 1}`"
:title="group.label"
>
<div
v-for="(item, index) in group.list"
:key="index"
class="group_item"
:class="{ active: activeRoleId === item.id }"
@click="setActiveRole(item)"
>
<span>{{ item.name }}</span>
</div>
</ElCollapseItem>
</ElCollapse>
</ElScrollbar>
</template>
<template #page_right>
<div class="page_content">
<div
style="
height: 40px;
display: flex;
box-sizing: border-box;
justify-content: space-between;
padding: 10px 10px 0;
"
>
<div v-if="currentRole.name" style="line-height: 40px">
<span style="font-weight: bold; font-size: 18px"> 角色名称: </span>
<span style="font-weight: bold; font-size: 20px; color: red">
{{ currentRole.name }}
</span>
</div>
<div>
<ElButton
v-if="activeRoleId != '1'"
:disabled="!currentRole.name"
type="primary"
@click="editRole()"
>
编辑
</ElButton>
<ElButton
v-if="activeRoleId != '1'"
:disabled="!currentRole.name"
type="success"
@click="copyRole()"
>
复制
</ElButton>
<ElButton
v-if="activeRoleId != '1'"
:disabled="!currentRole.name"
type="danger"
@click="removeRole()"
>
删除
</ElButton>
<ElButton type="primary" @click="addRole()"> 添加 </ElButton>
<ElButton
:disabled="!currentRole.name"
type="success"
@click="saveSelect()"
>
保存
</ElButton>
</div>
</div>
<div v-if="!currentRole.name" class="no_role_tip">
<span style="color: red; font-weight: bold; font-size: 18px"
>请先添加角色吧!</span
>
</div>
<ElTabs v-else v-model="tabActive" @tab-click="tabClick">
<ElTabPane label="功能权限" name="second">
<div
style="
padding: 5px 10px 2px;
height: 100%;
box-sizing: border-box;
"
>
<Linkage
:menu-list="menuList"
:is-active="tabActive === 'second'"
>
<template #default="{ children, name, id, indeter }">
<ElCheckboxGroup v-model="menuCheckList" style="height: 100%">
<div data-level="一级" class="level_one">
<div class="menu_name">
<ElCheckbox
:value="id"
:indeterminate="indeter"
@change="
(v: boolean | string | number) =>
checkChange(
{ id, indeter, children } as unknown as MenuTreeNode,
v,
)
"
>
{{ name }}
</ElCheckbox>
</div>
<div class="menu_content" style="flex: 1">
<div
v-for="item1 in (children as MenuTreeNode[] | undefined) || []"
:key="item1.id"
class="level_two"
data-level="二级"
>
<div class="menu_name">
<ElCheckbox
:value="item1.id"
:indeterminate="item1.indeter"
@change="
(v: boolean | string | number) =>
checkChange(item1, v)
"
>
{{ item1.name }}
</ElCheckbox>
</div>
<div
v-if="item1.children && item1.children.length > 0"
class="menu_content"
style="flex: 1"
>
<div
v-for="item2 in item1.children"
:key="item2.id"
data-level="三级"
:class="{
level_three: item2.type == 0,
}"
:style="{
display: item2.type == 1 ? 'inline' : 'flex',
}"
>
<div
class="menu_name"
:class="item2.type === 2 ? 'status-bar' : ''"
>
<ElCheckbox
:value="item2.id"
:indeterminate="item2.indeter"
@change="
(v: boolean | string | number) =>
checkChange(item2, v)
"
>
{{ item2.name }}
</ElCheckbox>
</div>
<div
v-if="
item2.children && item2.children.length > 0
"
class="menu_content"
style="flex: 1"
>
<div
v-for="item3 in item2.children"
:key="item3.id"
data-level="四级"
:style="{
display:
item3.children &&
item3.children[0] &&
item3.children[0].type == 1
? 'flex'
: 'inline',
}"
>
<div
class="menu_name"
:class="
item3.type === 2 ? 'status-bar' : ''
"
>
<ElCheckbox
:value="item3.id"
:indeterminate="false"
@change="
(v: boolean | string | number) =>
checkChange(item3, v)
"
>
{{ item3.name }}
</ElCheckbox>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</ElCheckboxGroup>
</template>
</Linkage>
</div>
</ElTabPane>
<ElTabPane label="角色用户" name="third">
<ul class="user_wrap">
<li v-for="(item, index) in userLists" :key="index">
<span style="color: #333">{{ item.account }}</span>
</li>
</ul>
</ElTabPane>
</ElTabs>
</div>
</template>
</layout>
<ElDialog
v-model="editVisible"
:title="isEdit ? '编辑角色' : '添加角色'"
width="500px"
:close-on-click-modal="false"
@closed="onRoleDialogClosed"
>
<ElForm
ref="editFormRef"
:model="editForm"
:rules="roleFormRules"
label-width="90px"
>
<ElFormItem label="角色名称" prop="name">
<ElInput
v-model="editForm.name"
placeholder="请输入角色名称"
maxlength="15"
show-word-limit
clearable
/>
</ElFormItem>
<ElFormItem label="角色分组">
<ElInput
v-model="editForm.roleGroup"
placeholder="请输入角色分组"
maxlength="15"
show-word-limit
clearable
/>
</ElFormItem>
</ElForm>
<template #footer>
<div style="text-align: right">
<ElButton @click="editVisible = false">取消</ElButton>
<ElButton type="primary" :loading="saveLoading" @click="saveRole">
保存
</ElButton>
</div>
</template>
</ElDialog>
</template>
<script setup lang="ts">
import layout from '@/components/leftRightLayout.vue'
import { computed, onMounted, reactive, ref } from 'vue'
import {
addRoleApi,
copyRoleApi,
deleteRoleApi,
getRoleListApi,
getRolePermsApi,
getRoleUsersApi,
getSysMenusTreeApi,
saveRoleAuthApi,
updateRoleApi,
} from '@/api/role'
import useMenuTreeStore from '@/store/menuTree'
import { showConfirm } from '@/utils/ui'
import type {
RoleItem,
RoleFormPayload,
RoleGroup,
RoleUser,
MenuTreeNode,
} from '@/types/api/role'
import type { FormInstance, FormRules } from 'element-plus'
import Linkage from './components/linkage.vue'
const roleName = ref('')
const activeNames = ref<string>('')
const activeRoleId = ref<string | number>('')
const tabActive = ref<'second' | 'third'>('second')
const currentRole = ref<RoleItem>({} as RoleItem)
const menuCheckList = ref<number[]>([])
const groupLists = ref<RoleItem[]>([])
const showGroups = ref<RoleGroup[]>([])
const userLists = ref<RoleUser[]>([])
const menuTreeStore = useMenuTreeStore()
const menuList = computed<MenuTreeNode[]>(() => menuTreeStore.menusTree)
// 新增/编辑角色弹窗
const editVisible = ref(false)
const isEdit = ref(false)
const saveLoading = ref(false)
const editFormRef = ref<FormInstance>()
const editForm = reactive<RoleFormPayload>({
id: undefined,
name: '',
roleGroup: '',
})
const roleFormRules = reactive<FormRules<RoleFormPayload>>({
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
})
function addRole() {
isEdit.value = false
editForm.id = undefined
editForm.name = ''
editForm.roleGroup = ''
editVisible.value = true
}
function editRole() {
isEdit.value = true
editForm.id = currentRole.value.id
editForm.name = currentRole.value.name || ''
editForm.roleGroup = currentRole.value.roleGroup || ''
editVisible.value = true
}
async function saveRole() {
if (!editFormRef.value) return
try {
await editFormRef.value.validate()
} catch {
return
}
saveLoading.value = true
try {
const payload: RoleFormPayload = {
name: editForm.name.trim(),
...(editForm.roleGroup ? { roleGroup: editForm.roleGroup.trim() } : {}),
}
if (isEdit.value && editForm.id !== undefined) {
payload.id = editForm.id
await updateRoleApi(payload)
ElMessage({
type: 'success',
message: '编辑成功!',
offset: window.innerHeight / 2,
})
} else {
await addRoleApi(payload)
ElMessage({
type: 'success',
message: '添加成功!',
offset: window.innerHeight / 2,
})
}
editVisible.value = false
getList()
} catch (e) {
console.log(e)
} finally {
saveLoading.value = false
}
}
function onRoleDialogClosed() {
editFormRef.value?.clearValidate()
}
async function removeRole() {
try {
await showConfirm('确定删除选中的信息?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await deleteRoleApi(currentRole.value.id)
ElMessage({
type: 'success',
message: '删除成功!',
offset: window.innerHeight / 2,
})
getList()
return res
} catch (e) {
return e
}
}
async function copyRole() {
try {
const res = await copyRoleApi(currentRole.value.id)
if (res.code === 200) {
ElMessage.success(res.message as string)
getList(res.data)
}
return res
} catch (e) {
return e
}
}
function buildGroups(list: RoleItem[]): RoleGroup[] {
const arr: RoleGroup[] = []
for (const iterator of list) {
const group = arr.find((v) => v.label === iterator.roleGroup)
if (group) {
group.list.push(iterator)
} else {
arr.push({ label: iterator.roleGroup, list: [iterator] })
}
}
return arr
}
function searchRole(value: string) {
const filtered = groupLists.value.filter(
(item: RoleItem) => String(item.name).indexOf(value) !== -1,
)
showGroups.value = buildGroups(filtered)
}
function tabClick() {
if (tabActive.value === 'third') {
getUsers()
}
}
async function getUsers() {
try {
const res = await getRoleUsersApi(activeRoleId.value)
userLists.value = (res.data || []) as RoleUser[]
} catch (err) {
console.log(err)
}
}
function setActiveRole(item: RoleItem) {
activeRoleId.value = item.id
currentRole.value = item
getActiveData()
}
function syncNodeCheckState(node: MenuTreeNode) {
const children = node.children || []
if (children.length === 0) return
const childSelectedCount = children.filter((c) =>
menuCheckList.value.includes(c.id),
).length
const allSelected = children.every((c) => menuCheckList.value.includes(c.id))
const anyIndeter = children.some((c) => c.indeter)
node.indeter = (childSelectedCount > 0 && !allSelected) || anyIndeter
const idx = menuCheckList.value.indexOf(node.id)
if (childSelectedCount > 0) {
// 只要有任一子节点被选中,父节点 id 也应进入 checklist(视觉上仍由 indeter 决定半/全勾)
if (idx === -1) menuCheckList.value.push(node.id)
} else {
// 没有任何子节点被选中,移除父节点 id
if (idx !== -1) menuCheckList.value.splice(idx, 1)
node.indeter = false
}
}
function findMenuNode(id: number | string): MenuTreeNode | undefined {
const walk = (list?: MenuTreeNode[]): MenuTreeNode | undefined => {
if (!list) return undefined
for (const node of list) {
if (node.id === id) return node
const found = walk(node.children)
if (found) return found
}
return undefined
}
return walk(menuList.value)
}
function updateParentsIndeter(
list: MenuTreeNode[] | undefined,
target: MenuTreeNode,
parent?: MenuTreeNode,
) {
if (!list) return
for (const node of list) {
if (node.id === target.id) {
if (parent) syncNodeCheckState(parent)
return
}
if (node.children && node.children.length > 0) {
updateParentsIndeter(node.children, target, node)
syncNodeCheckState(node)
}
}
}
function selectChecked(item: MenuTreeNode) {
const checkList = menuCheckList.value
const walk = (list?: MenuTreeNode[]) => {
if (!list) return
for (const node of list) {
node.indeter = false
if (!checkList.includes(node.id)) {
checkList.push(node.id)
}
if (node.children && node.children.length > 0) walk(node.children)
}
}
walk(item.children)
// 同步当前节点的 id 和 indeterminate
syncNodeCheckState(item)
// 更新父级 indeterminate
updateParentsIndeter(menuList.value, item)
}
function clearChecked(item: MenuTreeNode) {
const checkList = menuCheckList.value
const walk = (list?: MenuTreeNode[]) => {
if (!list) return
for (const node of list) {
node.indeter = false
const index = checkList.indexOf(node.id)
if (index !== -1) checkList.splice(index, 1)
if (node.children && node.children.length > 0) walk(node.children)
}
}
walk(item.children)
// 同步当前节点的 id 和 indeterminate
syncNodeCheckState(item)
updateParentsIndeter(menuList.value, item)
}
function checkChange(item: MenuTreeNode, value: boolean | string | number) {
// 一级传入的是合成对象,需查找真实树节点,避免只改到临时对象的 indeter
const target = findMenuNode(item.id) ?? item
if (value || target.indeter) {
selectChecked(target)
} else {
clearChecked(target)
}
target.indeter = false
}
async function saveSelect() {
try {
await showConfirm('确认保存?', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await saveRoleAuthApi({
roleId: activeRoleId.value,
menuIds: menuCheckList.value,
})
ElMessage.success(res.message as string)
return res
} catch (err) {
console.log(err)
return err
}
}
async function getActiveData() {
try {
const res = await getRolePermsApi(activeRoleId.value)
if (res.code == 200) {
menuCheckList.value = (res.data?.menuIds as number[]) || []
checkMenus(menuList.value)
}
} catch (e) {
console.log(e)
}
await getUsers()
}
function checkMenus(menulist: MenuTreeNode[]) {
const check = (list: MenuTreeNode[]) => {
list.forEach((item) => {
if (item.children && item.children.length > 0) {
check(item.children)
syncNodeCheckState(item)
}
})
}
check(menulist)
}
async function getSysMenus() {
try {
const res = await getSysMenusTreeApi()
if (res.code == 200) {
menuTreeStore.setMenuTree((res.data || []) as MenuTreeNode[])
} else {
await ElMessageBox.alert(res.message as string, '错误提示', {
dangerouslyUseHTMLString: true,
}).catch(() => {})
}
} catch (err) {
console.log(err)
}
}
async function getList(role: RoleItem | null = null) {
try {
const res = await getRoleListApi()
const noSuperAdmin = (res.data || []).filter(
(item: RoleItem) => item.name !== '超级管理员',
)
groupLists.value = noSuperAdmin
showGroups.value = buildGroups(groupLists.value)
if (groupLists.value.length > 0) {
if (role) {
activeRoleId.value = role.id
currentRole.value = role
} else {
activeRoleId.value = groupLists.value[0].id
currentRole.value = groupLists.value[0]
}
const currentActiveIndex = showGroups.value.findIndex(
(item: RoleGroup) => item.label == currentRole.value.roleGroup,
)
activeNames.value =
currentActiveIndex >= 0 ? String(currentActiveIndex + 1) : '1'
await getActiveData()
}
if (groupLists.value.length === 0) {
activeRoleId.value = ''
currentRole.value = {} as RoleItem
menuCheckList.value = []
userLists.value = []
}
return res
} catch (err) {
console.log(err)
return err
}
}
onMounted(() => {
getList()
getSysMenus()
})
</script>
<style scoped>
.group_item {
height: 40px;
line-height: 40px;
padding: 0 10px;
background: #fff;
}
.group_item:hover,
.group_item.active {
color: #333333;
cursor: pointer;
user-select: none;
}
.group_item:hover {
background: #f5f7fa;
}
.group_item.active {
background: #409eff;
}
.row :deep .el-collapse-item__header {
padding-left: 10px;
background: #efefef;
color: #333;
font-weight: bold;
}
.row :deep .el-collapse-item__content {
padding-bottom: 0px;
}
.row :deep .el-tabs--top .el-tabs__item.is-top:nth-child(2),
.el-tabs--top .el-tabs__item.is-bottom:nth-child(2),
.el-tabs--bottom .el-tabs__item.is-top:nth-child(2),
.el-tabs--bottom .el-tabs__item.is-bottom:nth-child(2) {
padding-left: 10px;
}
.page_content {
height: 100%;
}
.page_content :deep .el-tabs {
height: calc(100% - 40px);
}
.page_content :deep .el-tabs__content {
height: calc(100% - 50px);
}
.page_content :deep .el-tab-pane {
height: 100%;
}
.menu_content {
flex: 1;
border-left: 1px solid #efefef;
}
.menu_wrap {
height: 100%;
display: flex;
}
.no_role_tip {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.menu_name {
display: inline-block;
min-width: 160px;
padding-left: 10px;
height: 30px;
line-height: 30px;
}
.level_one {
display: flex;
align-items: center;
}
.level_one + .level_one {
border-top: 1px solid #efefef;
}
.level_two {
display: flex;
align-items: center;
}
.level_two + .level_two {
border-top: 1px solid #efefef;
}
.level_three + .level_three {
border-top: 1px solid #efefef;
}
.menu_module:hover,
.menu_module.active {
background: #ddd;
}
.menu_module + .menu_module {
border-top: 1px solid #ddd;
}
.data_gorup > div {
display: inline-block;
width: 160px;
padding-left: 10px;
}
.data_gorup :deep .el-checkbox {
display: block;
line-height: 30px;
}
.user_wrap {
height: 100%;
overflow: auto;
}
.user_wrap li {
display: inline-block;
width: 240px;
height: 50px;
margin: 5px 15px;
border-radius: 5px;
border: 1px solid #ddd;
padding: 12px 15px;
box-sizing: border-box;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* 悬停效果 */
.user_wrap li:hover {
background-color: #f0f7ff;
border-color: #b3d8ff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transform: translateY(-1px);
}
.status-bar :deep .el-checkbox__label {
color: #67c23a;
font-weight: bold;
}
.status-bar :deep .el-checkbox__input.is-checked + span.el-checkbox__label {
color: #f56c6c !important;
}
.status-bar :deep .el-checkbox__input.is-checked .el-checkbox__inner {
background-color: #f56c6c;
border-color: #f56c6c;
}
</style>
...@@ -39,6 +39,7 @@ ...@@ -39,6 +39,7 @@
<div class="user-list flex-1 overflow-hidden"> <div class="user-list flex-1 overflow-hidden">
<ElTable <ElTable
:data="tableData" :data="tableData"
border
default-expand-all default-expand-all
style="width: 100%; height: 100%" style="width: 100%; height: 100%"
@selection-change="handleSelectionChange" @selection-change="handleSelectionChange"
...@@ -77,6 +78,13 @@ ...@@ -77,6 +78,13 @@
</div> </div>
</template> </template>
</ElTableColumn> </ElTableColumn>
<ElTableColumn prop="account" header-align="center" label="功能角色">
<template #default="scope">
<div>
<span>{{ getRoleName(scope.row.roleIds) }} </span>
</div>
</template>
</ElTableColumn>
<ElTableColumn <ElTableColumn
prop="factoryCode" prop="factoryCode"
header-align="center" header-align="center"
...@@ -114,6 +122,9 @@ ...@@ -114,6 +122,9 @@
size="24" size="24"
title="编辑" title="编辑"
color="#EF6C00" color="#EF6C00"
:disabled="
scope.row.firstSupperMark && scope.row.supperMark === 1
"
style="cursor: pointer; vertical-align: middle" style="cursor: pointer; vertical-align: middle"
@click="editUser(scope.row)" @click="editUser(scope.row)"
> >
...@@ -175,6 +186,24 @@ ...@@ -175,6 +186,24 @@
:show-password="true" :show-password="true"
/> />
</ElFormItem> </ElFormItem>
<ElFormItem label="功能角色" prop="roleIds">
<ElSelect
v-model="editForm.roleIds"
placeholder="请选择功能角色"
multiple
:disabled="editForm.firstSupperMark && editForm.supperMark === '1'"
clearable
filterable
style="width: 100%"
>
<ElOption
v-for="item in roleList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="状态" prop="status"> <ElFormItem label="状态" prop="status">
<ElCheckbox <ElCheckbox
v-model="editForm.status" v-model="editForm.status"
...@@ -183,14 +212,14 @@ ...@@ -183,14 +212,14 @@
false-value="0" false-value="0"
></ElCheckbox> ></ElCheckbox>
</ElFormItem> </ElFormItem>
<ElFormItem label="角色" prop="supperMark"> <!-- <ElFormItem label="角色" prop="supperMark">
<ElCheckbox <ElCheckbox
v-model="editForm.supperMark" v-model="editForm.supperMark"
label="超级管理员" label="超级管理员"
true-value="1" true-value="1"
false-value="0" false-value="0"
></ElCheckbox> ></ElCheckbox>
</ElFormItem> </ElFormItem> -->
</ElForm> </ElForm>
</div> </div>
<template #footer> <template #footer>
...@@ -226,6 +255,8 @@ const [editForm, resetEditForm] = useValue<UserEditForm>({ ...@@ -226,6 +255,8 @@ const [editForm, resetEditForm] = useValue<UserEditForm>({
password: '', password: '',
supperMark: '0', supperMark: '0',
status: '1', status: '1',
roleIds: [],
firstSupperMark: false,
}) })
const { const {
currentPage, currentPage,
...@@ -255,6 +286,12 @@ const rules = reactive<FormRules<UserEditForm>>({ ...@@ -255,6 +286,12 @@ const rules = reactive<FormRules<UserEditForm>>({
message: '请输入密码', message: '请输入密码',
}, },
], ],
roleIds: [
{
required: true,
message: '请选择功能角色',
},
],
}) })
const editId = ref<number | undefined>(undefined) const editId = ref<number | undefined>(undefined)
const addUser = () => { const addUser = () => {
...@@ -270,6 +307,16 @@ const deleteUser = async () => { ...@@ -270,6 +307,16 @@ const deleteUser = async () => {
offset: window.innerHeight / 2, offset: window.innerHeight / 2,
}) })
} }
const isDel = selection.value.some(
(item: userData) => item.firstSupperMark && item.supperMark === 1,
)
if (isDel) {
return ElMessage({
message: '超级管理员不能删除',
type: 'error',
offset: window.innerHeight / 2,
})
}
try { try {
await showConfirm('是否删除用户', { await showConfirm('是否删除用户', {
confirmButtonText: '确认', confirmButtonText: '确认',
...@@ -298,30 +345,66 @@ const editUser = async (item: userData) => { ...@@ -298,30 +345,66 @@ const editUser = async (item: userData) => {
try { try {
const res = await getDetailsByIdApi(item.id) const res = await getDetailsByIdApi(item.id)
res.data.supperMark = res.data.supperMark + '' res.data.supperMark = res.data.supperMark + ''
res.data.firstSupperMark = item.firstSupperMark
res.data.status = res.data.status + '' res.data.status = res.data.status + ''
editForm.value = res.data editForm.value = res.data
editForm.value.roleIds =
typeof res.data.roleIds === 'string'
? res.data.roleIds.split(',').map(Number)
: res.data.roleIds ?? []
dialogVisible.value = true dialogVisible.value = true
} catch (e) { } catch (e) {
//showError(e) //showError(e)
} }
} }
onMounted(() => getRoleList())
import { getRoleListApi } from '@/api/role'
const roleList = ref<{ id: number; name: string }[]>([])
const getRoleList = async () => {
try {
const res = await getRoleListApi()
roleList.value = res.data || []
} catch (err) {
console.log(err)
return
}
}
const getRoleName = (roleIds: number[] | string) => {
if (!roleIds) return ''
const ids = Array.isArray(roleIds)
? roleIds
: String(roleIds).split(',').filter(Boolean).map(Number)
return roleList.value
.filter((item: { id: number }) => ids.includes(item.id))
.map((item: { name: string }) => item.name)
.join(',')
}
const save = async () => { const save = async () => {
try { try {
await editFormRef.value.validate() await editFormRef.value.validate()
} catch { } catch {
return return
} }
const roleIdsStr = Array.isArray(editForm.value.roleIds)
? editForm.value.roleIds.join(',')
: editForm.value.roleIds
const hasSuper = getRoleName(editForm.value.roleIds)
.split(',')
.filter(Boolean)
.includes('超级管理员')
try { try {
if (!editId.value) { if (!editId.value) {
await addUserApi({ await addUserApi({
...editForm.value, ...editForm.value,
supperMark: Number(editForm.value.supperMark), roleIds: roleIdsStr,
supperMark: hasSuper ? 1 : 0,
status: Number(editForm.value.status), status: Number(editForm.value.status),
}) })
} else { } else {
await updateUserApi({ await updateUserApi({
...editForm.value, ...editForm.value,
supperMark: Number(editForm.value.supperMark), roleIds: roleIdsStr,
supperMark: hasSuper ? 1 : 0,
status: Number(editForm.value.status), status: Number(editForm.value.status),
}) })
} }
......
<template>
<div class="menu_wrap">
<ElScrollbar
style="
height: calc(100% - 10px);
min-width: 170px;
overflow-y: auto;
overflow-x: hidden;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 5px;
"
>
<div
v-for="(item, index) in menuList"
:key="index"
class="menu_title"
:class="{ active: index === activeIndex }"
@click="setActive(index)"
>
{{ formatFirstName(item.name) }}
</div>
</ElScrollbar>
<div style="flex: 1; display: flex; flex-direction: column">
<slot name="header"></slot>
<ElScrollbar
ref="scrollBarRef"
style="flex: 1; background: #fff; border-radius: 10px"
>
<div v-for="(item, index) in menuList" :key="index" class="scrollbar">
<slot v-bind="item" :index="index"></slot>
</div>
</ElScrollbar>
</div>
</div>
</template>
<script setup lang="ts">
import { capitalize, throttle } from 'lodash-es'
import { nextTick, onBeforeUnmount, onMounted, ref, watch, type Ref } from 'vue'
import type { MenuTreeNode } from '@/types/api/role'
const props = withDefaults(
defineProps<{
menuList?: MenuTreeNode[]
isActive?: boolean
}>(),
{
menuList: () => [],
isActive: false,
},
)
const emit = defineEmits<{
(e: 'setActive', item: MenuTreeNode, index: number): void
}>()
defineSlots<{
header(): unknown
default(props: MenuTreeNode & { index: number }): unknown
}>()
const scrollBarRef = ref<{
wrapRef: Ref<HTMLElement | undefined>
setScrollTop?: (value: number) => void
} | null>(null)
const activeIndex = ref(0)
const itemHeight = ref<number[]>([])
const notScroll = ref(false)
function getWrap(): HTMLElement | undefined {
return scrollBarRef.value?.wrapRef;
}
function formatFirstName(value: unknown) {
const str = String(value ?? '')
if (/^[a-zA-Z]+$/.test(str)) {
return capitalize(str)
}
return str
}
function setHeight() {
nextTick(() => {
if (!props.isActive) return
const wrap = getWrap()
if (!wrap) return
const list = wrap.querySelectorAll('.scrollbar')
itemHeight.value = []
let h = 0
itemHeight.value.push(h)
list.forEach((el) => {
h += (el as HTMLElement).clientHeight
itemHeight.value.push(h)
})
})
}
function setActive(index: number) {
notScroll.value = true
activeIndex.value = index
const wrap = getWrap()
if (wrap) {
wrap.scrollTop = itemHeight.value[index] ?? 0
}
window.setTimeout(() => {
notScroll.value = false
}, 200)
emit('setActive', props.menuList[index], index)
}
const scrollMenu = throttle((event: Event) => {
if (notScroll.value) return
const target = event.target as HTMLElement
const newy = target.scrollTop + 100
const heights = itemHeight.value
for (let i = 0; i < heights.length - 1; i++) {
const h1 = heights[i]
const h2 = heights[i + 1]
if (newy >= h1 && newy < h2) {
if (activeIndex.value !== i) {
activeIndex.value = i
}
return
}
}
}, 200)
function onScroll(event: Event) {
scrollMenu(event)
}
onMounted(() => {
const wrap = getWrap()
if (wrap) {
wrap.addEventListener('scroll', onScroll)
}
setHeight()
})
onBeforeUnmount(() => {
const wrap = getWrap()
if (wrap) {
wrap.removeEventListener('scroll', onScroll)
}
})
watch(
() => props.menuList,
(value, oldValue) => {
if (value !== oldValue) setHeight()
},
)
watch(
() => props.isActive,
(v) => {
if (v && itemHeight.value.length === 0) {
setHeight()
}
},
)
</script>
<style scoped>
.menu_wrap {
height: 100%;
display: flex;
}
.menu_title {
padding-left: 10px;
height: 36px;
line-height: 36px;
font-weight: bold;
font-size: 14px;
cursor: pointer;
}
.menu_title:hover,
.menu_title.active {
color: #333333;
}
.menu_title:hover {
background: #f5f7fa;
}
.menu_title.active {
background: #409eff;
}
.scrollbar {
margin-bottom: 10px;
border-radius: 10px;
border: 1px solid #ddd;
}
</style>
...@@ -1057,7 +1057,7 @@ function getStartTime() { ...@@ -1057,7 +1057,7 @@ function getStartTime() {
return `${year}-${month}-${day} 00:00:00` return `${year}-${month}-${day} 00:00:00`
} }
const selectSku = ref('') const selectSku = ref('')
const treeData = ref<InterWarehouseTree[]>() const treeData = ref<InterWarehouseTree[]>([])
const [searchForm, resetSearchForm] = useValue<warehouseSearchForm>({}) const [searchForm, resetSearchForm] = useValue<warehouseSearchForm>({})
const tradingTime = ref<string[]>([]) const tradingTime = ref<string[]>([])
...@@ -1114,11 +1114,14 @@ const setCostPrice = (item: InterProductList) => { ...@@ -1114,11 +1114,14 @@ const setCostPrice = (item: InterProductList) => {
item.totalPrice = Number(amount) item.totalPrice = Number(amount)
} }
} }
import { roleShow } from '@/utils/roleShow'
const getTreeNum = async () => { const getTreeNum = async () => {
try { try {
const res = await getOutRecordStatusTree() const res = await getOutRecordStatusTree()
res.data = [{ code: 'all', name: '全部', children: res.data }] const result = res.data.filter((item: InterWarehouseTree) =>
treeData.value = res.data roleShow(item.code),
)
treeData.value = [{ code: 'all', name: '全部', children: result }]
await nextTick(() => { await nextTick(() => {
treeRef.value!.setCurrentKey(nodeCode.value, true) treeRef.value!.setCurrentKey(nodeCode.value, true)
}) })
......
...@@ -653,7 +653,8 @@ import { ...@@ -653,7 +653,8 @@ import {
warehouseInfo, warehouseInfo,
InRecordBatchCheckPrintApi, InRecordBatchCheckPrintApi,
factoryWarehouseInventoryPrint, factoryWarehouseInventoryPrint,
warehouseInRecordExport, getLocalFactoryList, warehouseInRecordExport,
getLocalFactoryList,
} from '@/api/warehouse' } from '@/api/warehouse'
import { filePath } from '@/api/axios.ts' import { filePath } from '@/api/axios.ts'
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
...@@ -799,7 +800,7 @@ const selectSku = ref('') ...@@ -799,7 +800,7 @@ const selectSku = ref('')
const userMark = ref(0) const userMark = ref(0)
const batchUserMark = ref(0) const batchUserMark = ref(0)
const importUserMark = ref(0) const importUserMark = ref(0)
const treeData = ref<InterWarehouseTree[]>() const treeData = ref<InterWarehouseTree[]>([])
const [searchForm, resetSearchForm] = useValue<warehouseSearchForm>({}) const [searchForm, resetSearchForm] = useValue<warehouseSearchForm>({})
const tradingTime = ref<string[]>([]) const tradingTime = ref<string[]>([])
...@@ -847,12 +848,14 @@ const { ...@@ -847,12 +848,14 @@ const {
pageSize, pageSize,
).then((res) => res.data), ).then((res) => res.data),
}) })
import { roleShow } from '@/utils/roleShow'
const getTreeNum = async () => { const getTreeNum = async () => {
try { try {
const res = await getInRecordStatusTree() const res = await getInRecordStatusTree()
res.data = [{ code: 'all', name: '全部', children: res.data }] const result = res.data.filter((item: InterWarehouseTree) =>
treeData.value = res.data roleShow(item.code),
)
treeData.value = [{ code: 'all', name: '全部', children: result }]
await nextTick(() => { await nextTick(() => {
treeRef.value!.setCurrentKey(nodeCode.value, true) treeRef.value!.setCurrentKey(nodeCode.value, true)
}) })
...@@ -910,7 +913,6 @@ async function handlePrintProductTag() { ...@@ -910,7 +913,6 @@ async function handlePrintProductTag() {
window.open(filePath + res.message, '_blank') window.open(filePath + res.message, '_blank')
} }
interface InterImportData { interface InterImportData {
warehouseSku: string warehouseSku: string
remark?: string | null remark?: string | null
......
...@@ -96,7 +96,9 @@ const stockProductsColumns = computed(() => { ...@@ -96,7 +96,9 @@ const stockProductsColumns = computed(() => {
width: 100, width: 100,
align: 'center', align: 'center',
render: (item: StockingApplyOrderDetailList) => ( render: (item: StockingApplyOrderDetailList) => (
<ImageView src={item.warehouseSkuImage} width="50px" height="50px" /> <div class="sku-image-cell">
<ImageView src={item.warehouseSkuImage} width="50px" height="50px" />
</div>
), ),
}, },
{ {
...@@ -260,4 +262,12 @@ const handleTabClick = (tab: TabsPaneContext) => { ...@@ -260,4 +262,12 @@ const handleTabClick = (tab: TabsPaneContext) => {
color: #909399; color: #909399;
font-size: 14px; font-size: 14px;
} }
.sku-image-cell {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
</style> </style>
...@@ -138,13 +138,16 @@ ...@@ -138,13 +138,16 @@
></ElPagination> ></ElPagination>
</template> </template>
<template #bottom> <template #bottom>
<StockingApplyOrderDetailTabs :selected-row="selectedRow" /> <StockingApplyOrderDetailTabs :selected-row="selectedRow ?? null" />
</template> </template>
</SplitDiv> </SplitDiv>
</div> </div>
</div> </div>
<!-- 提交入库弹框 --> <!-- 提交入库弹框 -->
<SubmitWarehousingDialog ref="submitWarehousingDialogRef" @refresh="onRefresh" /> <SubmitWarehousingDialog
ref="submitWarehousingDialogRef"
@refresh="onRefresh"
/>
</div> </div>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
...@@ -168,7 +171,8 @@ const treeData = ref<TreeData[]>([]) ...@@ -168,7 +171,8 @@ const treeData = ref<TreeData[]>([])
const treeRef = ref() const treeRef = ref()
const tableRef = ref() const tableRef = ref()
const selectedRow = ref<TableData | null>(null) const selectedRow = ref<TableData | null>(null)
const submitWarehousingDialogRef = ref<InstanceType<typeof SubmitWarehousingDialog>>() const submitWarehousingDialogRef =
ref<InstanceType<typeof SubmitWarehousingDialog>>()
const [searchForm, resetSearchForm] = useValue<SearchForm>({} as SearchForm) const [searchForm, resetSearchForm] = useValue<SearchForm>({} as SearchForm)
const status = ref<string>('TO_BE_RECEIPT') const status = ref<string>('TO_BE_RECEIPT')
const warehouseList = ref<WarehouseListData[]>([]) const warehouseList = ref<WarehouseListData[]>([])
...@@ -271,13 +275,15 @@ const loadWarehouseList = async () => { ...@@ -271,13 +275,15 @@ const loadWarehouseList = async () => {
console.error(e) console.error(e)
} }
} }
import { roleShow } from '@/utils/roleShow'
const loadTreeData = async () => { const loadTreeData = async () => {
try { try {
const res = await getStockingApplyOrderTreeApi() const res = await getStockingApplyOrderTreeApi()
if (res.code !== 200) return if (res.code !== 200) return
const result = res.data.filter((item) => roleShow(item.code)) as TreeData[]
treeData.value = [ treeData.value = [
{ code: '-1', name: '全部', children: res.data || [] } as TreeData, { code: '-1', name: '全部', children: result },
] ] as typeof treeData.value
await nextTick(() => { await nextTick(() => {
treeRef.value!.setCurrentKey(status.value, true) treeRef.value!.setCurrentKey(status.value, true)
}) })
......
...@@ -1165,7 +1165,7 @@ function getStartTime() { ...@@ -1165,7 +1165,7 @@ function getStartTime() {
return `${year}-${month}-${day} 00:00:00` return `${year}-${month}-${day} 00:00:00`
} }
const selectSku = ref('') const selectSku = ref('')
const treeData = ref<InterWarehouseTree[]>() const treeData = ref<InterWarehouseTree[]>([])
const [searchForm, resetSearchForm] = useValue<stockingPlanSearchForm>({ const [searchForm, resetSearchForm] = useValue<stockingPlanSearchForm>({
dateStr: 'createTime', dateStr: 'createTime',
}) })
...@@ -1227,11 +1227,14 @@ const setCostPrice = (item: InterProductList) => { ...@@ -1227,11 +1227,14 @@ const setCostPrice = (item: InterProductList) => {
item.totalPrice = Number(amount) item.totalPrice = Number(amount)
} }
} }
import { roleShow } from '@/utils/roleShow'
const getTreeNum = async () => { const getTreeNum = async () => {
try { try {
const res = await getStackingPlanStatusTree() const res = await getStackingPlanStatusTree()
res.data = [{ code: 'all', name: '全部', children: res.data }] const result = res.data.filter((item: InterWarehouseTree) =>
treeData.value = res.data roleShow(item.code),
)
treeData.value = [{ code: 'all', name: '全部', children: result }]
await nextTick(() => { await nextTick(() => {
treeRef.value!.setCurrentKey(nodeCode.value, true) treeRef.value!.setCurrentKey(nodeCode.value, true)
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment