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

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_LOGO_TITLE = RuoYi-Vue-Plus
VITE_APP_TITLE = 水电表充值管理平台
VITE_APP_LOGO_TITLE = 水电表充值管理平台
# 开发环境配置
VITE_APP_ENV = 'development'

View File

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

View File

@@ -35,16 +35,14 @@ const getBreadcrumb = () => {
} else {
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);
};
const findPathNum = (str, char = '/') => {
if (typeof str !== 'string' || str.length === 0) return 0;
return str.split(char).length - 1;
};
const getMatched = (pathList, routeList, matched) => {
const data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0]);
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 { redirect, path } = item;
redirect ? router.push(redirect) : router.push(path);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@
<div class="bullshit__info">
对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
</div>
<router-link to="/index" class="bullshit__return-home">返回首页</router-link>
<router-link to="/" class="bullshit__return-home">返回主页</router-link>
</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>
<div class="login">
<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">
<div class="title-box">
<div>
@@ -77,7 +58,7 @@
</router-link>
</div>
<div class="social-panel">
<!-- <div class="social-panel">
<span class="social-label">第三方登录</span>
<div class="social-actions">
<el-button circle :title="$t('login.social.wechat')" @click="doSocialLogin('wechat')">
@@ -96,7 +77,7 @@
<svg-icon icon-class="github" />
</el-button>
</div>
</div>
</div> -->
<el-form-item class="submit-row">
<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 currentYear = new Date().getFullYear();
const quickStats = [
{ label: '细粒度权限管理', value: '动态权限控制' },
{ label: '主流技术栈', value: '全栈技术集成' },
{ label: 'UI样式', value: '卡片式' }
];
const highlights = ['技术栈全面升级', '动态菜单', '多主题布局', '深浅色主题'];
const userStore = useUserStore();
const router = useRouter();
const { t } = useI18n();
const loginForm = ref<LoginData>({
username: 'admin',
password: 'admin123',
username: '',
password: '',
rememberMe: false,
code: '',
uuid: ''
@@ -226,8 +201,9 @@ const getLoginData = () => {
const rememberMe = localStorage.getItem('rememberMe');
localStorage.removeItem('password');
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'
} as LoginData;
};
@@ -264,102 +240,18 @@ onMounted(() => {
.login-shell {
width: min(1180px, 100%);
display: grid;
grid-template-columns: minmax(0, 1.05fr) minmax(360px, 440px);
grid-template-columns: minmax(0, 440px);
justify-content: center;
gap: 26px;
align-items: stretch;
}
.login-brand,
.login-form {
width: 100%;
border-radius: 28px;
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 30px 80px rgba(2, 8, 23, 0.32);
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;
z-index: 1;
background: var(--app-surface-bg);
@@ -536,25 +428,9 @@ onMounted(() => {
.login {
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) {
.login-brand {
display: none;
}
.login-form {
padding: 26px 18px 20px;
}

View File

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

View File

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