公共服务平台-大屏代码。现在废弃,改成其他项目开发

This commit is contained in:
lSun 2025-09-09 10:08:10 +08:00
parent f1c606fa4b
commit 8bd21926c8
11 changed files with 2498 additions and 150 deletions

View File

@ -20,7 +20,7 @@ export function getMenu(menuId) {
// 查询菜单下拉树结构
export function treeselect() {
return request({
url: '/system/menu/treeselect',
url: '/system/menu/treeselectNew',
method: 'get'
})
}
@ -28,7 +28,7 @@ export function treeselect() {
// 根据角色ID查询菜单下拉树结构
export function roleMenuTreeselect(roleId) {
return request({
url: '/system/menu/roleMenuTreeselect/' + roleId,
url: '/system/menu/roleMenuTreeselectNew/' + roleId,
method: 'get'
})
}
@ -57,4 +57,4 @@ export function delMenu(menuId) {
url: '/system/menu/' + menuId,
method: 'delete'
})
}
}

View File

@ -68,9 +68,9 @@ export const constantRoutes = [
children: [
{
path: 'index',
component: () => import('@/views/index'),
component: () => import('@/views/psp/productCenter/index'),
name: 'Index',
meta: { title: '首页', icon: 'dashboard', affix: true }
meta: { title: '公共服务平台', icon: 'dashboard', affix: true }
}
]
},
@ -88,27 +88,48 @@ export const constantRoutes = [
}
]
},
// 在路由配置中导航条的路由
{
path: '/',
component: Layout, // 布局组件,包含左侧菜单和顶部导航
hidden: true, // 隐藏左侧菜单项
children: [
{
path: '/productCenter/index',
component: () => import('@/views/psp/productCenter/index.vue')
},
{
path: '/common/index',
component: () => import('@/views/psp/common/index.vue')
},
// ... 其他子路由
]
},
// 在路由配置中添加产品详情页面路由
{
path: '/psp/productCenter',
component: Layout,
redirect: '/psp/productCenter/index',
name: 'ProductCenter',
meta: { title: '产品中心', icon: 'product' },
meta: { title: '产品中心', icon: 'product', },
hidden: true, // 隐藏左侧菜单项
children: [
{
path: 'index',
name: 'ProductCenterIndex',
component: () => import('@/views/psp/productCenter/index'),
meta: { title: '产品中心', icon: 'product' }
},
{
path: 'detail/:id(\\d+)',
name: 'ProductDetail',
component: () => import('@/views/psp/productCenter/product-detail'),
meta: { title: '产品详情', activeMenu: '/psp/productCenter/index' },
hidden: true
},
{
path: 'case/:id(\\d+)',
name: 'ProductCase',
component: () => import('@/views/psp/productCenter/product-case'),
meta: { title: '产品案例', activeMenu: '/psp/productCenter/index' },
}
]
}
]

View File

@ -0,0 +1,457 @@
<template>
<div class="platform-container">
<!-- 使用导航栏组件 -->
<nav-bar
:active-nav="activeNav"
:search-keyword.sync="searchKeyword"
@search="handleSearch">
</nav-bar>
<!-- 主体内容 -->
<div class="main-content">
<!-- 左侧筛选栏 -->
<aside class="sidebar">
<div class="filter-section">
<h3 class="filter-title">
<i class="el-icon-sort"></i> 排序方式
</h3>
<ul class="filter-options">
<li class="filter-option" :class="{ active: sortType === 'publish' }">
<el-radio v-model="sortType" label="publish">最新发布</el-radio>
</li>
<li class="filter-option" :class="{ active: sortType === 'download' }">
<el-radio v-model="sortType" label="download">下载次数</el-radio>
</li>
</ul>
</div>
<div class="filter-section">
<h3 class="filter-title">
<i class="el-icon-menu"></i> 组件类型
</h3>
<ul class="filter-options">
<li class="filter-option" v-for="type in componentTypes" :key="type.value">
<el-checkbox v-model="selectedTypes" :label="type.value">{{ type.label }}</el-checkbox>
</li>
</ul>
</div>
</aside>
<!-- 右侧内容区域 -->
<main class="content-area">
<div class="component-grid">
<!-- 组件卡片 -->
<div class="component-card" v-for="component in filteredComponents" :key="component.id">
<div class="card-header">
<img :src="apiBase + component.image" alt="Component Image" class="card-image">
<!-- 名称和版本在同一行 -->
<div class="card-title-wrapper">
<h3 class="card-title">{{ component.name }}</h3>
<div v-if="showVersion" class="card-version">{{ component.version }}</div>
</div>
</div>
<div class="card-footer">
<button class="card-action-btn" style="background: linear-gradient( 180deg, #00C7EF 0%, #005EEF 100%);color: white; " @click="downloadComponent(component)">
下载
</button>
<button class="card-action-btn" @click="showComments(component)">
评论
</button>
<!-- <button class="card-action-btn" @click="viewDocument(component)">
文档
</button>-->
<div class="dropdown">
<button class="card-action-btn" @click="toggleDropdown(component)">
文档
</button>
<ul class="dropdown-menu" v-if="activeComponentId === component.id">
<li @click="viewDocument(component)">预览</li>
<li @click="downloadDocument(component)">下载</li>
</ul>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
</template>
<script>
import NavBar from '@/views/psp/navBar.vue'
export default {
name: "index.vue",
components: {
NavBar,
},
data() {
return {
apiBase: process.env.VUE_APP_BASE_API,
activeNav: 'components',
searchKeyword: '',
sortType: 'publish',
selectedTypes: [],
showVersion: true, //
componentTypes: [
{ value: 'data-display', label: '数据展示' },
{ value: 'form-control', label: '表单控件' },
{ value: 'layout-container', label: '布局容器' },
{ value: 'navigation', label: '导航组件' }
],
components: [
{
id: 1,
name: '数据表格组件',
version: 'V2.1.2',
type: 'data-display',
downloads: 1250,
publishDate: '2023-06-15',
image: '/profile/avatar/2025/09/03/001_20250903132300A001.JPG',
},
{
id: 2,
name: '报表可视化',
version: 'V2.1.2',
type: 'data-display',
downloads: 980,
publishDate: '2023-05-20',
image: '/profile/avatar/2025/09/03/ldst.png',
},
{
id: 3,
name: '布局容器',
version: 'V2.1.2',
type: 'layout-container',
downloads: 750,
publishDate: '2023-07-10',
image: '/profile/avatar/2025/09/03/001_20250903132300A001.JPG',
},
{
id: 4,
name: '数据接入组件-交互',
version: 'V2.1.2',
type: 'data-display',
downloads: 620,
publishDate: '2023-04-05',
image: '/profile/avatar/2025/09/03/ldst.png',
},
{
id: 5,
name: '表单控件',
version: 'V2.1.2',
type: 'form-control',
downloads: 1100,
publishDate: '2023-08-12',
image: '/profile/avatar/2025/09/03/ldst.png',
}
]
}
},
computed: {
filteredComponents() {
let filtered = this.components;
//
if (this.selectedTypes.length > 0) {
filtered = filtered.filter(component =>
this.selectedTypes.includes(component.type)
);
}
//
if (this.sortType === 'download') {
filtered = filtered.sort((a, b) => b.downloads - a.downloads);
} else {
filtered = filtered.sort((a, b) =>
new Date(b.publishDate) - new Date(a.publishDate)
);
}
return filtered;
}
},
methods: {
handleSearch(keyword) {
this.$message({
message: `搜索关键词: ${keyword}`,
type: 'success'
});
},
downloadComponent(component) {
this.$message({
message: `开始下载 ${component.name}`,
type: 'success'
});
},
showComments(component) {
this.$message({
message: `查看 ${component.name} 的评论`,
type: 'info'
});
},
viewDocument(component) {
this.$message({
message: `查看 ${component.name} 的文档`,
type: 'info'
});
this.activeComponentId = null; //
},
toggleDropdown(component) {
if (this.activeComponentId === component.id) {
this.activeComponentId = null; //
} else {
this.activeComponentId = component.id; //
}
},
downloadDocument(component) {
this.$message({
message: `开始下载 ${component.name} 的文档`,
type: 'success'
});
this.activeComponentId = null; //
}
}
}
</script>
<style scoped>
.platform-container {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
background-color: #FFFFFF;
}
.user-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
}
/* 主体内容 */
.main-content {
display: flex;
width: 100%;
margin: 20px auto;
padding: 0 20px;
gap: 20px;
flex: 1;
}
/* 左侧筛选栏 */
.sidebar {
width: 250px;
flex-shrink: 0;
background:transparent;
}
.filter-section {
background: #F6F6F6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.filter-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 15px;
color: #303133;
display: flex;
align-items: center;
}
.filter-title i {
margin-right: 8px;
color: #409EFF;
}
.filter-options {
list-style: none;
}
.filter-option {
padding: 10px 0;
cursor: pointer;
display: flex;
align-items: center;
transition: color 0.2s;
}
.filter-option:hover {
color: #409EFF;
}
.filter-option.active {
color: #409EFF;
font-weight: 500;
}
.filter-option .el-checkbox {
width: 100%;
}
.card-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 8px 8px 8px 8px;
}
/* 名称和版本的容器 */
.card-title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10px 0 5px;
}
/* 右侧内容区域 */
.content-area {
flex: 1;
}
.component-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.component-card {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.3s, box-shadow 0.3s;
}
.component-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
}
.card-header {
padding: 10px;
border-bottom: 1px solid #ebeef5;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #303133;
margin: 0;
flex: 1;
}
.card-version {
font-size: 12px;
color: #909399;
background-color: #f0f2f5;
padding: 2px 8px;
border-radius: 4px;
margin-left: 10px;
}
.card-footer {
padding: 10px;
display: flex;
justify-content: space-between;
gap: 45px; /* 增大按钮间距 */
}
.card-action-btn {
flex: 1;
background-color: #ffffff;
color: #2B2B2B;
border: 1px solid #58D9F4; /* 定义边框宽度和透明度 */
border-radius: 4px ;
padding: 4px 0;
font-size: 13px;
cursor: pointer;
transition: all 0.3s ease;
text-align: center;
min-height: 28px;
}
.card-action-btn:hover {
background-color: #ecf5ff;
color: #409eff;
border-color: #409eff;
}
.card-action-btn:active {
background-color: #409eff;
color: #ffffff;
border-color: #409eff;
}
.card-action {
color: #409eff;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
}
.card-action i {
margin-right: 5px;
}
.card-action:hover {
color: #66b1ff;
}
.dropdown {
position: relative;
display: inline-block;
}
.dropdown-menu {
display: block;
position: absolute;
top: 100%;
left: 0;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
list-style-type: none;
padding: 0;
margin: 0;
min-width: 100px;
z-index: 1;
}
.dropdown-menu li {
padding: 8px 16px;
cursor: pointer;
}
.dropdown-menu li:hover {
background-color: #f1f1f1;
}
/* 响应式设计 */
@media (max-width: 768px) {
.main-content {
flex-direction: column;
}
.sidebar {
width: 100%;
}
.component-grid {
grid-template-columns: 1fr;
}
}
</style>

