搭建页面骨架,写账号管理和设备管理

This commit is contained in:
zhouyanli
2026-10-13 08:01:47 +08:00
parent d73b554975
commit febbb83dad
13 changed files with 124 additions and 671 deletions

View File

@@ -1,6 +1,6 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统 VITE_APP_TITLE = 水电表充值管理平台
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus VITE_APP_LOGO_TITLE = 水电表充值管理平台
# 开发环境配置 # 开发环境配置
VITE_APP_ENV = 'development' VITE_APP_ENV = 'development'

View File

@@ -1,6 +1,6 @@
# 页面标题 # 页面标题
VITE_APP_TITLE = RuoYi-Vue-Plus后台管理系统 VITE_APP_TITLE = 水电表充值管理平台
VITE_APP_LOGO_TITLE = RuoYi-Vue-Plus VITE_APP_LOGO_TITLE = 水电表充值管理平台
# 生产环境配置 # 生产环境配置
VITE_APP_ENV = 'production' VITE_APP_ENV = 'production'

View File

@@ -35,16 +35,14 @@ const getBreadcrumb = () => {
} else { } else {
matched = route.matched.filter(item => item.meta && item.meta.title); matched = route.matched.filter(item => item.meta && item.meta.title);
} }
// 判断是否为首页
if (!isDashboard(matched[0])) {
matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched);
}
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false); levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false);
}; };
const findPathNum = (str, char = '/') => { const findPathNum = (str, char = '/') => {
if (typeof str !== 'string' || str.length === 0) return 0; if (typeof str !== 'string' || str.length === 0) return 0;
return str.split(char).length - 1; return str.split(char).length - 1;
}; };
const getMatched = (pathList, routeList, matched) => { const getMatched = (pathList, routeList, matched) => {
const data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0]); const data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0]);
if (data) { if (data) {
@@ -55,13 +53,7 @@ const getMatched = (pathList, routeList, matched) => {
} }
} }
}; };
const isDashboard = (route: RouteLocationMatched) => {
const name = route && (route.name as string);
if (!name) {
return false;
}
return name.trim() === 'Index';
};
const handleLink = item => { const handleLink = item => {
const { redirect, path } = item; const { redirect, path } = item;
redirect ? router.push(redirect) : router.push(path); redirect ? router.push(redirect) : router.push(path);

View File

@@ -25,11 +25,11 @@
<div class="right-menu flex align-center"> <div class="right-menu flex align-center">
<template v-if="appStore.device !== 'mobile'"> <template v-if="appStore.device !== 'mobile'">
<search-menu ref="searchMenuRef" /> <search-menu ref="searchMenuRef" />
<el-tooltip content="搜索" effect="dark" placement="bottom"> <!-- <el-tooltip content="搜索" effect="dark" placement="bottom">
<div class="right-menu-item hover-effect" @click="openSearchMenu"> <div class="right-menu-item hover-effect" @click="openSearchMenu">
<svg-icon class-name="search-icon" icon-class="search" /> <svg-icon class-name="search-icon" icon-class="search" />
</div> </div>
</el-tooltip> </el-tooltip> -->
<!-- 消息 --> <!-- 消息 -->
<el-tooltip :content="$t('navbar.message')" effect="dark" placement="bottom"> <el-tooltip :content="$t('navbar.message')" effect="dark" placement="bottom">
<div> <div>
@@ -47,7 +47,7 @@
</el-popover> </el-popover>
</div> </div>
</el-tooltip> </el-tooltip>
<el-tooltip content="Github" effect="dark" placement="bottom"> <!-- <el-tooltip content="Github" effect="dark" placement="bottom">
<ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip>
@@ -65,7 +65,7 @@
<el-tooltip :content="$t('navbar.layoutSize')" effect="dark" placement="bottom"> <el-tooltip :content="$t('navbar.layoutSize')" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" /> <size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip> </el-tooltip> -->
</template> </template>
<div class="avatar-container"> <div class="avatar-container">
<el-dropdown class="avatar-dropdown" trigger="click" @command="handleCommand"> <el-dropdown class="avatar-dropdown" trigger="click" @command="handleCommand">

View File

@@ -181,7 +181,7 @@ const isFirstView = () => {
return false; return false;
} }
try { try {
return tag.fullPath === '/index' || tag.fullPath === visitedViews.value[1]?.fullPath; return tag.fullPath === visitedViews.value[0]?.fullPath;
} catch { } catch {
return false; return false;
} }

View File

@@ -49,15 +49,15 @@ router.beforeEach(async (to, from) => {
router.addRoute(route); // 动态添加可访问路由表 router.addRoute(route); // 动态添加可访问路由表
} }
}); });
// hack方法 确保addRoutes已完成 // hack方法 确保addRoutes已完成:
return { // 动态路由挂载前解析出的 to 可能只是 404 兜底路由(后台菜单路由此时还没注册),
path: to.path, // 所以这里按 fullPath 重新解析一次,用挂载后的真实路由再走一遍导航
replace: true, const resolved = router.resolve(to.fullPath);
params: to.params, // 目标地址确实不存在(如后台菜单还没建 / 历史遗留的已删除地址)时退到默认落点,避免登录后直接落到 404
query: to.query, if (resolved.name === 'NotFound') {
hash: to.hash, return { path: '/', replace: true };
name: to.name as string }
}; return { path: resolved.fullPath, replace: true };
} }
} else { } else {
return true; return true;

View File

@@ -54,6 +54,7 @@ export const constantRoutes: RouteRecordRaw[] = [
}, },
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error/404.vue'), component: () => import('@/views/error/404.vue'),
hidden: true hidden: true
}, },
@@ -63,17 +64,10 @@ export const constantRoutes: RouteRecordRaw[] = [
hidden: true hidden: true
}, },
{ {
path: '', // 无首页:登录后默认落点设备列表(设备路由由后台菜单下发)
component: Layout, path: '/',
redirect: '/index', redirect: '/device/devicelist',
children: [ hidden: true
{
path: '/index',
component: () => import('@/views/index.vue'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
}
]
}, },
{ {
path: '/user', path: '/user',

View File

@@ -91,7 +91,9 @@ export const usePermissionStore = defineStore('permission', () => {
} }
// Layout ParentView 组件特殊处理 // Layout ParentView 组件特殊处理
if (route.component?.toString() === 'Layout') { if (route.component?.toString() === 'Layout') {
route.component = Layout; // 仅一级目录用 Layout。子级菜单若也返回 Layout(后台菜单未配置真实组件路径),
// 再套一层 Layout 会出现“布局里又套一套侧边栏/顶栏”,这里降级为 ParentView
route.component = lastRouter ? ParentView : Layout;
} else if (route.component?.toString() === 'ParentView') { } else if (route.component?.toString() === 'ParentView') {
route.component = ParentView; route.component = ParentView;
} else if (route.component?.toString() === 'InnerLink') { } else if (route.component?.toString() === 'InnerLink') {

View File

@@ -15,7 +15,7 @@
<div class="bullshit__info"> <div class="bullshit__info">
对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。 对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
</div> </div>
<router-link to="/index" class="bullshit__return-home">返回首页</router-link> <router-link to="/" class="bullshit__return-home">返回主页</router-link>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,370 +0,0 @@
<template>
<div class="home">
<section class="hero-panel">
<div class="hero-copy">
<h1>RuoYi-Vue-Plus 控制台</h1>
<p>
企业级后台管理系统 重写 RuoYi-Vue 所有功能 集成 Sa-Token、Mybatis-Plus、WarmFlow、SpringDoc、Hutool、OSS
等组件
</p>
<div class="hero-actions">
<el-button type="primary" @click="goTarget('https://gitee.com/dromara/RuoYi-Vue-Plus')">查看源码</el-button>
<el-button plain @click="goTarget('https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog')">
更新日志
</el-button>
</div>
</div>
</section>
<div class="content-grid">
<section class="section-card">
<div class="section-head">
<div>
<h2>项目矩阵</h2>
</div>
</div>
<div class="product-list">
<article v-for="product in products" :key="product.name" class="product-card">
<div class="product-top">
<div>
<h3>{{ product.name }}</h3>
<p>{{ product.summary }}</p>
</div>
<span class="product-version">{{ product.version }}</span>
</div>
<div class="product-tags">
<el-tag v-for="tag in product.tags" :key="tag" effect="plain">{{ tag }}</el-tag>
</div>
<div class="product-actions">
<el-button type="primary" plain @click="goTarget(product.primaryUrl)">
{{ product.primaryLabel }}
</el-button>
<el-button plain @click="goTarget(product.secondaryUrl)">{{ product.secondaryLabel }}</el-button>
</div>
</article>
</div>
</section>
<section class="section-card capability-card">
<div class="section-head">
<div>
<h2>能力地图</h2>
</div>
</div>
<div class="capability-groups">
<article v-for="group in capabilityGroups" :key="group.title" class="capability-group">
<h3>{{ group.title }}</h3>
<ul>
<li v-for="item in group.items" :key="item">{{ item }}</li>
</ul>
</article>
</div>
</section>
</div>
</div>
</template>
<script setup name="Index" lang="ts">
const products = [
{
name: 'RuoYi-Vue-Plus',
version: 'v6.0.0',
summary: '面向分布式集群场景的后台管理系统,保持现有业务接口与权限逻辑,适合先完成前端壳升级。',
tags: ['Vue 3', 'Element Plus', 'Spring Boot', 'Sa-Token'],
primaryLabel: '访问 GitHub',
primaryUrl: 'https://github.com/dromara/RuoYi-Vue-Plus',
secondaryLabel: '查看更新日志',
secondaryUrl: 'https://plus-doc.dromara.org/#/ruoyi-vue-plus/changlog'
},
{
name: 'RuoYi-Cloud-Plus',
version: 'v6.0.0',
summary: '微服务通用权限管理系统,适合更复杂的服务治理场景,也可以沿用同样的前端升级思路。',
tags: ['Spring Cloud', 'Gateway', 'Nacos', 'Dubbo'],
primaryLabel: '访问 GitHub',
primaryUrl: 'https://github.com/dromara/RuoYi-Cloud-Plus',
secondaryLabel: '查看更新日志',
secondaryUrl: 'https://plus-doc.dromara.org/#/ruoyi-cloud-plus/changlog'
}
];
const capabilityGroups = [
{
title: '后端基建',
items: ['Spring Boot / Spring Cloud', 'Sa-Token 认证与权限', 'MySQL / Redis', '代码生成器']
},
{
title: '平台能力',
items: ['动态菜单与按钮权限', '监控、日志、在线用户', '任务调度与工作流', '文件存储与多云适配']
},
{
title: '前端方向',
items: ['UI 卡片化', '主题与布局统一', '通用页面容器规范化', '企业化布局']
}
];
const goTarget = (url: string) => {
window.open(url, '__blank');
};
</script>
<style lang="scss" scoped>
.home {
display: flex;
flex-direction: column;
gap: 20px;
}
.hero-panel,
.section-card {
border-radius: 28px;
border: 1px solid var(--app-surface-border);
background: var(--app-surface-bg);
box-shadow: var(--app-shadow-sm);
backdrop-filter: blur(18px);
}
.hero-panel {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(280px, 0.7fr);
gap: 18px;
padding: 30px;
background: radial-gradient(circle at top left, rgba(53, 109, 255, 0.16), transparent 30%), var(--app-surface-bg);
}
.hero-copy {
display: flex;
flex-direction: column;
gap: 14px;
h1 {
margin: 0;
font-size: clamp(30px, 4vw, 46px);
line-height: 1.06;
letter-spacing: -0.04em;
color: var(--app-text-title);
}
p {
margin: 0;
max-width: 760px;
color: var(--app-text-muted);
font-size: 15px;
line-height: 1.8;
}
}
.hero-badge {
display: inline-flex;
width: fit-content;
padding: 8px 14px;
border-radius: 999px;
background: rgba(53, 109, 255, 0.12);
color: var(--app-accent-strong);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 8px;
}
.hero-stats {
display: grid;
gap: 12px;
}
.stat-card {
padding: 18px 20px;
border-radius: 22px;
background: var(--app-elevated-soft-bg);
border: 1px solid var(--app-surface-border);
display: flex;
flex-direction: column;
gap: 6px;
strong {
color: var(--app-text-title);
font-size: 24px;
letter-spacing: -0.03em;
}
span {
color: var(--app-text-muted);
font-size: 13px;
}
}
.content-grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(300px, 0.85fr);
gap: 20px;
}
.section-card {
padding: 24px;
}
.section-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 18px;
h2 {
margin: 6px 0 0;
color: var(--app-text-title);
font-size: 26px;
letter-spacing: -0.03em;
}
}
.section-kicker {
color: var(--app-text-muted);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.product-list,
.capability-groups {
display: grid;
gap: 16px;
}
.product-card {
padding: 22px;
border-radius: 24px;
background: var(--app-elevated-soft-bg);
border: 1px solid var(--app-surface-border);
transition:
transform 0.25s ease,
box-shadow 0.25s ease,
border-color 0.25s ease;
&:hover {
transform: translateY(-2px);
box-shadow: var(--app-shadow-sm);
border-color: rgba(53, 109, 255, 0.2);
}
}
.product-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
h3 {
margin: 0 0 8px;
color: var(--app-text-title);
font-size: 22px;
letter-spacing: -0.03em;
}
p {
margin: 0;
color: var(--app-text-muted);
line-height: 1.8;
}
}
.product-version {
flex-shrink: 0;
padding: 8px 12px;
border-radius: 999px;
background: rgba(53, 109, 255, 0.12);
color: var(--app-accent-strong);
font-size: 12px;
font-weight: 700;
}
.product-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 18px;
}
.product-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 18px;
}
.capability-card {
background: radial-gradient(circle at top right, rgba(14, 165, 233, 0.12), transparent 28%), var(--app-surface-bg);
}
.capability-group {
padding: 18px 18px 18px 20px;
border-radius: 22px;
background: var(--app-elevated-soft-bg);
border: 1px solid var(--app-surface-border);
h3 {
margin: 0 0 12px;
color: var(--app-text-title);
font-size: 18px;
}
ul {
margin: 0;
padding: 0;
list-style: none;
display: grid;
gap: 10px;
}
li {
position: relative;
padding-left: 16px;
color: var(--app-text-muted);
line-height: 1.7;
&::before {
content: '';
position: absolute;
left: 0;
top: 10px;
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--app-accent-strong);
box-shadow: 0 0 0 5px rgba(53, 109, 255, 0.12);
}
}
}
@media (max-width: 960px) {
.hero-panel,
.content-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 640px) {
.hero-panel,
.section-card {
padding: 20px;
border-radius: 22px;
}
.product-top {
flex-direction: column;
}
}
html.dark {
.hero-panel {
background: radial-gradient(circle at top left, rgba(53, 109, 255, 0.18), transparent 30%), var(--app-surface-bg);
}
}
</style>