254
src/views/psp/navBar.vue Normal file
View File

@ -0,0 +1,254 @@
<template>
<div class="header">
<div class="header-content">
<div class="header-left">
<div class="logo-section">
<img :src="logoUrl" alt="Logo" class="logo"/>
<span class="platform-title">{{ platformTitle }}</span>
</div>
</div>
<div class="header-nav">
<div
v-for="item in navItems"
:key="item.key"
class="nav-item"
:class="{ active: activeNav === item.key }"
@click="handleNavClick(item)"
>
<img :src="item.icon" :alt="item.label" class="nav-icon"/>
<span class="nav-text">{{ item.label }}</span>
</div>
</div>
<div v-if="showRightSection" class="header-right">
<!-- <div v-if="showSearch" class="search-box">
<input
v-model="internalSearchKeyword"
type="text"
placeholder="输入关键词搜索"
class="search-input"
@keyup.enter="handleSearch"
/>
<button class="search-btn" @click="handleSearch">
<i class="el-icon-search"></i>
</button>
</div>
<div v-if="showUserAvatar" class="user-avatar">
<img :src="$store.state.user.avatar" alt="用户头像" />
</div>-->
</div>
</div>
</div>
</template>
<script>
export default {
name: 'NavBar',
props: {
logoUrl: {
type: String,
default: '/img/psp/productCenter/logo.png'
},
platformTitle: {
type: String,
default: '公共服务平台'
},
navItems: {
type: Array,
default: () => [
{ key: 'products', label: '产品中心', icon: '/img/psp/productCenter/products.png',route: '/productCenter/index' },
{ key: 'components', label: '公共组件', icon: '/img/psp/productCenter/components.png',route: '/common/index' },
{ key: 'materials', label: '宣传物料', icon: '/img/psp/productCenter/materials.png',route: '/materials/index'},
{ key: 'docs', label: '文档中心', icon: '/img/psp/productCenter/docs.png',route: '/docs/index'}
]
},
activeNav: {
type: String,
default: 'products'
},
showRightSection: {
type: Boolean,
default: true
},
showSearch: {
type: Boolean,
default: true
},
showUserAvatar: {
type: Boolean,
default: true
},
userAvatar: {
type: String,
default: ''
},
searchKeyword: {
type: String,
default: ''
}
},
data() {
return {
internalSearchKeyword: this.searchKeyword,
userAvatarUrl: this.userAvatar,
}
},
watch: {
searchKeyword(newVal) {
this.internalSearchKeyword = newVal;
},
userAvatar(newVal) {
this.userAvatarUrl = newVal;
}
},
methods: {
handleNavClick(item) {
//
this.$emit('nav-change', item.key);
//
if (item.route) {
this.$router.push(item.route);
}
},
handleSearch() {
// 使internalSearchKeyword
this.$emit('update:searchKeyword', this.internalSearchKeyword);
this.$emit('search', this.internalSearchKeyword);
},
}
}
</script>
<style scoped>
.header {
background-image: url("/img/psp/productCenter/topbg.png");
color: white;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 100vw;
margin: 0 auto;
padding: 12px 20px;
}
.header-left {
display: flex;
align-items: center;
}
.logo-section {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 32px;
height: 32px;
border-radius: 6px;
}
.platform-title {
font-size: 18px;
font-weight: 600;
color: white;
}
.header-nav {
display: flex;
gap: 32px;
justify-content: space-between;
}
.nav-item {
padding: 8px 16px;
cursor: pointer;
border-radius: 6px;
transition: all 0.2s ease;
font-size: 14px;
display: flex;
align-items: center;
}
.nav-icon {
width: 20px;
height: 20px;
object-fit: contain;
margin-right: 10px;
flex-shrink: 0;
}
.nav-text {
white-space: nowrap;
}
.nav-item:hover,
.nav-item.active {
background-color: rgba(255, 255, 255, 0.15);
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-box {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.15);
border-radius: 20px;
padding: 6px 12px;
}
.search-input {
background: transparent;
border: none;
outline: none;
color: white;
font-size: 14px;
width: 200px;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.7);
}
.search-btn {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 4px;
}
.user-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.3);
}
/* 响应式设计 */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 12px;
padding: 16px;
}
.header-nav {
gap: 16px;
}
.search-input {
width: 150px;
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="platform-container">
<!-- 顶部导航栏 -->
<div class="header">
<!-- <div class="header">
<div class="header-content">
<div class="header-left">
<div class="logo-section">
@ -41,7 +41,14 @@
</div>
</div>
</div>
</div>
</div>-->
<!-- 使用导航栏组件 -->
<NavBar
:active-nav="activeNav"
:search-keyword.sync="searchKeyword"
@nav-change="handleNavChange"
/>
<!-- 主体内容 -->
<div class="main-content">
@ -96,8 +103,14 @@
</template>
<script>
//
import NavBar from '@/views/psp/navBar.vue'
export default {
name: 'PlatformIndex',
components: {
NavBar
},
data() {
return {
activeNav: 'products',
@ -199,10 +212,11 @@ export default {
}
},
methods: {
handleSearch() {
console.log('搜索关键词:', this.searchKeyword)
// computed
handleNavChange(navKey) {
this.activeNav = navKey
//
},
handleDemo(service) {
console.log('访问演示:', service.title)
//

View File

@ -0,0 +1,873 @@
<template>
<div class="product-detail">
<!-- 顶部导航栏 -->
<!-- 使用导航栏组件 -->
<NavBar
:active-nav="activeNav"
:search-keyword.sync="searchKeyword"
@nav-change="handleNavChange"
/>
<!-- Added breadcrumb navigation with back button -->
<div class="breadcrumb">
<div class="breadcrumb-nav">
<span class="breadcrumb-link" >产品中心</span>
<span class="breadcrumb-separator">></span>
<span class="breadcrumb-current">产品详情</span>
<span class="breadcrumb-separator">></span>
<span class="breadcrumb-current">案例集</span>
</div>
<div>
<i class="arrow-left-icon"></i>
<button class="btn-edit" @click="goBack">返回</button>
</div>
</div>
<div class="main-content">
<div class="section product-intro">
<div class="intro-content">
<div class="intro-image">
<img :src="apiBase + intro" alt="安徽送变电工程有限公司" />
</div>
<div class="intro-text">
<div class="intro-header">
<h2>安徽送变电工程有限公司</h2>
</div>
<p>
基于移动互联网微信APP超前的互联网思维在线订餐互动点评由内到外的方方面面的管理功能硬件完整兼容版本无缝衔接适用于多种食堂经营模式单位自营 承包经营自由消费固定消费 计次消费自提派送食堂就餐多样餐补方式整体提升食堂服务和管理的质量打造名副其实与时俱进的互联网食堂
</p>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
//
import NavBar from '@/views/psp/navBar.vue'
export default {
name: 'ProductDetail',
components: {
NavBar
},
data() {
return {
activeNav: 'products',
searchKeyword: '',
brochureIndex: 0,
videoIndex: 0,
caseIndex: 0,
itemsPerView: 4,
autoPlayInterval: null,
brochureOffset: 0,
videoOffset: 0,
brochureAutoInterval: null,
videoAutoInterval: null,
brochurePaused: false,
videoPaused: false,
scrollSpeed: 0.5, // pixels per frame
apiBase: process.env.VUE_APP_BASE_API,
intro :"/profile/avatar/2025/09/03/ldst.png",
navItems: [
{ key: 'products',label: '产品中心', icon: '/img/psp/productCenter/products.png' },
{ key: 'components', label: '公共组件', icon: '/img/psp/productCenter/components.png' },
{ key: 'materials', label: '宣传物料', icon: '/img/psp/productCenter/materials.png'},
{ key: 'docs', label: '文档中心', icon: '/img/psp/productCenter/docs.png'}
],
brochures: [
{ title: '智慧食堂产品手册1', image: '/profile/avatar/2025/09/03/ldst.png' },
{ title: '智慧食堂产品手册2', image: '/profile/avatar/2025/09/03/001_20250903132300A001.jpg' },
{ title: '智慧食堂产品手册3', image: '/profile/avatar/2025/09/03/ldst.png' },
{ title: '智慧食堂产品手册4', image: '/profile/avatar/2025/09/03/001_20250903132300A001.jpg' },
{ title: '智慧食堂产品手册5', image: '/profile/avatar/2025/09/03/ldst.png' },
{ title: '智慧食堂产品手册6', image: '/profile/avatar/2025/09/03/001_20250903132300A001.jpg' }
],
videos: [
{ title: '智慧食堂产品视频1', thumbnail: '/profile/avatar/2025/09/03/vi1.mp4' },
{ title: '智慧食堂产品视频2', thumbnail: '/profile/avatar/2025/09/03/vi2.mp4' },
{ title: '智慧食堂产品视频3', thumbnail: '/profile/avatar/2025/09/03/vi1.mp4' },
{ title: '智慧食堂产品视频4', thumbnail: '/profile/avatar/2025/09/03/vi2.mp4' },
{ title: '智慧食堂产品视频5', thumbnail: '/profile/avatar/2025/09/03/vi1.mp4' },
{ title: '智慧食堂产品视频6', thumbnail: '/profile/avatar/2025/09/03/vi2.mp4' }
],
cases: [
{
company: '安徽送变电工程有限公司',
description: '基于移动互联网(微信、APP),整合所有数据资源(在线订餐、充值、点评),由内到外的为学校师生提供便民服务,深度定制餐厅、校本文档精准、适用于智慧食堂数据资源,通过设备、消费设备、自助充值、智慧收银、零售管理、多样化计方式),致力提升学校整体服务和管理的能力,打造名副其实与时俱进的互联网餐厅。',
image: '/profile/avatar/2025/09/03/ldst.png?height=280&width=400'
},
{
company: '北京科技大学智慧食堂',
description: '通过智慧食堂系统的实施学校食堂管理效率提升了40%学生满意度达到95%以上,实现了真正的数字化转型,为师生提供更便捷的用餐体验。',
image: '/profile/avatar/2025/09/03/001_20250903132300A001.jpg?height=280&width=400'
},
{
company: '上海交通大学',
description: '采用先进的人工智能技术,实现了食堂的智能化管理,包括智能点餐、营养分析、库存管理等功能,大大提升了运营效率。',
image: '/profile/avatar/2025/09/03/ldst.png?height=280&width=400'
}
],
animationFrame: null
}
},
created() {
this.productId = this.$route.params.id
console.log('产品ID:', this.productId)
},
computed: {
maxBrochureIndex() {
return Math.max(0, this.brochures.length - this.itemsPerView)
},
maxVideoIndex() {
return Math.max(0, this.videos.length - this.itemsPerView)
},
maxCaseIndex() {
return Math.max(0, this.cases.length - 1)
},
displayBrochures() {
return [...this.brochures, ...this.brochures, ...this.brochures]
},
displayVideos() {
return [...this.videos, ...this.videos, ...this.videos]
},
brochureTrackStyle() {
const itemWidth = 280 // approximate width including gap
return {
transform: `translateX(-${this.brochureOffset}px)`,
transition: 'none'
}
},
videoTrackStyle() {
const itemWidth = 280 // approximate width including gap
return {
transform: `translateX(-${this.videoOffset}px)`,
transition: 'none'
}
},
caseTrackStyle() {
return {
transform: `translateX(-${this.caseIndex * 100}%)`
}
}
},
mounted() {
this.startAutoPlay()
this.startSmoothScroll()
this.handleResize()
window.addEventListener('resize', this.handleResize)
},
beforeDestroy() {
this.stopAutoPlay()
this.stopSmoothScroll()
window.removeEventListener('resize', this.handleResize)
},
methods: {
handleNavChange(navKey) {
this.activeNav = navKey
//
},
goBack() {
this.$router.go(-1)
},
prevBrochure() {
const containerWidth = this.$refs.brochureCarousel.offsetWidth
this.brochureOffset -= containerWidth
if (this.brochureOffset < 0) {
this.brochureOffset = this.brochures.length * 280
}
},
nextBrochure() {
const containerWidth = this.$refs.brochureCarousel.offsetWidth
this.brochureOffset += containerWidth
const maxOffset = this.brochures.length * 280
if (this.brochureOffset >= maxOffset * 2) {
this.brochureOffset = maxOffset
}
},
prevVideo() {
const containerWidth = this.$refs.videoCarousel.offsetWidth
this.videoOffset -= containerWidth
if (this.videoOffset < 0) {
this.videoOffset = this.videos.length * 280
}
},
nextVideo() {
const containerWidth = this.$refs.videoCarousel.offsetWidth
this.videoOffset += containerWidth
const maxOffset = this.videos.length * 280
if (this.videoOffset >= maxOffset * 2) {
this.videoOffset = maxOffset
}
},
prevCase() {
if (this.caseIndex > 0) {
this.caseIndex--
}
},
nextCase() {
if (this.caseIndex < this.maxCaseIndex) {
this.caseIndex++
}
},
startAutoPlay() {
this.autoPlayInterval = setInterval(() => {
// Auto-advance cases every 5 seconds
if (this.caseIndex < this.maxCaseIndex) {
this.caseIndex++
} else {
this.caseIndex = 0
}
}, 5000)
},
stopAutoPlay() {
if (this.autoPlayInterval) {
clearInterval(this.autoPlayInterval)
this.autoPlayInterval = null
}
},
startSmoothScroll() {
const animate = () => {
if (!this.brochurePaused) {
this.brochureOffset += 0.5
const itemWidth = 280
const maxOffset = this.brochures.length * itemWidth
if (this.brochureOffset >= maxOffset * 2) {
this.brochureOffset = maxOffset
}
}
if (!this.videoPaused) {
this.videoOffset += 0.5
const itemWidth = 280
const maxOffset = this.videos.length * itemWidth
if (this.videoOffset >= maxOffset * 2) {
this.videoOffset = maxOffset
}
}
this.animationFrame = requestAnimationFrame(animate)
}
const itemWidth = 280
this.brochureOffset = this.brochures.length * itemWidth
this.videoOffset = this.videos.length * itemWidth
this.animationFrame = requestAnimationFrame(animate)
},
stopSmoothScroll() {
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame)
this.animationFrame = null
}
},
pauseBrochureScroll() {
this.brochurePaused = true
},
resumeBrochureScroll() {
this.brochurePaused = false
},
pauseVideoScroll() {
this.videoPaused = true
},
resumeVideoScroll() {
this.videoPaused = false
},
startInfiniteScroll() {
},
stopInfiniteScroll() {
},
playVideo(video) {
console.log('Playing video:', video.title)
//
},
handleVideoError(event) {
console.log('[v0] Video loading error:', event.target.src)
//
event.target.style.display = 'none'
const img = document.createElement('img')
img.src = '/placeholder.svg?height=180&width=240'
img.alt = 'Video thumbnail'
img.style.width = '100%'
img.style.height = '100%'
img.style.objectFit = 'cover'
event.target.parentNode.appendChild(img)
},
handleVideoLoaded(event) {
console.log('[v0] Video metadata loaded:', event.target.src)
//
}
}
}
</script>
<style scoped>
.product-detail {
height: 100%;
}
/* 顶部导航栏 */
.header {
background-image: url("/img/psp/productCenter/topbg.png");
color: white;
padding: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
max-width: 100vw;
margin: 0 auto;
padding: 12px 20px;
}
.header-left {
display: flex;
align-items: center;
}
.logo-section {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
width: 32px;
height: 32px;
border-radius: 6px;
}
.platform-title {
color: white;
font-size: 18px;
font-weight: 600;
}
.header-nav {
display: flex;
gap: 32px;
justify-content: space-between;
}
.nav-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
border-radius: 6px;
transition: all 0.3s ease;
}
.nav-item:hover,
.nav-item.active {
color: white;
background-color: rgba(255, 255, 255, 0.1);
}
.nav-icon {
width: 20px;
height: 20px;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
.search-box {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 20px;
padding: 6px 12px;
}
.search-input {
background: none;
border: none;
color: white;
outline: none;
width: 200px;
font-size: 14px;
}
.search-input::placeholder {
color: rgba(255, 255, 255, 0.6);
}
.search-btn {
background: none;
border: none;
cursor: pointer;
padding: 4px;
}
.search-btn img {
width: 16px;
height: 16px;
filter: brightness(0) invert(1);
}
.user-avatar img {
width: 32px;
height: 32px;
border-radius: 50%;
}
/* Enhanced breadcrumb styling with back button */
.breadcrumb {
padding: 16px 24px;
background: #f8f9fa;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e8eaec;
}
.breadcrumb-nav {
display: flex;
align-items: center;
gap: 12px;
}
.back-button {
background: white;
border: 1px solid #d1d5db;
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
color: #374151;
transition: all 0.2s ease;
}
.back-button:hover {
background: #f3f4f6;
border-color: #9ca3af;
}
.arrow-left-icon {
font-size: 20px;
font-weight: bold;
color: #4A90E2;
}
.breadcrumb-link {
text-decoration: none;
font-size: 14px;
}
.breadcrumb-separator {
margin: 0 4px;
color: #9ca3af;
font-size: 14px;
}
.breadcrumb-current {
color: #374151;
font-size: 14px;
font-weight: 500;
}
.btn-edit {
color: #4A90E2;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
background: transparent;
}
/* Main Content */
.main-content {
padding: 24px;
background: #f8f9fa;
}
.section {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
border: 1px solid #e5e7eb;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.section-header h3 {
font-size: 20px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.view-more {
color: #4A90E2;
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.view-more:hover {
text-decoration: underline;
}
/* Product Introduction */
.product-intro {
border: none;
box-shadow: 0 4px 20px rgba(74, 144, 226, 0.08);
}
.intro-content {
display: flex;
gap: 48px;
align-items: center;
min-height: 320px;
}
.intro-image {
flex: 0 0 400px;
}
.intro-image img {
width: 100%;
height: 280px;
object-fit: cover;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(74, 144, 226, 0.12);
}
.intro-text {
flex: 1;
}
.intro-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.intro-text h2 {
font-size: 28px;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.intro-text p {
color: #4b5563;
line-height: 1.8;
margin: 0;
font-size: 15px;
}
/* Enhanced button styling */
.btn-primary {
background: linear-gradient(135deg, #4A90E2 0%, #357ABD 100%);
color: white;
border: none;
padding: 12px 28px;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.25);
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(74, 144, 226, 0.35);
}
/* Enhanced carousel styling for infinite loop */
.carousel-container {
position: relative;
display: flex;
align-items: center;
gap: 20px;
}
.carousel-btn {
background: white;
border: 2px solid #e5e7eb;
width: 44px;
height: 44px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
z-index: 2;
}
.carousel-btn:hover {
background: #4A90E2;
border-color: #4A90E2;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(74, 144, 226, 0.25);
}
.carousel-btn:hover .arrow-left,
.carousel-btn:hover .arrow-right {
border-color: white;
}
/* Added arrow icons */
.arrow-left,
.arrow-right {
width: 0;
height: 0;
border-style: solid;
transition: border-color 0.3s ease;
}
.arrow-left {
border-width: 6px 8px 6px 0;
border-color: transparent #6b7280 transparent transparent;
}
.arrow-right {
border-width: 6px 0 6px 8px;
border-color: transparent transparent transparent #6b7280;
}
.carousel-content {
flex: 1;
overflow: hidden;
border-radius: 12px;
}
.carousel-track {
display: flex;
gap: 20px;
/* Remove transition as we're using smooth animation */
}
/* Enhanced carousel items */
.carousel-item {
flex: 0 0 260px; /* Fixed width instead of percentage */
text-align: center;
}
.brochure-item .item-image,
.video-item .item-image {
position: relative;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.brochure-item:hover .item-image,
.video-item:hover .item-image {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.item-image img {
width: 100%;
height: 180px;
object-fit: cover;
}
.item-title {
font-size: 14px;
color: #374151;
margin: 16px 0 0 0;
font-weight: 500;
}
/* Enhanced video styling */
.video-thumbnail {
position: relative;
cursor: pointer;
}
.play-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.3);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease;
}
.video-item:hover .play-overlay {
opacity: 1;
}
.play-button {
background: rgba(74, 144, 226, 0.9);
color: white;
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
transition: all 0.3s ease;
}
.play-button:hover {
background: #357ABD;
transform: scale(1.1);
}
.play-icon {
margin-left: 3px;
}
/* Enhanced case items */
.cases-carousel .carousel-track {
gap: 0;
}
.case-item {
display: flex;
gap: 32px;
align-items: center;
width: 100%;
flex: 0 0 100%;
padding: 32px;
background: linear-gradient(135deg, #f8fbff 0%, #ffffff 100%);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.06);
}
.case-image {
flex: 0 0 400px;
}
.case-image img {
width: 100%;
height: 280px;
object-fit: cover;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.case-content {
flex: 1;
padding-left: 16px;
}
.case-content h4 {
font-size: 22px;
font-weight: 600;
color: #1f2937;
margin: 0 0 20px 0;
}
.case-content p {
color: #4b5563;
line-height: 1.8;
margin: 0;
font-size: 15px;
}
/* Enhanced responsive design */
@media (max-width: 1024px) {
.carousel-item {
flex: 0 0 calc(33.333% - 14px);
}
}
@media (max-width: 768px) {
.breadcrumb {
padding: 12px 16px;
}
.breadcrumb-nav {
gap: 8px;
}
.main-content {
padding: 16px;
}
.intro-content {
flex-direction: column;
gap: 24px;
text-align: center;
}
.intro-image {
flex: none;
width: 100%;
}
.intro-header {
flex-direction: column;
gap: 16px;
align-items: center;
}
.carousel-item {
flex: 0 0 calc(50% - 10px);
}
.case-item {
flex-direction: column;
gap: 20px;
padding: 24px;
}
.case-image {
flex: none;
width: 100%;
}
.case-content {
padding-left: 0;
text-align: center;
}
}
@media (max-width: 480px) {
.carousel-container {
gap: 12px;
}
.carousel-btn {
width: 36px;
height: 36px;
}
.carousel-item {
flex: 0 0 calc(100% - 0px);
}
}
</style>

View File

@ -1,47 +1,12 @@
<template>
<div class="product-detail">
<!-- 顶部导航栏 -->
<div class="header">
<div class="header-content">
<div class="header-left">
<div class="logo-section">
<img src="/img/psp/productCenter/logo.png" alt="Logo" class="logo"/>
<span class="platform-title">公共服务平台</span>
</div>
</div>
<div class="header-nav">
<div
v-for="item in navItems"
:key="item.key"
class="nav-item"
:class="{ active: activeNav === item.key }"
@click="activeNav = item.key"
>
<img :src="item.icon" :alt="item.label" class="nav-icon"/>
<span class="nav-text">{{ item.label }}</span>
</div>
</div>
<div class="header-right">
<!-- <div class="search-box">
<input
v-model="searchKeyword"
type="text"
placeholder="输入关键词搜索"
class="search-input"
@keyup.enter="handleSearch"
/>
<button class="search-btn" @click="handleSearch">
<i class="el-icon-search"></i>
</button>
</div>
<div class="user-avatar">
<img :src="$store.state.user.avatar" alt="用户头像"/>
</div>-->
</div>
</div>
</div>
<!-- 使用导航栏组件 -->
<NavBar
:active-nav="activeNav"
:search-keyword.sync="searchKeyword"
@nav-change="handleNavChange"
/>
<!-- Added breadcrumb navigation with back button -->
<div class="breadcrumb">
@ -173,7 +138,7 @@
<div class="section">
<div class="section-header">
<h3>产品案例</h3>
<a href="#" class="view-more">查看更多</a>
<a href="#" class="view-more" @click.prevent="viewCase">查看更多</a>
</div>
<div class="carousel-container cases-carousel">
<button class="carousel-btn prev" @click="prevCase" :disabled="caseIndex === 0">
@ -206,8 +171,14 @@
</template>
<script>
//
import NavBar from '@/views/psp/navBar.vue'
export default {
name: 'ProductDetail',
components: {
NavBar
},
data() {
return {
activeNav: 'products',
@ -320,6 +291,10 @@ export default {
window.removeEventListener('resize', this.handleResize)
},
methods: {
handleNavChange(navKey) {
this.activeNav = navKey
//
},
goBack() {
this.$router.go(-1)
},
@ -453,7 +428,17 @@ export default {
handleVideoLoaded(event) {
console.log('[v0] Video metadata loaded:', event.target.src)
//
},
viewCase() {
// ID
// this.$router.push(`/product-detail/${this.productId}`);
this.$router.push({
name: 'ProductCase',
params: { id: `${this.productId}`}
})
}
}
}
</script>

View File

@ -70,6 +70,12 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope">
<el-button
size="mini"
type="text"
icon="el-icon-search"
@click="handleViewUsers(scope.row)"
>查看人员</el-button>
<el-button
size="mini"
type="text"
@ -306,6 +312,14 @@ export default {
})
})
},
/** 查看部门人员操作 */
handleViewUsers(row) {
// ID
this.$router.push({
path: '/system/user',
query: { deptId: row.deptId , deptName: row.deptName }
})
},
/** 提交按钮 */
submitForm: function() {
this.$refs["form"].validate(valid => {

View File

@ -120,6 +120,14 @@
</el-table-column>
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<template slot-scope="scope" v-if="scope.row.roleId !== 1">
<el-button
size="mini"
type="text"
icon="el-icon-search"
@click="handleViewUsers(scope.row)"
>查看人员</el-button>
<el-button
size="mini"
type="text"
@ -138,9 +146,9 @@
<el-button size="mini" type="text" icon="el-icon-d-arrow-right">更多</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="handleDataScope" icon="el-icon-circle-check"
v-hasPermi="['system:role:edit']">数据权限</el-dropdown-item>
v-hasPermi="['system:role:edit']">数据权限</el-dropdown-item>
<el-dropdown-item command="handleAuthUser" icon="el-icon-user"
v-hasPermi="['system:role:edit']">分配用户</el-dropdown-item>
v-hasPermi="['system:role:edit']">分配用户</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
@ -156,7 +164,7 @@
/>
<!-- 添加或修改角色配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
<el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
@ -183,19 +191,51 @@
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限">
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
<el-tree
class="tree-border"
:data="menuOptions"
show-checkbox
ref="menu"
node-key="id"
:check-strictly="!form.menuCheckStrictly"
empty-text="加载中,请稍候"
:props="defaultProps"
></el-tree>
<!-- Modified table structure to show grouped menu permissions -->
<el-table
ref="menuTable"
:data="groupedMenuData"
:span-method="cellMergeMethod"
class="menu-permission-table"
border
style="width: 100%; margin-top: 10px;"
max-height="400"
>
<el-table-column label="系统功能编码" width="120" align="center">
<template slot-scope="scope">
<span>{{ scope.row.systemCode }}</span>
</template>
</el-table-column>
<el-table-column label="一级目录" width="150" align="center">
<template slot-scope="scope">
<span>{{ scope.row.topLevelDirectory }}</span>
</template>
</el-table-column>
<el-table-column label="菜单" width="200">
<template slot-scope="scope">
<div v-for="menu in scope.row.menus" :key="menu.menuId" style="margin-bottom: 5px;">
<el-checkbox
v-model="menu.checked"
@change="handleMenuCheck(menu)"
>
{{ menu.menuName }}
</el-checkbox>
</div>
</template>
</el-table-column>
<el-table-column label="按钮授权" align="center">
<template slot-scope="scope">
<div v-for="permission in scope.row.permissions" :key="permission.menuId" style="margin-bottom: 5px;">
<el-checkbox
v-model="permission.checked"
@change="handlePermissionCheck(permission)"
>
{{ permission.menuName }}
</el-checkbox>
</div>
</template>
</el-table-column>
</el-table>
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
@ -216,7 +256,7 @@
<el-form-item label="权限字符">
<el-input v-model="form.roleKey" :disabled="true" />
</el-form-item>
<el-form-item label="权限范围">
<!-- <el-form-item label="权限范围">
<el-select v-model="form.dataScope" @change="dataScopeSelectChange">
<el-option
v-for="item in dataScopeOptions"
@ -225,7 +265,7 @@
:value="item.value"
></el-option>
</el-select>
</el-form-item>
</el-form-item>-->
<el-form-item label="数据权限" v-show="form.dataScope == 2">
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">展开/折叠</el-checkbox>
<el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
@ -311,6 +351,8 @@ export default {
],
//
menuOptions: [],
menuTableData: [],
groupedMenuData: [],
//
deptOptions: [],
//
@ -338,7 +380,8 @@ export default {
roleSort: [
{ required: true, message: "角色顺序不能为空", trigger: "blur" }
]
}
},
mergeMap: {}
}
},
created() {
@ -359,22 +402,218 @@ export default {
getMenuTreeselect() {
menuTreeselect().then(response => {
this.menuOptions = response.data
this.convertMenuDataToGroupedTable(response.data)
})
},
//
getMenuAllCheckedKeys() {
//
let checkedKeys = this.$refs.menu.getCheckedKeys()
//
let halfCheckedKeys = this.$refs.menu.getHalfCheckedKeys()
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
return checkedKeys
convertMenuDataToGroupedTable(menuData) {
const directories = menuData.filter(item => item.parentId === 0) //
const menus = menuData.filter(item => item.parentId !== 0 && item.menuType === 'C') //
const buttons = menuData.filter(item => item.menuType === 'F') //
const grouped = []
this.mergeMap = {}
// Process each directory
directories.forEach(directory => {
const directoryMenus = menus.filter(menu => menu.parentId === directory.menuId)
if (directoryMenus.length > 0) {
const startIndex = grouped.length
directoryMenus.forEach((menu, index) => {
const menuButtons = buttons.filter(button => button.parentId === menu.menuId)
grouped.push({
systemCode: directory.menuId,
topLevelDirectory: directory.menuName,
directoryId: directory.menuId, // Added for merge tracking
directoryChecked: false,
menus: [{
menuId: menu.menuId,
menuName: menu.menuName,
checked: false
}],
permissions: menuButtons.map(button => ({
menuId: button.menuId,
menuName: button.menuName,
checked: false
}))
})
})
this.mergeMap[directory.menuId] = {
startIndex: startIndex,
rowspan: directoryMenus.length
}
} else {
// If directory has no menus, still show it
const startIndex = grouped.length
grouped.push({
systemCode: directory.menuId,
topLevelDirectory: directory.menuName,
directoryId: directory.menuId,
directoryChecked: false,
menus: [],
permissions: []
})
this.mergeMap[directory.menuId] = {
startIndex: startIndex,
rowspan: 1
}
}
})
this.groupedMenuData = grouped
},
//
cellMergeMethod({ row, column, rowIndex, columnIndex }) {
// Only merge the first two columns ( and )
if (columnIndex === 0 || columnIndex === 1) {
const directoryId = row.directoryId
const mergeInfo = this.mergeMap[directoryId]
if (mergeInfo && rowIndex === mergeInfo.startIndex) {
// First row of the group - show the merged cell
return {
rowspan: mergeInfo.rowspan,
colspan: 1
}
} else if (mergeInfo && rowIndex > mergeInfo.startIndex && rowIndex < mergeInfo.startIndex + mergeInfo.rowspan) {
// Other rows in the group - hide the cell
return {
rowspan: 0,
colspan: 0
}
}
}
// Default - no merge
return {
rowspan: 1,
colspan: 1
}
},
handleMenuCheck(menu) {
if (menu.checked) {
this.groupedMenuData.forEach(group => {
const menuItem = group.menus.find(m => m.menuId === menu.menuId)
if (menuItem) {
const directory = this.menuOptions.find(item =>
item.parentId === 0 && item.menuId === group.directoryId
)
if (directory) {
group.directoryChecked = true
}
}
})
} else {
this.groupedMenuData.forEach(group => {
group.permissions.forEach(permission => {
const menuButtons = this.menuOptions.filter(item =>
item.menuType === 'F' && item.parentId === menu.menuId
)
if (menuButtons.some(button => button.menuId === permission.menuId)) {
permission.checked = false
}
})
})
const directoryGroup = this.groupedMenuData.find(group =>
group.menus.some(m => m.menuId === menu.menuId)
)
if (directoryGroup) {
const allMenusUnchecked = this.groupedMenuData
.filter(g => g.directoryId === directoryGroup.directoryId)
.every(g => g.menus.every(m => !m.checked))
if (allMenusUnchecked) {
this.groupedMenuData.forEach(group => {
if (group.directoryId === directoryGroup.directoryId) {
group.directoryChecked = false
}
})
}
}
}
},
handlePermissionCheck(permission) {
const parentMenu = this.menuOptions.find(item =>
item.menuType === 'C' &&
this.menuOptions.some(button =>
button.menuType === 'F' &&
button.parentId === item.menuId &&
button.menuId === permission.menuId
)
)
if (parentMenu) {
if (permission.checked) {
this.groupedMenuData.forEach(group => {
const menuItem = group.menus.find(menu => menu.menuId === parentMenu.menuId)
if (menuItem) {
menuItem.checked = true
group.directoryChecked = true
}
})
} else {
const allPermissionsUnchecked = this.groupedMenuData.every(group => {
return group.permissions.every(perm => {
const isRelatedButton = this.menuOptions.some(button =>
button.menuType === 'F' &&
button.parentId === parentMenu.menuId &&
button.menuId === perm.menuId
)
return !isRelatedButton || !perm.checked
})
})
if (allPermissionsUnchecked) {
this.groupedMenuData.forEach(group => {
const menuItem = group.menus.find(menu => menu.menuId === parentMenu.menuId)
if (menuItem) {
menuItem.checked = false
const allMenusUnchecked = this.groupedMenuData
.filter(g => g.directoryId === group.directoryId)
.every(g => g.menus.every(m => !m.checked))
if (allMenusUnchecked) {
group.directoryChecked = false
}
}
})
}
}
}
},
getMenuAllCheckedKeys() {
const checkedIds = []
this.groupedMenuData.forEach(group => {
if (group.directoryChecked) {
checkedIds.push(group.directoryId)
}
group.menus.forEach(menu => {
if (menu.checked) {
checkedIds.push(menu.menuId)
}
})
group.permissions.forEach(permission => {
if (permission.checked) {
checkedIds.push(permission.menuId)
}
})
})
return [...new Set(checkedIds)]
},
getDeptAllCheckedKeys() {
//
let checkedKeys = this.$refs.dept.getCheckedKeys()
//
let halfCheckedKeys = this.$refs.dept.getHalfCheckedKeys()
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys)
return checkedKeys
@ -383,6 +622,7 @@ export default {
getRoleMenuTreeselect(roleId) {
return roleMenuTreeselect(roleId).then(response => {
this.menuOptions = response.menus
this.convertMenuDataToGroupedTable(response.menus)
return response
})
},
@ -416,25 +656,32 @@ export default {
},
//
reset() {
if (this.$refs.menu != undefined) {
this.$refs.menu.setCheckedKeys([])
}
this.groupedMenuData.forEach(group => {
group.directoryChecked = false
group.menus.forEach(menu => {
menu.checked = false
})
group.permissions.forEach(permission => {
permission.checked = false
})
})
this.menuExpand = false,
this.menuNodeAll = false,
this.deptExpand = true,
this.deptNodeAll = false,
this.form = {
roleId: undefined,
roleName: undefined,
roleKey: undefined,
roleSort: 0,
status: "0",
menuIds: [],
deptIds: [],
menuCheckStrictly: true,
deptCheckStrictly: true,
remark: undefined
}
this.menuNodeAll = false,
this.deptExpand = true,
this.deptNodeAll = false,
this.form = {
roleId: undefined,
roleName: undefined,
roleKey: undefined,
roleSort: 0,
status: "0",
menuIds: [],
deptIds: [],
menuCheckStrictly: true,
deptCheckStrictly: true,
remark: undefined
}
this.resetForm("form")
},
/** 搜索按钮操作 */
@ -451,7 +698,7 @@ export default {
//
handleSelectionChange(selection) {
this.ids = selection.map(item => item.roleId)
this.single = selection.length!=1
this.single = selection.length != 1
this.multiple = !selection.length
},
//
@ -469,12 +716,7 @@ export default {
},
// /
handleCheckedTreeExpand(value, type) {
if (type == 'menu') {
let treeList = this.menuOptions
for (let i = 0; i < treeList.length; i++) {
this.$refs.menu.store.nodesMap[treeList[i].id].expanded = value
}
} else if (type == 'dept') {
if (type == 'dept') {
let treeList = this.deptOptions
for (let i = 0; i < treeList.length; i++) {
this.$refs.dept.store.nodesMap[treeList[i].id].expanded = value
@ -483,17 +725,13 @@ export default {
},
// /
handleCheckedTreeNodeAll(value, type) {
if (type == 'menu') {
this.$refs.menu.setCheckedNodes(value ? this.menuOptions: [])
} else if (type == 'dept') {
if (type == 'dept') {
this.$refs.dept.setCheckedNodes(value ? this.deptOptions: [])
}
},
//
handleCheckedTreeConnect(value, type) {
if (type == 'menu') {
this.form.menuCheckStrictly = value ? true: false
} else if (type == 'dept') {
if (type == 'dept') {
this.form.deptCheckStrictly = value ? true: false
}
},
@ -515,22 +753,56 @@ export default {
this.$nextTick(() => {
roleMenu.then(res => {
let checkedKeys = res.checkedKeys
checkedKeys.forEach((v) => {
this.$nextTick(()=>{
this.$refs.menu.setChecked(v, true ,false)
})
checkedKeys.forEach((menuId) => {
this.groupedMenuData.forEach(group => {
const menuItem = group.menus.find(menu => menu.menuId === menuId)
if (menuItem) {
menuItem.checked = true
}
const permissionItem = group.permissions.find(permission => permission.menuId === menuId)
if (permissionItem) {
permissionItem.checked = true
}
})
})
this.applyParentChildLinkageOnLoad()
})
})
})
this.title = "修改角色"
},
/** 选择角色权限范围触发 */
dataScopeSelectChange(value) {
if(value !== '2') {
this.$refs.dept.setCheckedKeys([])
}
applyParentChildLinkageOnLoad() {
this.groupedMenuData.forEach(group => {
group.permissions.forEach(permission => {
if (permission.checked) {
const parentMenu = this.menuOptions.find(item =>
item.menuType === 'C' &&
this.menuOptions.some(button =>
button.menuType === 'F' &&
button.parentId === item.menuId &&
button.menuId === permission.menuId
)
)
if (parentMenu) {
this.groupedMenuData.forEach(innerGroup => {
const menuItem = innerGroup.menus.find(menu => menu.menuId === parentMenu.menuId)
if (menuItem) {
menuItem.checked = true
innerGroup.directoryChecked = true
}
})
}
}
})
if (group.menus.some(menu => menu.checked)) {
group.directoryChecked = true
}
})
},
/** 分配数据权限操作 */
handleDataScope(row) {
this.reset()
@ -599,7 +871,32 @@ export default {
this.download('system/role/export', {
...this.queryParams
}, `role_${new Date().getTime()}.xlsx`)
}
},
/** 查看角色人员操作 */
handleViewUsers(row) {
// ID
this.$router.push({
path: '/system/user',
query: {
roleId: row.roleId,
}
})
},
}
}
</script>
</script>
<style scoped>
.menu-permission-controls {
margin-bottom: 10px;
}
.menu-permission-table {
border: 1px solid #dcdfe6;
}
.menu-permission-table .el-table__header {
background-color: #f5f7fa;
}
</style>

View File

@ -0,0 +1,344 @@
<template>
<div>
<div class="head-container">
<el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" />
</div>
<div class="head-container">
<el-tree
:data="deptOptions"
:props="defaultProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="tree"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span class="tree-node-operations">
<el-button
type="text"
size="mini"
icon="el-icon-plus"
@click.stop="() => handleAddDept(data)"
v-hasPermi="['system:dept:add']"
title="新增子部门"
></el-button>
<el-button
type="text"
size="mini"
icon="el-icon-edit"
@click.stop="() => handleEditDept(data)"
v-hasPermi="['system:dept:edit']"
title="修改部门"
></el-button>
<el-button
v-if="data.parentId !== 0 && data.id !== 0"
type="text"
size="mini"
icon="el-icon-delete"
@click.stop="() => handleDeleteDept(data)"
v-hasPermi="['system:dept:remove']"
title="删除部门"
></el-button>
</span>
</span>
</el-tree>
</div>
<!-- 添加或修改部门对话框 -->
<el-dialog :title="deptTitle" :visible.sync="deptOpen" width="600px" append-to-body>
<el-form ref="deptForm" :model="deptForm" :rules="deptRules" label-width="80px">
<el-row>
<el-col :span="24" v-if="deptForm.parentId !== 0">
<el-form-item label="上级部门" prop="parentId">
<treeselect v-model="deptForm.parentId" :options="deptOptionsForDialog" :normalizer="normalizer" placeholder="选择上级部门" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="部门名称" prop="deptName">
<el-input v-model="deptForm.deptName" placeholder="请输入部门名称" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="显示排序" prop="orderNum">
<el-input-number v-model="deptForm.orderNum" controls-position="right" :min="0" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="负责人" prop="leader">
<el-input v-model="deptForm.leader" placeholder="请输入负责人" maxlength="20" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="联系电话" prop="phone">
<el-input v-model="deptForm.phone" placeholder="请输入联系电话" maxlength="11" />
</el-form-item>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input v-model="deptForm.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门状态">
<el-radio-group v-model="deptForm.status">
<el-radio
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitDeptForm"> </el-button>
<el-button @click="cancelDept"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from "@/api/system/dept"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
export default {
name: "DeptTreeWithOperations",
dicts: ['sys_normal_disable'],
components: { Treeselect },
props: {
deptOptions: {
type: Array,
default: () => []
},
defaultProps: {
type: Object,
default: () => ({
children: "children",
label: "label"
})
}
},
data() {
return {
deptName: undefined,
deptOpen: false, //
deptTitle: "", //
deptForm: {}, //
deptOptionsForDialog: [], //
//
deptRules: {
parentId: [
{ required: true, message: "上级部门不能为空", trigger: "blur" }
],
deptName: [
{ required: true, message: "部门名称不能为空", trigger: "blur" }
],
orderNum: [
{ required: true, message: "显示排序不能为空", trigger: "blur" }
],
email: [
{
type: "email",
message: "请输入正确的邮箱地址",
trigger: ["blur", "change"]
}
],
phone: [
{
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur"
}
]
}
}
},
watch: {
//
deptName(val) {
this.$refs.tree.filter(val)
}
},
methods: {
/** 转换部门数据结构 */
normalizer(node) {
if (node.children && !node.children.length) {
delete node.children
}
return {
id: node.deptId,
label: node.deptName,
children: node.children
}
},
//
filterNode(value, data) {
if (!value) return true
return data.label.indexOf(value) !== -1
},
//
handleNodeClick(data) {
this.$emit('node-click', data)
},
/** 新增部门按钮操作 */
handleAddDept(data) {
this.resetDeptForm()
this.deptForm.parentId = data.id
this.deptOpen = true
this.deptTitle = "添加部门"
listDept().then(response => {
this.deptOptionsForDialog = this.handleTree(response.data, "deptId")
})
},
/** 修改部门按钮操作 */
handleEditDept(data) {
this.resetDeptForm()
const deptId = data.id
getDept(deptId).then(response => {
this.deptForm = response.data
this.deptOpen = true
this.deptTitle = "修改部门"
listDeptExcludeChild(deptId).then(response => {
this.deptOptionsForDialog = this.handleTree(response.data, "deptId")
if (this.deptOptionsForDialog.length == 0) {
const noResultsOptions = { deptId: this.deptForm.parentId, deptName: this.deptForm.parentName, children: [] }
this.deptOptionsForDialog.push(noResultsOptions)
}
})
})
},
/** 删除部门按钮操作 */
handleDeleteDept(data) {
const deptName = data.label
const deptId = data.id
this.$modal.confirm('是否确认删除名称为"' + deptName + '"的数据项?').then(function() {
return delDept(deptId)
}).then(() => {
this.$emit('dept-deleted')
this.$modal.msgSuccess("删除成功")
}).catch(() => {})
},
/** 部门表单重置 */
resetDeptForm() {
this.deptForm = {
deptId: undefined,
parentId: undefined,
deptName: undefined,
orderNum: undefined,
leader: undefined,
phone: undefined,
email: undefined,
status: "0"
}
this.resetForm("deptForm")
},
//
cancelDept() {
this.deptOpen = false
this.resetDeptForm()
},
/** 提交部门表单 */
submitDeptForm() {
this.$refs["deptForm"].validate(valid => {
if (valid) {
if (this.deptForm.deptId != undefined) {
updateDept(this.deptForm).then(response => {
this.$modal.msgSuccess("修改成功")
this.deptOpen = false
this.$emit('dept-updated')
})
} else {
addDept(this.deptForm).then(response => {
this.$modal.msgSuccess("新增成功")
this.deptOpen = false
this.$emit('dept-added')
})
}
}
})
},
//
handleTree(data, id, parentId, children) {
let config = {
id: id || 'id',
parentId: parentId || 'parentId',
childrenList: children || 'children'
}
var childrenListMap = {}
var nodeIds = {}
var tree = []
for (let d of data) {
let parentId = d[config.parentId]
if (childrenListMap[parentId] == null) {
childrenListMap[parentId] = []
}
nodeIds[d[config.id]] = d
childrenListMap[parentId].push(d)
}
for (let d of data) {
let parentId = d[config.parentId]
if (nodeIds[parentId] == null) {
tree.push(d)
}
}
for (let t of tree) {
adaptToChildrenList(t)
}
function adaptToChildrenList(o) {
if (childrenListMap[o[config.id]] !== null) {
o[config.childrenList] = childrenListMap[o[config.id]]
}
if (o[config.childrenList]) {
for (let c of o[config.childrenList]) {
adaptToChildrenList(c)
}
}
}
return tree
}
}
}
</script>
<style scoped>
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
.tree-node-operations {
display: none;
}
.custom-tree-node:hover .tree-node-operations {
display: block;
}
.tree-node-operations .el-button {
padding: 4px;
margin-left: 2px;
}
</style>

View File

@ -5,12 +5,16 @@
<!--部门数据-->
<pane size="16">
<el-col>
<div class="head-container">
<el-input v-model="deptName" placeholder="请输入部门名称" clearable size="small" prefix-icon="el-icon-search" style="margin-bottom: 20px" />
</div>
<div class="head-container">
<el-tree :data="deptOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode" ref="tree" node-key="id" default-expand-all highlight-current @node-click="handleNodeClick" />
</div>
<!-- 使用新的部门树组件 -->
<dept-tree-with-operations
:dept-options="deptOptions"
:default-props="defaultProps"
@node-click="handleNodeClick"
@dept-added="refreshDeptTree"
@dept-updated="refreshDeptTree"
@dept-deleted="refreshDeptTree"
ref="deptTree"
/>
</el-col>
</pane>
<!--用户数据-->
@ -28,6 +32,20 @@
<el-option v-for="dict in dict.type.sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
</el-select>
</el-form-item>
<!-- 添加角色下拉选择框 -->
<el-form-item label="角色" prop="roleId">
<el-select v-model="queryParams.roleId" placeholder="请选择角色" clearable style="width: 240px">
<el-option
v-for="role in roleOptions"
:key="role.roleId"
:label="role.roleName"
:value="role.roleId"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker v-model="dateRange" style="width: 240px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
</el-form-item>
@ -202,16 +220,25 @@
<script>
import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus, deptTreeSelect } from "@/api/system/user"
import { listRole } from "@/api/system/role" // API
import { getToken } from "@/utils/auth"
import Treeselect from "@riophae/vue-treeselect"
import "@riophae/vue-treeselect/dist/vue-treeselect.css"
import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css"
//
import DeptTreeWithOperations from "./dept"
export default {
name: "User",
dicts: ['sys_normal_disable', 'sys_user_sex'],
components: { Treeselect, Splitpanes, Pane },
components: {
Treeselect,
Splitpanes,
Pane,
DeptTreeWithOperations
},
data() {
return {
//
@ -274,8 +301,11 @@ export default {
userName: undefined,
phonenumber: undefined,
status: undefined,
deptId: undefined
deptId: undefined,
roleId: undefined
},
//
roleName: "",
//
columns: {
userId: { label: '用户编号', visible: true },
@ -314,18 +344,25 @@ export default {
trigger: "blur"
}
]
}
},
}
},
watch: {
//
deptName(val) {
this.$refs.tree.filter(val)
}
this.$refs.deptTree.filter(val)
},
//
'$route'(to, from) {
this.handleRouteParams()
},
},
created() {
this.getList()
this.getDeptTree()
this.getRoleList().then(() => {
this.handleRouteParams()
})
this.getConfigKey("sys.user.initPassword").then(response => {
this.initPassword = response.msg
})
@ -341,13 +378,52 @@ export default {
}
)
},
/** 获取角色列表 */
getRoleList() {
listRole().then(response => {
this.roleOptions = response.rows
})
},
/** 查询部门下拉树结构 */
getDeptTree() {
deptTreeSelect().then(response => {
this.deptOptions = response.data
this.enabledDeptOptions = this.filterDisabledDept(JSON.parse(JSON.stringify(response.data)))
//
this.handleRouteParams()
})
},
//
handleRouteParams() {
const deptId = this.$route.query.deptId
if (deptId && this.deptOptions && this.deptOptions.length > 0) {
// ID
this.queryParams.deptId = deptId
//
this.$nextTick(() => {
if (this.$refs.deptTree && this.$refs.deptTree.$refs.tree) {
this.$refs.deptTree.$refs.tree.setCurrentKey(parseInt(deptId))
//
const node = this.$refs.deptTree.$refs.tree.getNode(parseInt(deptId))
if (node) {
this.handleNodeClick(node.data)
}
}
})
}
// ID -
const roleId = this.$route.query.roleId
if (roleId && this.roleOptions) {
const roleIdNum = parseInt(roleId)
this.queryParams.roleId = roleIdNum
this.handleQuery()
}
},
//
filterDisabledDept(deptList) {
return deptList.filter(dept => {
@ -370,6 +446,7 @@ export default {
this.queryParams.deptId = data.id
this.handleQuery()
},
//
handleStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用"
@ -414,7 +491,14 @@ export default {
this.dateRange = []
this.resetForm("queryForm")
this.queryParams.deptId = undefined
this.$refs.tree.setCurrentKey(null)
this.queryParams.roleId = undefined
this.roleName = ""
//
if (this.$refs.deptTree && this.$refs.deptTree.$refs.tree) {
this.$refs.deptTree.$refs.tree.setCurrentKey(null)
}
this.handleQuery()
},
//
@ -476,10 +560,10 @@ export default {
}
},
}).then(({ value }) => {
resetUserPwd(row.userId, value).then(response => {
this.$modal.msgSuccess("修改成功,新密码是:" + value)
})
}).catch(() => {})
resetUserPwd(row.userId, value).then(response => {
this.$modal.msgSuccess("修改成功,新密码是:" + value)
})
}).catch(() => {})
},
/** 分配角色操作 */
handleAuthRole: function(row) {
@ -547,7 +631,12 @@ export default {
//
submitFileForm() {
this.$refs.upload.submit()
}
},
//
refreshDeptTree() {
this.getDeptTree()
},
}
}
</script>
</script>