View File

@@ -1,25 +1,6 @@
<template> <template>
<div class="login"> <div class="login">
<div class="login-shell"> <div class="login-shell">
<section class="login-brand">
<span class="brand-pill">Plus UI Workspace</span>
<h1 class="brand-title">企业级后台管理系统</h1>
<p class="brand-desc">
真正面向企业级的应用框架 组件化 模块化 轻耦合 高扩展 针对企业痛点 业界一流技术栈
<br />
重写 RuoYi-Vue 所有功能 集成 Sa-Token、Mybatis-Plus、WarmFlow、SpringDoc、Hutool、OSS 定期同步。
</p>
<div class="brand-highlights">
<span v-for="item in highlights" :key="item" class="highlight-chip">{{ item }}</span>
</div>
<div class="brand-metrics">
<article v-for="item in quickStats" :key="item.label" class="metric-card">
<strong>{{ item.value }}</strong>
<span>{{ item.label }}</span>
</article>
</div>
</section>
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form"> <el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<div class="title-box"> <div class="title-box">
<div> <div>
@@ -77,7 +58,7 @@
</router-link> </router-link>
</div> </div>
<div class="social-panel"> <!-- <div class="social-panel">
<span class="social-label">第三方登录</span> <span class="social-label">第三方登录</span>
<div class="social-actions"> <div class="social-actions">
<el-button circle :title="$t('login.social.wechat')" @click="doSocialLogin('wechat')"> <el-button circle :title="$t('login.social.wechat')" @click="doSocialLogin('wechat')">
@@ -96,7 +77,7 @@
<svg-icon icon-class="github" /> <svg-icon icon-class="github" />
</el-button> </el-button>
</div> </div>
</div> </div> -->
<el-form-item class="submit-row"> <el-form-item class="submit-row">
<el-button :loading="loading" size="large" type="primary" class="submit-button" @click.prevent="handleLogin"> <el-button :loading="loading" size="large" type="primary" class="submit-button" @click.prevent="handleLogin">
@@ -124,19 +105,13 @@ import { useUserStore } from '@/store/modules/user';
const title = import.meta.env.VITE_APP_TITLE; const title = import.meta.env.VITE_APP_TITLE;
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const quickStats = [
{ label: '细粒度权限管理', value: '动态权限控制' },
{ label: '主流技术栈', value: '全栈技术集成' },
{ label: 'UI样式', value: '卡片式' }
];
const highlights = ['技术栈全面升级', '动态菜单', '多主题布局', '深浅色主题'];
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const loginForm = ref<LoginData>({ const loginForm = ref<LoginData>({
username: 'admin', username: '',
password: 'admin123', password: '',
rememberMe: false, rememberMe: false,
code: '', code: '',
uuid: '' uuid: ''
@@ -226,8 +201,9 @@ const getLoginData = () => {
const rememberMe = localStorage.getItem('rememberMe'); const rememberMe = localStorage.getItem('rememberMe');
localStorage.removeItem('password'); localStorage.removeItem('password');
loginForm.value = { loginForm.value = {
username: username === null ? String(loginForm.value.username) : username, // 仅回填勾选“记住密码”时保存的用户名,密码一律不回填
password: username === null ? String(loginForm.value.password) : '', username: username === null ? '' : username,
password: '',
rememberMe: rememberMe === 'true' rememberMe: rememberMe === 'true'
} as LoginData; } as LoginData;
}; };
@@ -264,102 +240,18 @@ onMounted(() => {
.login-shell { .login-shell {
width: min(1180px, 100%); width: min(1180px, 100%);
display: grid; display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(360px, 440px); grid-template-columns: minmax(0, 440px);
justify-content: center;
gap: 26px; gap: 26px;
align-items: stretch; align-items: stretch;
} }
.login-brand,
.login-form { .login-form {
width: 100%;
border-radius: 28px; border-radius: 28px;
border: 1px solid rgba(255, 255, 255, 0.14); border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 30px 80px rgba(2, 8, 23, 0.32); box-shadow: 0 30px 80px rgba(2, 8, 23, 0.32);
backdrop-filter: blur(18px); backdrop-filter: blur(18px);
}
.login-brand {
padding: 42px;
display: flex;
flex-direction: column;
justify-content: space-between;
color: #eef4ff;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)),
linear-gradient(135deg, rgba(53, 109, 255, 0.32), rgba(15, 23, 42, 0.24));
}
.brand-pill {
display: inline-flex;
align-items: center;
width: fit-content;
padding: 8px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.12);
color: rgba(255, 255, 255, 0.9);
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.brand-title {
margin: 22px 0 14px;
font-size: clamp(34px, 4vw, 52px);
line-height: 1.08;
letter-spacing: -0.03em;
}
.brand-desc {
margin: 0;
max-width: 580px;
color: rgba(226, 232, 240, 0.88);
font-size: 15px;
line-height: 1.8;
}
.brand-highlights {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 28px 0 34px;
}
.highlight-chip {
padding: 9px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #f8fbff;
font-size: 13px;
}
.brand-metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.metric-card {
padding: 18px 16px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
flex-direction: column;
gap: 6px;
strong {
font-size: 24px;
color: #fff;
}
span {
color: rgba(226, 232, 240, 0.72);
font-size: 13px;
}
}
.login-form {
width: 100%;
padding: 34px 30px 26px; padding: 34px 30px 26px;
z-index: 1; z-index: 1;
background: var(--app-surface-bg); background: var(--app-surface-bg);
@@ -536,25 +428,9 @@ onMounted(() => {
.login { .login {
padding: 24px 14px 80px; padding: 24px 14px 80px;
} }
.login-shell {
grid-template-columns: 1fr;
}
.login-brand {
padding: 28px 24px;
}
.brand-metrics {
grid-template-columns: 1fr;
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.login-brand {
display: none;
}
.login-form { .login-form {
padding: 26px 18px 20px; padding: 26px 18px 20px;
} }

View File

@@ -1,22 +1,7 @@
<template> <template>
<div class="p-2 system-user-page"> <div class="p-2 system-user-page">
<el-row :gutter="20" class="content-grid"> <el-row :gutter="20" class="content-grid">
<!-- 部门树 --> <el-col :span="24" class="content-main">
<tree-panel
ref="treePanelRef"
v-model:collapsed="treeCollapsed"
title="部门结构"
placeholder="请输入部门名称"
:data="deptOptions"
:expanded-span="5"
@node-click="handleNodeClick"
/>
<el-col
:lg="treeCollapsed ? 23 : 19"
:xs="24"
class="tree-content-col content-main"
:class="{ 'is-tree-collapsed': treeCollapsed }"
>
<div class="search-wrap"> <div class="search-wrap">
<el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }"> <el-card shadow="hover" class="search-panel" :class="{ 'is-collapsed': !showSearch }">
<template #header> <template #header>
@@ -28,30 +13,30 @@
</div> </div>
</template> </template>
<el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form"> <el-form ref="queryFormRef" :model="queryParams" :inline="true" class="query-form">
<el-form-item label="用户名称" prop="userName"> <el-form-item label="账号" prop="userName">
<el-input <el-input
v-model="queryParams.userName" v-model="queryParams.userName"
placeholder="请输入用户名称" placeholder="请输入账号"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="用户昵称" prop="nickName"> <el-form-item label="管理员姓名" prop="nickName">
<el-input <el-input
v-model="queryParams.nickName" v-model="queryParams.nickName"
placeholder="请输入用户昵称" placeholder="请输入管理员姓名"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item>
<el-form-item label="手机号码" prop="phoneNumber"> <!-- <el-form-item label="手机号码" prop="phoneNumber">
<el-input <el-input
v-model="queryParams.phoneNumber" v-model="queryParams.phoneNumber"
placeholder="请输入手机号码" placeholder="请输入手机号码"
clearable clearable
@keyup.enter="handleQuery" @keyup.enter="handleQuery"
/> />
</el-form-item> </el-form-item> -->
<el-form-item label="状态" prop="status"> <el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="用户状态" clearable> <el-select v-model="queryParams.status" placeholder="用户状态" clearable>
@@ -63,7 +48,7 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="创建时间" style="width: 308px"> <!-- <el-form-item label="创建时间" style="width: 308px">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="dateRange"
value-format="YYYY-MM-DD HH:mm:ss" value-format="YYYY-MM-DD HH:mm:ss"
@@ -73,7 +58,7 @@
end-placeholder="结束日期" end-placeholder="结束日期"
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]" :default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
></el-date-picker> ></el-date-picker>
</el-form-item> </el-form-item> -->
<el-form-item> <el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button> <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -114,7 +99,7 @@
> >
删除 删除
</el-button> </el-button>
<el-button <!-- <el-button
v-hasPermi="['system:user:edit']" v-hasPermi="['system:user:edit']"
type="warning" type="warning"
plain plain
@@ -123,17 +108,17 @@
@click="handleUnlock()" @click="handleUnlock()"
> >
解锁 解锁
</el-button> </el-button> -->
<el-dropdown class="mt-[1px]"> <!-- <el-dropdown class="mt-[1px]">
<el-button plain type="info"> <el-button plain type="info">
更多 更多
<el-icon class="el-icon--right"><arrow-down /></el-icon> <el-icon class="el-icon--right"><arrow-down /></el-icon>
</el-button> </el-button>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item> <el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item> -->
<!-- 注意 由于el-dropdown-item标签是延迟加载的 所以v-has-permi自定义标签不生效 需要使用v-if调用方法执行 --> <!-- 注意 由于el-dropdown-item标签是延迟加载的 所以v-has-permi自定义标签不生效 需要使用v-if调用方法执行 -->
<el-dropdown-item v-if="checkPermi(['system:user:import'])" icon="Top" @click="handleImport"> <!-- <el-dropdown-item v-if="checkPermi(['system:user:import'])" icon="Top" @click="handleImport">
导入数据 导入数据
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item v-if="checkPermi(['system:user:export'])" icon="Download" @click="handleExport"> <el-dropdown-item v-if="checkPermi(['system:user:export'])" icon="Download" @click="handleExport">
@@ -141,7 +126,7 @@
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown> -->
<right-toolbar <right-toolbar
v-model:show-search="showSearch" v-model:show-search="showSearch"
:columns="columns" :columns="columns"
@@ -165,7 +150,7 @@
<el-table-column <el-table-column
v-if="columns[1].visible" v-if="columns[1].visible"
key="userName" key="userName"
label="用户名称" label="账号"
align="center" align="center"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
> >
@@ -178,27 +163,35 @@
<el-table-column <el-table-column
v-if="columns[2].visible" v-if="columns[2].visible"
key="nickName" key="nickName"
label="用户昵称" label="管理者姓名"
align="center" align="center"
prop="nickName" prop="nickName"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> />
<el-table-column <el-table-column
v-if="columns[3].visible"
key="roleNames"
label="角色权限"
align="center"
prop="roleNames"
width=""
/>
<!-- <el-table-column
v-if="columns[3].visible" v-if="columns[3].visible"
key="deptName" key="deptName"
label="部门" label="部门"
align="center" align="center"
prop="deptName" prop="deptName"
:show-overflow-tooltip="true" :show-overflow-tooltip="true"
/> /> -->
<el-table-column <!-- <el-table-column
v-if="columns[4].visible" v-if="columns[4].visible"
key="phoneNumber" key="phoneNumber"
label="手机号码" label="手机号码"
align="center" align="center"
prop="phoneNumber" prop="phoneNumber"
width="120" width="120"
/> /> -->
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center"> <el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
<template #default="scope"> <template #default="scope">
<el-switch <el-switch
@@ -280,14 +273,19 @@
append-to-body append-to-body
@close="closeDialog" @close="closeDialog"
> >
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px"> <el-form ref="userFormRef" :model="form" :rules="rules" label-width="100px">
<el-row> <el-row>
<el-col :span="12"> <el-col :span="24">
<el-form-item label="用户昵称" prop="nickName"> <el-form-item label="登录账号" prop="userName">
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" /> <el-input v-model="form.userName" :disabled="isEdit" placeholder="请输入登录账号" maxlength="30" />
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId"> <el-col :span="24">
<el-form-item label="管理者姓名" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入管理者姓名" maxlength="30" />
</el-form-item>
</el-col>
<!-- <el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
<el-form-item label="归属部门" prop="deptId"> <el-form-item label="归属部门" prop="deptId">
<el-tree-select <el-tree-select
v-model="form.deptId" v-model="form.deptId"
@@ -299,9 +297,9 @@
@change="handleDeptChange" @change="handleDeptChange"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col> -->
</el-row> </el-row>
<el-row> <!-- <el-row>
<el-col :span="12"> <el-col :span="12">
<el-form-item label="手机号码" prop="phoneNumber"> <el-form-item label="手机号码" prop="phoneNumber">
<el-input v-model="form.phoneNumber" placeholder="请输入手机号码" maxlength="11" /> <el-input v-model="form.phoneNumber" placeholder="请输入手机号码" maxlength="11" />
@@ -312,64 +310,10 @@
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" /> <el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row> -->
<el-row> <el-row>
<el-col :span="12"> <el-col :span="24" v-if="form.userId == null || form.userId != useUserStore().userId">
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName"> <el-form-item label="角色权限" prop="roleIds">
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
<el-input
v-model="form.password"
placeholder="请输入用户密码"
type="password"
maxlength="20"
show-password
/>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="用户性别">
<el-select v-model="form.gender" placeholder="请选择">
<el-option
v-for="dict in sys_user_gender"
:key="dict.value"
:label="dict.label"
:value="dict.value"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
<el-form-item label="岗位">
<el-select v-model="form.postIds" multiple placeholder="请选择">
<el-option
v-for="item in postOptions"
:key="item.postId"
:label="item.postName"
:value="item.postId"
:disabled="item.status == '1'"
></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
<el-form-item label="角色" prop="roleIds">
<el-select v-model="form.roleIds" filterable multiple placeholder="请选择"> <el-select v-model="form.roleIds" filterable multiple placeholder="请选择">
<el-option <el-option
v-for="item in roleOptions" v-for="item in roleOptions"
@@ -382,13 +326,38 @@
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
<el-row> <!-- 初始密码 / 账号状态:仅新增时展示,修改只保留账号、姓名、角色 -->
<template v-if="isAdd">
<el-row>
<el-col :span="24">
<el-form-item label="初始密码" prop="password">
<el-input
v-model="form.password"
placeholder="请输入初始密码"
type="password"
maxlength="20"
show-password
/>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="账号状态">
<el-radio-group v-model="form.status">
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
{{ dict.label }}
</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</template>
<!-- <el-row>
<el-col :span="24"> <el-col :span="24">
<el-form-item label="备注"> <el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input> <el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row> -->
</el-form> </el-form>
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
@@ -454,20 +423,18 @@
import { to } from 'await-to-js'; import { to } from 'await-to-js';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { getConfigKey } from '@/api/system/config'; import { getConfigKey } from '@/api/system/config';
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types'; import { DeptTreeVO } from '@/api/system/dept/types';
import { optionselect } from '@/api/system/post'; import { optionselect } from '@/api/system/post';
import { PostVO } from '@/api/system/post/types'; import { PostVO } from '@/api/system/post/types';
import { RoleVO } from '@/api/system/role/types'; import { RoleVO } from '@/api/system/role/types';
import api from '@/api/system/user'; import api from '@/api/system/user';
import { UserForm, UserQuery, UserVO } from '@/api/system/user/types'; import { UserForm, UserQuery, UserVO } from '@/api/system/user/types';
import TreePanel from '@/components/TreePanel/index.vue';
import { useLoading } from '@/hooks/async/useLoading'; import { useLoading } from '@/hooks/async/useLoading';
import { useDialogState } from '@/hooks/dialog/useDialogState'; import { useDialogState } from '@/hooks/dialog/useDialogState';
import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery'; import { useDateRangeQuery } from '@/hooks/form/useDateRangeQuery';
import { useSearchReset } from '@/hooks/form/useSearchReset'; import { useSearchReset } from '@/hooks/form/useSearchReset';
import { useSearchToggle } from '@/hooks/form/useSearchToggle'; import { useSearchToggle } from '@/hooks/form/useSearchToggle';
import { useTableSelection } from '@/hooks/table/useTableSelection'; import { useTableSelection } from '@/hooks/table/useTableSelection';
import { useTreeCollapsed } from '@/hooks/tree/useTreeCollapsed';
import modal from '@/plugins/modal'; import modal from '@/plugins/modal';
import { useUserStore } from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user';
import { useDict } from '@/utils/dict'; import { useDict } from '@/utils/dict';
@@ -483,8 +450,6 @@ const { loading, withLoading } = useLoading(true);
const { showSearch } = useSearchToggle(); const { showSearch } = useSearchToggle();
const total = ref(0); const total = ref(0);
const { dateRange, applyDateRange, resetDateRange } = useDateRangeQuery(); const { dateRange, applyDateRange, resetDateRange } = useDateRangeQuery();
const { treeCollapsed } = useTreeCollapsed();
const deptOptions = ref<DeptTreeVO[]>([]);
const enabledDeptOptions = ref<DeptTreeVO[]>([]); const enabledDeptOptions = ref<DeptTreeVO[]>([]);
const initPassword = ref<string>(''); const initPassword = ref<string>('');
const postOptions = ref<PostVO[]>([]); const postOptions = ref<PostVO[]>([]);
@@ -515,7 +480,6 @@ const columns = ref<FieldOption[]>([
{ key: 6, label: `创建时间`, visible: true, children: [] } { key: 6, label: `创建时间`, visible: true, children: [] }
]); ]);
const treePanelRef = ref<InstanceType<typeof TreePanel>>();
const queryFormRef = ref<ElFormInstance>(); const queryFormRef = ref<ElFormInstance>();
const userFormRef = ref<ElFormInstance>(); const userFormRef = ref<ElFormInstance>();
const uploadRef = ref<ElUploadInstance>(); const uploadRef = ref<ElUploadInstance>();
@@ -550,15 +514,15 @@ const initData: PageData<UserForm, UserQuery> = {
}, },
rules: { rules: {
userName: [ userName: [
{ required: true, message: '用户名称不能为空', trigger: 'blur' }, { required: true, message: '登录账号不能为空', trigger: 'blur' },
{ {
min: 2, min: 2,
max: 20, max: 20,
message: '用户名称长度必须介于 2 和 20 之间', message: '登录账号长度必须介于 2 和 20 之间',
trigger: 'blur' trigger: 'blur'
} }
], ],
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }], nickName: [{ required: true, message: '管理者姓名不能为空', trigger: 'blur' }],
password: [ password: [
{ required: true, message: '用户密码不能为空', trigger: 'blur' }, { required: true, message: '用户密码不能为空', trigger: 'blur' },
{ {
@@ -593,6 +557,9 @@ const initData: PageData<UserForm, UserQuery> = {
const data = reactive<PageData<UserForm, UserQuery>>(initData); const data = reactive<PageData<UserForm, UserQuery>>(initData);
const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data); const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
/** 新增:没有 userId;修改:带 userId */
const isAdd = computed(() => form.value.userId == undefined);
const isEdit = computed(() => !isAdd.value);
const { ids, single, multiple, handleSelectionChange } = useTableSelection<UserVO>(item => item.userId); const { ids, single, multiple, handleSelectionChange } = useTableSelection<UserVO>(item => item.userId);
const { dialog, openDialog: openUserDialog, closeDialog: closeUserDialog, setTitle: setDialogTitle } = useDialogState(); const { dialog, openDialog: openUserDialog, closeDialog: closeUserDialog, setTitle: setDialogTitle } = useDialogState();
@@ -605,14 +572,13 @@ const getList = async () => {
}); });
}; };
/** 查询部门下拉树结构 */ /** 查询部门下拉树结构(仅用于表单的“归属部门”选择) */
const getDeptTree = async () => { const getDeptTree = async () => {
const res = await api.deptTreeSelect(); const res = await api.deptTreeSelect();
deptOptions.value = res.data;
enabledDeptOptions.value = filterDisabledDept(res.data); enabledDeptOptions.value = filterDisabledDept(res.data);
}; };
/** 过滤禁用的部门,并返回独立的新树,避免污染左侧完整部门树 */ /** 过滤禁用的部门 */
const filterDisabledDept = (deptList: DeptTreeVO[]): DeptTreeVO[] => { const filterDisabledDept = (deptList: DeptTreeVO[]): DeptTreeVO[] => {
return deptList.reduce<DeptTreeVO[]>((result, dept) => { return deptList.reduce<DeptTreeVO[]>((result, dept) => {
if (dept.disabled) { if (dept.disabled) {
@@ -626,12 +592,6 @@ const filterDisabledDept = (deptList: DeptTreeVO[]): DeptTreeVO[] => {
}, []); }, []);
}; };
/** 节点单击事件 */
const handleNodeClick = (data: DeptVO) => {
queryParams.value.deptId = data.id;
handleQuery();
};
/** 搜索按钮操作 */ /** 搜索按钮操作 */
const handleQuery = () => { const handleQuery = () => {
queryParams.value.pageNum = 1; queryParams.value.pageNum = 1;
@@ -643,8 +603,6 @@ const { resetQuery } = useSearchReset({
pageNumKey: 'pageNum', pageNumKey: 'pageNum',
resetExtras: () => { resetExtras: () => {
resetDateRange(); resetDateRange();
queryParams.value.deptId = undefined;
treePanelRef.value?.setCurrentKey(undefined);
}, },
afterReset: () => { afterReset: () => {
handleQuery(); handleQuery();
@@ -779,7 +737,7 @@ const cancel = () => {
const handleAdd = async () => { const handleAdd = async () => {
reset(); reset();
const { data } = await api.getUser(); const { data } = await api.getUser();
setDialogTitle('新增用户'); setDialogTitle('新增账户');
openUserDialog(); openUserDialog();
postOptions.value = data.posts; postOptions.value = data.posts;
roleOptions.value = data.roles; roleOptions.value = data.roles;
@@ -791,7 +749,7 @@ const handleUpdate = async (row?: Partial<UserForm>) => {
reset(); reset();
const userId = row?.userId || ids.value[0]; const userId = row?.userId || ids.value[0];
const { data } = await api.getUser(userId); const { data } = await api.getUser(userId);
setDialogTitle('修改用户'); setDialogTitle('修改账户');
openUserDialog(); openUserDialog();
Object.assign(form.value, data.user); Object.assign(form.value, data.user);
postOptions.value = data.posts; postOptions.value = data.posts;
@@ -861,7 +819,8 @@ async function handleDeptChange(value: number | string) {
<style lang="scss" scoped> <style lang="scss" scoped>
@use '@/assets/styles/components/page-shell' as pageShell; @use '@/assets/styles/components/page-shell' as pageShell;
@include pageShell.tree-table-crud-page; @include pageShell.content-stack;
@include pageShell.table-crud-page;
:global(.import-result-box .el-message-box__message) { :global(.import-result-box .el-message-box__message) {
max-height: 70vh; max-height: 70vh;

View File

@@ -30,7 +30,7 @@ export default defineConfig(({ mode, command }) => {
open: true, open: true,
proxy: { proxy: {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
target: 'http://localhost:8080', target: 'http://192.168.0.199:8080',
changeOrigin: true, changeOrigin: true,
ws: true, ws: true,
rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '') rewrite: path => path.replace(new RegExp('^' + env.VITE_APP_BASE_API), '')