第一次提交

This commit is contained in:
lzx 2025-12-09 10:52:00 +08:00
parent 9b89d24d2e
commit bbb9b4e6b1
46 changed files with 10952 additions and 2660 deletions

5
.env.development Normal file
View File

@ -0,0 +1,5 @@
# 开发环境空字符串走Vite代理
VITE_API_BASE_URL='http://127.0.0.1:8501'
VITE_ADMIN_BASE_URL='http://127.0.0.1:8501'
VITE_HEAT_API_BASE_URL='http://192.168.0.207:8000'
VITE_STRENGTH_API_BASE_URL='http://127.0.0.1:8501'

0
.env.production Normal file
View File

View File

@ -2,9 +2,10 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<link rel="icon" href="/ai_tu.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3-Element-Admin</title>
<!-- <title>Vue3-Element-Admin</title>-->
<title>凌空天行 AI大模型 应用系统</title>
</head>
<body>
<div id="app"></div>

7184
package-lock.json generated

File diff suppressed because it is too large Load Diff

BIN
public/ai_tu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

16
src/api/convert.js Normal file
View File

@ -0,0 +1,16 @@
import request from '@/utils/request'
/**
* 调用外部接口转换MinIO路径传递完整data对象
* @param {Object} uploadData - uploadToMinIO返回的完整data
* @param {String} type - 'heat' 'strength'
*/
export function convertToDownloadUrl(uploadData, type) {
// ✅ 绝对不能带域名,必须是相对路径
const url =
type === 'heat'
? '/heat-api/api/calculate' // ✅ 正确:相对路径
: '/strength-api/api/convert/download-url'
return request({ url, method: 'post', data: uploadData, timeout: 600000 })
}

30
src/api/document.js Normal file
View File

@ -0,0 +1,30 @@
import request from '@/utils/request'
/**
* 搜索文档分页
*/
export function searchDocuments(params) {
return request({
url: '/api/document/search/summary/paged',
method: 'get',
params,
})
}
/**
* 获取文档详情
*/
export function getDocumentDetail(id) {
return request({
url: `/api/document/detail/${id}`,
method: 'get',
})
}
// 下载文档根据fileNo获取MinIO URL
export function downloadDocument(fileNo) {
return request({
url: `/api/document/download/search/${fileNo}`,
method: 'get',
})
}

31
src/api/excel.js Normal file
View File

@ -0,0 +1,31 @@
import request from '@/utils/request'
export function uploadToMinIOjgmodel(formData) {
return request({
url: '/api/upload/folderjgmodel',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
})
}
/**
* 上传 Excel 文件到后端
* @param {File} file - Excel 文件对象
* @returns {Promise} - 返回上传结果
*/
export function uploadExcelFile(file) {
const formData = new FormData()
formData.append('file', file)
return request({
url: '/api/document/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
timeout: 60000, // 60秒超时Excel 解析可能需要较长时间
})
}

15
src/api/heat.js Normal file
View File

@ -0,0 +1,15 @@
import request from '@/utils/request'
// uploadToMinIO 增加超时和进度监听
export function uploadToMinIO(formData, onProgress) {
return request({
url: '/api/upload/folder',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data',
},
timeout: 600000, // ✅ 增加超时时间到10分钟
onUploadProgress: onProgress || function() {}, // ✅ 进度回调
})
}

28
src/api/image.js Normal file
View File

@ -0,0 +1,28 @@
import request from '@/utils/request'
/**
* 上传图片
*/
export function uploadImage(file) {
const formData = new FormData()
formData.append('file', file)
return request.post('/api/images/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
}
/**
* 模糊搜索图片
*/
export function searchImages(keyword) {
return request.get('/api/images/search', {
params: { keyword },
})
}
/**
* 获取原图高清 URL
*/
export function getOriginImage(id) {
return request.get(`/api/images/view/${id}`)
}

View File

@ -14,7 +14,8 @@ import request from '@/utils/request'
// 登录接口
export const Login = data => {
return request({
url: '/api/login',
// url: '/api/login',
url: '/admin/system/index/login',
method: 'post',
data,
})
@ -23,7 +24,16 @@ export const Login = data => {
// 获取登录用户信息
export const GetUserinfo = () => {
return request({
url: '/api/userinfo',
// url: '/api/userinfo',
url: '/admin/system/index/getUserInfo',
method: 'get',
})
}
// 请求验证码的函数
export const GetValidateCode = () => {
return request({
url: '/admin/system/index/generateValidateCode',
method: 'get',
})
}

View File

@ -1,3 +1,4 @@
;``
/*
* @Descripttion:
* @version:
@ -14,7 +15,7 @@ import request from '@/utils/request'
// 获取菜单
export const GetMenus = params => {
return request({
url: '/api/menus',
url: '/admin/system/index/menus',
method: 'get',
params,
})

48
src/api/rag.js Normal file
View File

@ -0,0 +1,48 @@
import request from '@/utils/request'
/**
* 提交RAG入库任务
* @param {string} folderId - 文件夹ID
* @returns {Promise} 任务提交结果
*/
// 提交RAG入库任务带角色参数
export function submitRagIngestTask(folderId, userRoleCode = 'default') {
return request({
url: '/api/rag/submit',
method: 'post',
data: {
folderId,
userRoleCode,
},
timeout: 50000, // 30秒超时
})
}
/**
* 查询RAG入库进度
* @param {string} taskId - 任务ID
* @returns {Promise} 进度信息
*/
export function getRagIngestProgress(taskId, userRoleCode = 'default') {
return request({
url: `/api/rag/progress/${taskId}`,
method: 'get',
params: { userRoleCode }, // 作为查询参数
timeout: 50000,
})
}
/**
* 上传文件夹到MinIO
* @param {FormData} formData - 包含files, relativePaths, sender, folderName
* @returns {Promise} 上传结果
*/
export function uploadFolder(formData) {
return request({
url: '/api/upload/folder',
method: 'post',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' },
timeout: 60000 * 10,
})
}

20
src/api/rasa.js Normal file
View File

@ -0,0 +1,20 @@
import request from '@/utils/request'
// 调用 Rasa AI 接口
// export const sendToAI = (data) => {
// return request({
// url: '/api/rasa/webhook',
// method: 'post',
// data
// })
// }
// 修改后
export function sendToAI(data) {
return request({
url: '/api/chat/message', // 新地址
method: 'post',
data,
timeout: 600000, // ✅ 增加超时时间到10分钟
})
}

10
src/api/sysFRole.js Normal file
View File

@ -0,0 +1,10 @@
import request from '@/utils/request'
// 获取当前用户角色
export function getUserRoles(userId) {
return request({
url: '/api/user/current-user-roles',
method: 'get',
params: { userId }, // ✅ 将用户ID作为查询参数传递
})
}

28
src/api/sysMenu.js Normal file
View File

@ -0,0 +1,28 @@
import request from '@/utils/request'
const api_name = '/admin/system/sysMenu'
// 分页列表
export const FindNodes = () => {
return request({
url: `${api_name}/findNodes`,
method: 'get',
})
}
// 保存信息
export const SaveSysMenu = sysMenu => {
return request({
url: `${api_name}/saveSysMenu`,
method: 'post',
data: sysMenu,
})
}
// 修改信息
export const UpdateSysMenu = sysMenu => {
return request({
url: `${api_name}/updateSysMenu`,
method: 'put',
data: sysMenu,
})
}

65
src/api/sysRole.js Normal file
View File

@ -0,0 +1,65 @@
import request from '@/utils/request'
// 获取菜单
export const GetSysRoleListByPage = (queryDto, page, limit) => {
return request({
// url: '/api/menus',
url: `/admin/system/sysRole/getSysRoleListByPage/${page}/${limit}`,
method: 'get',
params: queryDto,
})
}
//修改
export const UpdateSysRole = sysRole => {
return request({
// url: '/api/menus',
url: `/admin/system/sysRole/updateSysRole`,
method: 'put',
data: sysRole,
})
}
//添加
export const SaveSysRole = sysRole => {
return request({
// url: '/api/menus',
url: `/admin/system/sysRole/saveSysRole`,
method: 'post',
data: sysRole,
})
}
//删除
export const DeleteSysRoleById = roleId => {
return request({
// url: '/api/menus',
url: `/admin/system/sysRole/deleteSysRoleById/${roleId}`,
method: 'delete',
})
}
// 查询所有的角色数据
export const GetAllRoleList = userId => {
return request({
url: `/admin/system/sysRole/findAllRoles/${userId}`,
method: 'get',
})
}
// 查询指定角色所对应的菜单id
export const GetSysRoleMenuIds = roleId => {
return request({
url: '/admin/system/sysRoleMenu/findSysRoleMenuByRoleId/' + roleId,
method: 'get',
})
}
// 根据角色分配菜单请求方法
export const DoAssignMenuIdToSysRole = assignMenuDto => {
return request({
url: '/admin/system/sysRoleMenu/doAssign',
method: 'post',
data: assignMenuDto,
})
}

45
src/api/sysUser.js Normal file
View File

@ -0,0 +1,45 @@
import request from '@/utils/request'
// 登录接口
export const GetSysUserListByPage = (page, limit, queryDto) => {
return request({
url: `/admin/system/sysUser/getSysUserListByPage/${page}/${limit}`,
method: 'get',
params: queryDto,
})
}
// 修改
export const UpdateSysUser = sysUser => {
return request({
url: `/admin/system/sysUser/updateSysUser`,
method: 'put',
data: sysUser,
})
}
// 添加
export const SaveSysUser = sysUser => {
return request({
url: `/admin/system/sysUser/saveSysUser`,
method: 'post',
data: sysUser,
})
}
// 根据id删除用户
export const DeleteSysUserById = userId => {
return request({
url: `/admin/system/sysUser/deleteById/${userId}`,
method: 'delete',
})
}
// 给用户分配角色请求
export const DoAssignRoleToUser = assginRoleDto => {
return request({
url: '/admin/system/sysUserRole/doAssign',
method: 'post',
data: assginRoleDto,
})
}

BIN
src/assets/ai_tu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 412 KiB

1516
src/layout/chat-layout.css Normal file

File diff suppressed because it is too large Load Diff

2370
src/layout/chat_layout.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,9 @@
<template>
<div class="brand">
<img class="logo" src="~@/assets/logo.svg" @click="goHome" />
<div class="title">Vue3 Element Admin</div>
<!-- <img class="logo" src="~@/assets/logo.svg" @click="goHome" />-->
<img class="logo" src="~@/assets/ai_tu.png" @click="goHome" />
<div class="title">凌空天行</div>
</div>
</template>
<script>

View File

@ -40,7 +40,7 @@
class="menu"
:mode="mode"
:collapse="collapse"
:uniqueOpened="true"
:uniqueOpened="false"
:router="true"
:default-active="activePath"
:background-color="variables.menuBg"

View File

@ -69,7 +69,7 @@ import Hamburger from './Hamburger.vue'
import Breadcrumbs from './Breadcrumbs.vue'
import Userinfo from './Userinfo.vue'
import ChangeLang from './ChangeLang.vue'
import ErrorLog from '@/components/ErrorLog/index.vue'
// import ErrorLog from '@/components/ErrorLog/index.vue'
import { useLayoutsettings } from '@/pinia/modules/layoutSettings'
import { storeToRefs } from 'pinia'
import { useApp } from '@/pinia/modules/app'
@ -81,7 +81,7 @@ export default defineComponent({
Breadcrumbs,
Userinfo,
ChangeLang,
ErrorLog,
// ErrorLog,
},
setup() {
const defaultSettings = useLayoutsettings()

View File

@ -53,6 +53,7 @@
</div>
</div>
</template>
<script>
import { defineComponent, ref, computed } from 'vue'
import Sidebar from './components/Sidebar/index.vue'

View File

@ -35,7 +35,7 @@
import { ElLoading } from 'element-plus'
import router from '@/router'
import { TOKEN } from './pinia/modules/app' // TOKEN变量名
import { TOKEN } from './pinia/modules/app'
import { nextTick } from 'vue'
import { useApp } from './pinia/modules/app'
import { useAccount } from './pinia/modules/account'
@ -43,84 +43,78 @@ import { useMenus } from './pinia/modules/menu'
const getPageTitle = title => {
const { title: appTitle } = useApp()
if (title) {
return `${title} - ${appTitle}`
}
return appTitle
return title ? `${title} - ${appTitle}` : appTitle
}
// 白名单里面是路由对象的name
// 白名单
const WhiteList = ['login', 'lock']
let loadingInstance = null
// vue-router4的路由守卫不再是通过next放行而是通过return返回true或false或者一个路由地址
router.beforeEach(async to => {
loadingInstance = ElLoading.service({
lock: true,
// text: '正在加载数据,请稍候~',
background: 'rgba(0, 0, 0, 0.7)',
})
// 1. 白名单直接放行
if (WhiteList.includes(to.name)) return true
if (WhiteList.includes(to.name)) {
return true
}
// 2. 未登录强制跳转登录页
if (!window.localStorage[TOKEN]) {
return {
name: 'login',
query: {
redirect: to.fullPath, // redirect是指登录之后可以跳回到redirect指定的页面
},
query: { redirect: to.fullPath },
replace: true,
}
} else {
const { userinfo, getUserinfo } = useAccount()
// 获取用户角色信息,根据角色判断权限
if (!userinfo) {
try {
// 获取用户信息
await getUserinfo()
} catch (err) {
loadingInstance.close()
return false
}
}
return to.fullPath
const { userinfo, getUserinfo } = useAccount()
// 3. 首次加载:获取用户信息
if (!userinfo) {
try {
await getUserinfo()
} catch {
loadingInstance?.close()
return false
}
// 生成菜单(如果你的项目有动态菜单,在此处会添加动态路由)
const { menus, generateMenus } = useMenus()
if (menus.length <= 0) {
try {
await generateMenus()
return to.fullPath // 添加动态路由后必须加这一句触发重定向否则会404
} catch (err) {
loadingInstance.close()
return false
}
// 获取完用户信息后,如果当前不是 /chat_view强制跳转
// 这里的 to 可能是登录页(带 redirect或其他页面
if (to.path !== '/chat_view') {
return '/chat_view'
}
return true // 已在目标页,直接放行
}
// 判断是否处于锁屏状态
if (to.name !== 'lock') {
const { authorization } = useApp()
if (!!authorization && !!authorization.screenCode) {
return {
name: 'lock',
query: {
redirect: to.path,
},
replace: true,
}
// 4. 生成动态菜单(如有)
const { menus, generateMenus } = useMenus()
if (menus.length <= 0) {
try {
await generateMenus()
return to.fullPath // 生成后重新触发导航
} catch {
loadingInstance?.close()
return false
}
}
// 5. 锁屏检查
if (to.name !== 'lock') {
const { authorization } = useApp()
if (authorization?.screenCode) {
return {
name: 'lock',
query: { redirect: to.path },
replace: true,
}
}
}
// 6. 已登录且非首次,允许自由访问
return true
})
router.afterEach(to => {
loadingInstance.close()
loadingInstance?.close()
if (router.currentRoute.value.name === to.name) {
nextTick(() => {
document.title = getPageTitle(!!to.meta && to.meta.truetitle)
document.title = getPageTitle(to.meta?.truetitle)
})
}
})

View File

@ -22,7 +22,8 @@ const COLLAPSE = 'VEA-COLLAPSE'
export const useApp = defineStore('app', {
state: () => ({
title: 'Vue3 Element Admin',
// title: 'Vue3 Element Admin',
title: '凌空天行 AI大模型 应用系统',
authorization: getItem(TOKEN),
sidebar: {
collapse: getItem(COLLAPSE),

View File

@ -118,8 +118,9 @@ export const useMenus = defineStore('menu', () => {
}
const generateMenus = async () => {
// // 方式一:只有固定菜单
// const menus = getFilterMenus(fixedRoutes)
const menus = getFilterMenus(fixedRoutes)
// commit('SET_MENUS', menus)
// setMenus(menus)
// 方式二:有动态菜单
// 从后台获取菜单

View File

@ -32,13 +32,20 @@ import error from './modules/error'
import login from './modules/login'
import lock from './modules/lock'
import home from './modules/home'
import system from './modules/system'
import test from './modules/test'
import lktx_jiegou_chat from './modules/lktx_jiegou_chat'
import chat from './modules/chat'
import jg_gcb from './modules/jg_gcb'
/* 菜单栏的路由 */
// 固定菜单
export const fixedRoutes = [...home]
export const fixedRoutes = [...home, ...chat]
// export const fixedRoutes = [ ]
// 动态菜单
export const asyncRoutes = [...test]
export const asyncRoutes = [...test, ...system, ...jg_gcb]
const router = createRouter({
history: createWebHashHistory(),

View File

@ -0,0 +1,25 @@
const Layout = () => import('@/layout/chat_layout.vue')
export default [
{
path: '/chat',
component: Layout,
name: 'chat',
meta: {
title: 'chat',
},
icon: 'Setting',
children: [
{
path: '/chat_view',
name: 'chat_view',
component: Layout,
meta: {
title: 'chat界面',
affix: false,
},
icon: 'User',
},
],
},
]

View File

@ -0,0 +1,50 @@
/*
* @Descripttion:
* @version:
* @Date: 2021-04-20 11:06:21
* @LastEditors: huzhushan@126.com
* @LastEditTime: 2022-09-24 19:27:21
* @Author: huzhushan@126.com
* @HomePage: https://huzhushan.gitee.io/vue3-element-admin
* @Github: https://github.com/huzhushan/vue3-element-admin
* @Donate: https://huzhushan.gitee.io/vue3-element-admin/donate/
*/
// home.js
const Layout = () => import('@/layout/index.vue')
const jg_knowledge_files_add = () =>
import('@/views/jg_gcb/jg_knowledge_files_add.vue')
const jg_model_file_add = () => import('@/views/jg_gcb/jg_model_file_add.vue')
export default [
{
path: '/jg_gcb',
component: Layout,
name: 'jg_gcb',
meta: {
title: '结构工程部-工具集合',
},
icon: 'Setting',
children: [
{
path: '/jg_knowledge_files_add',
name: 'jg_knowledge_files_add',
component: jg_knowledge_files_add,
meta: {
title: 'JG-知识库文件新增管理',
affix: false,
},
icon: 'User',
},
{
path: '/jg_model_file_add',
name: 'jg_model_file_add',
component: jg_model_file_add,
meta: {
title: 'JG-模型文件新增',
affix: false,
},
icon: '',
},
],
},
]

View File

@ -0,0 +1,48 @@
/*
* @Descripttion:
* @version:
* @Date: 2021-04-20 11:06:21
* @LastEditors: huzhushan@126.com
* @LastEditTime: 2022-09-24 19:27:21
* @Author: huzhushan@126.com
* @HomePage: https://huzhushan.gitee.io/vue3-element-admin
* @Github: https://github.com/huzhushan/vue3-element-admin
* @Donate: https://huzhushan.gitee.io/vue3-element-admin/donate/
*/
// home.js
const Layout = () => import('@/layout/chat_layout.vue')
const first = () => import('@/views/lktx_jiegou/first.vue')
const second = () => import('@/views/lktx_jiegou/second.vue')
export default [
{
path: '/lktx_jiegou',
component: Layout,
name: 'lktx_jiegou',
meta: {
title: '结构组工具集合',
},
icon: 'Setting',
children: [
{
path: '/first',
name: 'first',
component: first,
meta: {
title: '结构组-知识库',
affix: false,
},
icon: 'User',
},
{
path: '/second',
name: 'second',
component: second,
meta: {
title: '图片检索',
affix: false,
},
icon: 'Service',
},
],
},
]

View File

@ -0,0 +1,59 @@
/*
* @Descripttion:
* @version:
* @Date: 2021-04-20 11:06:21
* @LastEditors: huzhushan@126.com
* @LastEditTime: 2022-09-24 19:27:21
* @Author: huzhushan@126.com
* @HomePage: https://huzhushan.gitee.io/vue3-element-admin
* @Github: https://github.com/huzhushan/vue3-element-admin
* @Donate: https://huzhushan.gitee.io/vue3-element-admin/donate/
*/
// home.js
const Layout = () => import('@/layout/index.vue')
const sysUser = () => import('@/views/system/sysUser.vue')
const sysRole = () => import('@/views/system/sysRole.vue')
const sysMenu = () => import('@/views/system/sysMenu.vue')
export default [
{
path: '/system',
component: Layout,
name: 'system',
meta: {
title: '系统管理',
},
icon: 'Setting',
children: [
{
path: '/sysUser',
name: 'sysUser',
component: sysUser,
meta: {
title: '用户管理',
affix: false,
},
icon: 'User',
},
{
path: '/sysRole',
name: 'sysRole',
component: sysRole,
meta: {
title: '角色管理',
affix: false,
},
icon: 'Service',
},
{
path: '/sysMenu',
name: 'sysMenu',
component: sysMenu,
meta: {
title: '菜单管理',
affix: false,
},
icon: 'Document',
},
],
},
]

View File

@ -35,22 +35,51 @@ import router from '@/router'
import { useApp } from '@/pinia/modules/app'
const service = axios.create({
baseURL: '/',
baseURL: '',
// baseURL: 'http://localhost:8501',
timeout: 10000,
withCredentials: true,
})
// 拦截请求
// 拦截请求(合并版)
service.interceptors.request.use(
config => {
// ✅ 1. 添加 Token你的原逻辑
const { authorization } = useApp()
if (authorization) {
config.headers.Authorization = `Bearer ${authorization.token}`
config.headers.token = `${authorization.token}`
}
// ✅ 2. 生产环境替换代理路径(跨域方案核心)
if (!import.meta.env.DEV) {
const { url } = config
if (url.startsWith('/heat-api')) {
config.url = url.replace(
'/heat-api',
import.meta.env.VITE_HEAT_API_BASE_URL
)
} else if (url.startsWith('/strength-api')) {
config.url = url.replace(
'/strength-api',
import.meta.env.VITE_STRENGTH_API_BASE_URL
)
} else if (url.startsWith('/api')) {
config.url = url.replace('/api', import.meta.env.VITE_API_BASE_URL)
} else if (url.startsWith('/admin')) {
config.url = url.replace('/admin', import.meta.env.VITE_ADMIN_BASE_URL)
} else if (url.startsWith('/system')) {
config.url = url.replace('/system', import.meta.env.VITE_API_BASE_URL)
}
console.log(`[PROD] ${config.method?.toUpperCase()} ${config.url}`)
} else {
console.log(`[DEV] ${config.method?.toUpperCase()} ${config.url}`)
}
return config
},
error => {
// console.log(error);
return Promise.reject(error)
}
)

View File

@ -0,0 +1,6 @@
<!-- first.vue -->
<template>
<el-radio-button label="chat_view" size="default">
知识库
</el-radio-button>
</template>

View File

@ -0,0 +1,456 @@
<template>
<div class="knowledge-ingest">
<!-- 第一步上传区域 -->
<div class="section upload-section">
<h3>步骤1: 上传文件夹到MinIO</h3>
<!-- 文件夹选择 -->
<input
type="file"
ref="folderInput"
style="display: none"
webkitdirectory
multiple
@change="handleFolderSelect"
:disabled="processCompleted"
/>
<el-button
type="primary"
@click="$refs.folderInput.click()"
:disabled="processCompleted"
>
选择文件夹
</el-button>
<!-- 文件列表预览 -->
<div v-if="selectedFiles.length > 0" class="file-list">
<h4>待上传文件 {{ selectedFiles.length }} </h4>
<el-tree
:data="fileTree"
:props="{ label: 'name', children: 'children' }"
default-expand-all
class="tree-view"
/>
</div>
<!-- 上传按钮 -->
<el-button
type="success"
:loading="uploading"
@click="uploadFolder"
:disabled="selectedFiles.length === 0 || uploading || processCompleted"
class="action-btn"
>
{{ uploading ? '上传中...' : '开始上传' }}
</el-button>
</div>
<!-- 第二步RAG入库区域 -->
<div v-if="showIngestSection" class="section ingest-section">
<el-divider />
<h3>步骤2: RAG知识库入库</h3>
<el-alert title="上传成功!" type="success" :closable="false" show-icon>
<div class="upload-info">
<span>
文件夹ID:
<strong>{{ uploadResult.folderId }}</strong>
</span>
<span>
文件数量:
<strong>{{ uploadResult.fileCount }}</strong>
</span>
<span>
存储桶:
<strong>{{ uploadResult.bucketName }}</strong>
</span>
</div>
</el-alert>
<el-button
type="warning"
:loading="ingesting"
@click="startRagIngest"
:disabled="ingesting || ingestCompleted || processCompleted"
class="action-btn"
>
{{
ingesting ? '入库中...' : ingestCompleted ? '已完成' : '开始RAG入库'
}}
</el-button>
<!-- 进度条显示条件进行中 已完成 -->
<div v-if="ingesting || ingestCompleted" class="progress-area">
<h4>RAG入库进度</h4>
<el-progress
:percentage="ingestProgress"
:status="ingestStatus"
:stroke-width="20"
:text-inside="true"
/>
<p v-if="currentFile" class="current-file">
正在处理: {{ currentFile }}
</p>
<!-- 完成提示 -->
<div v-if="ingestCompleted" class="completion-status">
<el-icon class="success-icon" :size="48" color="#67C23A">
<CircleCheckFilled />
</el-icon>
<p class="complete-text"> RAG入库已完成</p>
<p class="complete-detail">
共处理 {{ uploadResult.fileCount }} 个文件
</p>
</div>
</div>
<!-- 修改后的刷新按钮区域 -->
<div v-if="processCompleted" class="refresh-section">
<el-divider />
<el-button
type="primary"
size="large"
@click="resetAllData"
class="refresh-btn"
>
<el-icon><RefreshRight /></el-icon>
开始新流程
</el-button>
<p class="refresh-hint">点击按钮将重置所有数据开始新的上传入库流程</p>
</div>
</div>
</div>
</template>
<script>
import { ElMessage, ElIcon } from 'element-plus'
import { CircleCheckFilled, RefreshRight } from '@element-plus/icons-vue'
import {
submitRagIngestTask,
getRagIngestProgress,
uploadFolder,
} from '@/api/rag'
import { getUserRoles } from '@/api/sysFRole'
import { GetUserinfo } from '@/api/login'
export default {
components: {
ElIcon,
CircleCheckFilled,
RefreshRight,
},
data() {
return {
selectedFiles: [],
fileTree: [],
uploading: false,
uploadResult: null,
ingesting: false,
ingestProgress: 0,
ingestStatus: '',
currentFile: '',
taskId: null,
ingestCompleted: false,
processCompleted: false, //
}
},
computed: {
showIngestSection() {
//
return Boolean(this.uploadResult?.folderId) && !this.uploading
},
},
methods: {
handleFolderSelect(event) {
//
if (this.processCompleted) return
const files = Array.from(event.target.files)
if (files.length === 0) return
// 100MB
const MAX_FILE_SIZE = 200 * 1024 * 1024 * 1024 // 100MB
const oversizedFiles = files.filter(f => f.size > MAX_FILE_SIZE)
if (oversizedFiles.length > 0) {
//
ElMessage.error({
message: `以下文件超过 2GB 限制:\n${oversizedFiles
.map(f => f.name)
.join('\n')}`,
duration: 5000, // 5
showClose: true,
dangerouslyUseHTMLString: true, // HTML
})
//
this.$refs.folderInput.value = null
this.selectedFiles = []
this.fileTree = []
return //
}
this.selectedFiles = files.map(file => ({
file: file,
relativePath: file.webkitRelativePath,
size: file.size,
}))
this.buildFileTree()
},
buildFileTree() {
const tree = { name: '根目录', children: {} }
this.selectedFiles.forEach(({ relativePath }) => {
const parts = relativePath.split('/').filter(p => p)
let current = tree.children
parts.forEach(part => {
if (!current[part]) {
current[part] = { name: part, children: {} }
}
current = current[part].children
})
})
this.fileTree = this.convertToTreeData(tree.children)
},
convertToTreeData(children) {
return Object.keys(children).map(key => {
const node = children[key]
return {
name: node.name,
children:
Object.keys(node.children).length > 0
? this.convertToTreeData(node.children)
: undefined,
}
})
},
async uploadFolder() {
//
if (this.processCompleted) return
this.uploading = true
this.uploadResult = null
const formData = new FormData()
this.selectedFiles.forEach(({ file, relativePath }) => {
formData.append('files', file)
formData.append('relativePaths', relativePath)
})
const folderName = this.$refs.folderInput.files[0].webkitRelativePath.split(
'/'
)[0]
formData.append('sender', 'user')
formData.append('folderName', folderName)
try {
const res = await uploadFolder(formData)
if (res.code === 200 && res.data) {
this.uploadResult = res.data
ElMessage.success(`上传成功!共 ${res.data.fileCount} 个文件`)
} else {
throw new Error(res.message || `上传失败 (code: ${res.code})`)
}
} catch (error) {
const msg = error.response?.data?.message || error.message || '上传失败'
ElMessage.error(msg)
} finally {
this.uploading = false
}
},
async startRagIngest() {
//
if (this.processCompleted) return
if (!this.uploadResult?.folderId) {
ElMessage.warning('请先完成文件上传')
return
}
this.ingesting = true
this.ingestCompleted = false
this.ingestProgress = 0
this.ingestStatus = ''
this.currentFile = ''
try {
const userInfoRes = await GetUserinfo()
const userId = userInfoRes.data.id
const roleRes = await getUserRoles(userId)
const userRoles = roleRes.data || []
const userRoleCode = userRoles.some(r => r?.roleCode === 'admin')
? 'admin'
: userRoles[0]?.roleCode || 'default'
const res = await submitRagIngestTask(
this.uploadResult.folderId,
userRoleCode
)
if (res.code === 200 && res.data) {
this.taskId = res.data.taskId
ElMessage.success(`入库任务已提交,角色: ${userRoleCode}`)
this.pollIngestProgress(this.taskId, userRoleCode)
} else {
throw new Error(res.message || '提交失败')
}
} catch (error) {
const msg = error.response?.data?.message || error.message || '入库失败'
ElMessage.error(msg)
this.ingesting = false
}
},
pollIngestProgress(taskId, userRoleCode) {
let retryCount = 0
const maxRetries = 10
const interval = setInterval(async () => {
try {
const res = await getRagIngestProgress(taskId, userRoleCode)
const { code, data } = res
if (code === 200 && data) {
this.ingestProgress = data.progress || 0
this.currentFile = data.currentFile || ''
switch (data.status) {
case 'completed':
this.ingestStatus = 'success'
clearInterval(interval)
this.ingesting = false
this.ingestCompleted = true
this.processCompleted = true //
ElMessage.success('RAG入库完成')
//
ElMessage.info('请点击"开始新流程"按钮进行下一次操作')
break
case 'failed':
this.ingestStatus = 'exception'
clearInterval(interval)
this.ingesting = false
ElMessage.error(`入库失败: ${data.errorMessage}`)
break
default:
retryCount = 0
}
} else {
throw new Error('查询进度失败')
}
} catch (error) {
console.error('轮询错误:', error)
if (retryCount++ >= maxRetries) {
clearInterval(interval)
this.ingesting = false
ElMessage.error('进度查询失败')
}
}
}, 1500)
},
//
refreshPage() {
// 1: 使 Vue Router ()
this.$router.go(0)
// 2: 使 location.reload()
// location.reload()
// 3:
// this.resetAllData()
},
//
resetAllData() {
//
this.selectedFiles = []
this.fileTree = []
this.uploading = false
this.uploadResult = null
this.ingesting = false
this.ingestProgress = 0
this.ingestStatus = ''
this.currentFile = ''
this.taskId = null
this.ingestCompleted = false
this.processCompleted = false
//
if (this.$refs.folderInput) {
this.$refs.folderInput.value = null
}
//
ElMessage.success('页面已重置,可以开始新流程')
},
},
}
</script>
<style lang="css" scoped>
@import 'knowledge-ingest.css';
/* ✅ 新增:完成状态样式 */
.complete-text {
margin-top: 10px;
font-size: 16px;
color: #67c23a;
font-weight: bold;
text-align: center;
}
.complete-detail {
font-size: 14px;
color: #909399;
text-align: center;
margin-top: 5px;
}
.completion-status {
margin-top: 20px;
text-align: center;
padding: 20px;
background: #f0f9ff;
border-radius: 8px;
border: 1px solid #b3e19d;
}
.success-icon {
margin-bottom: 10px;
}
/* ✅ 新增:刷新按钮区域样式 */
.refresh-section {
margin-top: 30px;
text-align: center;
padding: 20px;
background: #fef0f0;
border-radius: 8px;
border: 1px solid #fbc4c4;
}
.refresh-btn {
margin-bottom: 10px;
}
.refresh-hint {
color: #f56c6c;
font-size: 12px;
margin: 5px 0 0 0;
}
/* 禁用状态按钮 */
.action-btn:disabled,
input[type='file']:disabled {
cursor: not-allowed;
opacity: 0.6;
}
</style>

View File

@ -0,0 +1,96 @@
.upload-container {
max-width: 800px;
margin: 0 auto;
padding: 30px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.upload-container h2 {
text-align: center;
color: #303133;
margin-bottom: 30px;
}
.upload-section {
margin-bottom: 40px;
padding: 20px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #ebeef5;
}
.upload-section h3 {
margin: 0 0 15px 0;
color: #409EFF;
font-size: 16px;
}
.upload-drop-zone {
width: 100%;
min-height: 150px;
border: 2px dashed #dcdfe6;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
background: #fff;
}
.upload-drop-zone:hover {
border-color: #409EFF;
background: #f0f7ff;
}
.upload-drop-zone.is-dragover {
border-color: #409EFF;
background: #e6f0ff;
}
.upload-drop-zone .el-icon {
margin-bottom: 10px;
}
.upload-drop-zone p {
margin: 5px 0;
color: #606266;
font-size: 14px;
}
.upload-drop-zone .upload-tip {
font-size: 12px;
color: #909399;
}
.upload-progress {
margin-top: 15px;
padding: 10px;
background: #f5f7fa;
border-radius: 4px;
}
.folder-info, .file-info {
text-align: center;
padding: 10px;
}
.folder-info p, .file-info p {
margin: 8px 0;
color: #606266;
}
/* 深度选择器用于影响子组件样式 */
.upload-section :deep(.el-button) {
width: 100%;
margin-top: 15px;
}
@media (max-width: 768px) {
.upload-container {
padding: 20px;
}
}

View File

@ -0,0 +1,333 @@
<template>
<div class="upload-container">
<h2>文件上传管理中心</h2>
<!-- 文件夹上传区域 -->
<div class="upload-section">
<h3>📁 文件夹上传结构+文件</h3>
<div
class="upload-drop-zone"
:class="{ 'is-dragover': folderDragover }"
@click="selectFolder"
@drop.prevent="handleFolderDrop"
@dragover.prevent="folderDragover = true"
@dragleave.prevent="folderDragover = false"
>
<input
ref="folderInput"
type="file"
webkitdirectory
directory
multiple
style="display: none"
@change="handleFolderSelect"
/>
<div v-if="!selectedFolder">
<el-icon :size="48" color="#409EFF"><FolderOpened /></el-icon>
<p>点击或拖拽文件夹到此处上传</p>
<p class="upload-tip">支持保留文件夹结构将自动上传到MinIO</p>
</div>
<div v-else class="folder-info">
<el-icon :size="32" color="#67C23A"><SuccessFilled /></el-icon>
<p>
已选择文件夹:
<strong>{{ selectedFolder.name }}</strong>
</p>
<p>文件数量: {{ selectedFolder.files.length }} </p>
</div>
</div>
<!-- 进度条 -->
<div v-if="folderUploading" class="upload-progress">
<el-progress
:percentage="uploadProgress"
:status="uploadProgress === 100 ? 'success' : ''"
:stroke-width="15"
>
<span>{{ uploadStatusText }}</span>
</el-progress>
</div>
<el-button
type="primary"
@click="uploadFolder"
:loading="folderUploading"
:disabled="!selectedFolder || folderUploading"
>
{{ folderUploading ? '上传中...' : '确认上传文件夹' }}
</el-button>
</div>
<!-- Excel文件上传区域 -->
<div class="upload-section">
<h3>📊 Excel文件上传</h3>
<div
class="upload-drop-zone"
:class="{ 'is-dragover': excelDragover }"
@click="selectExcel"
@drop.prevent="handleExcelDrop"
@dragover.prevent="excelDragover = true"
@dragleave.prevent="excelDragover = false"
>
<input
ref="excelInput"
type="file"
accept=".xlsx,.xls"
style="display: none"
@change="handleExcelSelect"
/>
<div v-if="!selectedExcel">
<el-icon :size="48" color="#409EFF"><Document /></el-icon>
<p>点击或拖拽Excel文件到此处上传</p>
<p class="upload-tip">仅支持 .xlsx 格式数据将导入系统</p>
</div>
<div v-else class="file-info">
<el-icon :size="32" color="#67C23A"><SuccessFilled /></el-icon>
<p>
已选择文件:
<strong>{{ selectedExcel.name }}</strong>
</p>
<p>文件大小: {{ formatFileSize(selectedExcel.size) }}</p>
</div>
</div>
<el-button
type="success"
@click="uploadExcel"
:loading="excelUploading"
:disabled="!selectedExcel || excelUploading"
>
{{ excelUploading ? '上传解析中...' : '确认上传Excel' }}
</el-button>
<!-- 新增Excel上传结果详情 -->
<el-alert
v-if="showResult && resultDetails"
:title="resultDetails"
type="info"
:closable="false"
show-icon
style="margin-top: 15px; font-size: 13px;"
/>
</div>
<!-- 全局上传结果提示 -->
<el-alert
v-if="showResult"
:title="resultMessage"
:type="resultType"
:closable="false"
show-icon
style="margin-top: 20px"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { FolderOpened, Document, SuccessFilled } from '@element-plus/icons-vue'
import { uploadToMinIOjgmodel, uploadExcelFile } from '@/api/excel'
//
const username = ref('admin')
//
const folderInput = ref(null)
const folderDragover = ref(false)
const selectedFolder = ref(null)
const folderUploading = ref(false)
const uploadProgress = ref(0)
const uploadStatusText = ref('准备上传...')
const BATCH_SIZE = 10
// Excel
const excelInput = ref(null)
const excelDragover = ref(false)
const selectedExcel = ref(null)
const excelUploading = ref(false)
// resultDetails
const showResult = ref(false)
const resultMessage = ref('')
const resultType = ref('success')
const resultDetails = ref('') //
//
const selectFolder = () => folderInput.value.click()
const handleFolderSelect = event => {
const files = Array.from(event.target.files)
if (files.length === 0) return
const firstPath = files[0].webkitRelativePath || files[0].name
selectedFolder.value = {
name: firstPath.split('/')[0],
files: files,
}
}
const handleFolderDrop = event => {
folderDragover.value = false
const items = event.dataTransfer.items
if (!items) return
const entries = Array.from(items)
.map(item => item.webkitGetAsEntry())
.filter(Boolean)
processEntries(entries)
}
const processEntries = async (entries, path = '') => {
const files = []
for (const entry of entries) {
if (entry.isFile) {
const file = await new Promise(resolve => entry.file(resolve))
file.webkitRelativePath = path ? `${path}/${entry.name}` : entry.name
files.push(file)
} else if (entry.isDirectory) {
const dirReader = entry.createReader()
const dirEntries = await new Promise(resolve =>
dirReader.readEntries(resolve)
)
files.push(
...(await processEntries(
dirEntries,
path ? `${path}/${entry.name}` : entry.name
))
)
}
}
if (path && files.length > 0) {
selectedFolder.value = { name: path.split('/')[0], files }
}
return files
}
const uploadFolder = async () => {
if (!selectedFolder.value) {
ElMessage.warning('请先选择文件夹')
return
}
folderUploading.value = true
showResult.value = false
uploadProgress.value = 0
try {
const { name: folderName, files } = selectedFolder.value
const totalBatches = Math.ceil(files.length / BATCH_SIZE)
let uploadedFiles = 0
let failedFiles = 0
for (let i = 0; i < totalBatches; i++) {
const batchFiles = files.slice(i * BATCH_SIZE, (i + 1) * BATCH_SIZE)
uploadStatusText.value = `上传中... (${i + 1}/${totalBatches})`
const formData = new FormData()
formData.append('sender', username.value)
formData.append('folderName', folderName)
batchFiles.forEach(file => formData.append('files', file))
const res = await uploadToMinIOjgmodel(formData)
if (res.code === 200) {
uploadedFiles += batchFiles.length
} else {
failedFiles += batchFiles.length
}
uploadProgress.value = Math.round(((i + 1) / totalBatches) * 100)
}
uploadProgress.value = 100
if (failedFiles === 0) {
resultMessage.value = `文件夹上传成功!共 ${uploadedFiles} 个文件`
resultType.value = 'success'
selectedFolder.value = null
folderInput.value.value = ''
} else {
resultMessage.value = `上传完成!成功: ${uploadedFiles}, 失败: ${failedFiles}`
resultType.value = 'warning'
}
} catch (error) {
resultMessage.value = `上传异常: ${error.message}`
resultType.value = 'error'
} finally {
folderUploading.value = false
showResult.value = true
setTimeout(() => {
uploadProgress.value = 0
uploadStatusText.value = '准备上传...'
}, 3000)
}
}
// Excel
const selectExcel = () => excelInput.value.click()
const handleExcelSelect = event => {
const file = event.target.files[0]
if (!file) return
validateAndSetExcel(file)
}
const handleExcelDrop = event => {
excelDragover.value = false
const file = event.dataTransfer.files[0]
if (file) validateAndSetExcel(file)
}
const validateAndSetExcel = file => {
if (!file.name.toLowerCase().endsWith('.xlsx')) {
ElMessage.error('请上传.xlsx格式的Excel文件')
excelInput.value.value = ''
return
}
selectedExcel.value = file
}
const uploadExcel = async () => {
if (!selectedExcel.value) {
ElMessage.warning('请先选择Excel文件')
return
}
excelUploading.value = true
showResult.value = false
resultDetails.value = '' //
try {
const res = await uploadExcelFile(selectedExcel.value)
if (res.code === 200) {
resultMessage.value = 'Excel上传成功'
resultType.value = 'success'
resultDetails.value = res.data || '数据已成功导入系统'
selectedExcel.value = null
excelInput.value.value = ''
} else {
resultMessage.value = '上传失败'
resultType.value = 'error'
resultDetails.value = res.message || '解析失败,请检查文件格式'
}
} catch (error) {
resultMessage.value = '上传异常'
resultType.value = 'error'
resultDetails.value = error.message || '网络错误或服务器无响应'
} finally {
excelUploading.value = false
showResult.value = true
}
}
//
const formatFileSize = bytes => {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
</script>
<style scoped>
@import 'jg_model_file_add.css';
</style>

View File

@ -0,0 +1,87 @@
.knowledge-ingest {
padding: 20px;
max-width: 900px;
margin: 0 auto;
}
.knowledge-ingest .section {
margin-bottom: 30px;
}
.knowledge-ingest .section h3 {
color: #303133;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #409EFF;
}
.knowledge-ingest .file-list {
margin: 20px 0;
background: #f5f7fa;
padding: 15px;
border-radius: 4px;
}
.knowledge-ingest .file-list h4 {
margin: 0 0 10px 0;
color: #606266;
}
.knowledge-ingest .file-list .tree-view {
background: white;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 10px;
}
.knowledge-ingest .action-btn {
margin-top: 15px;
margin-right: 10px;
min-width: 120px;
}
.knowledge-ingest .upload-info {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}
.knowledge-ingest .upload-info span {
font-size: 14px;
}
.knowledge-ingest .upload-info strong {
color: #409EFF;
}
.knowledge-ingest .progress-area {
margin-top: 20px;
}
.knowledge-ingest .progress-area h4 {
margin-bottom: 10px;
color: #606266;
}
.knowledge-ingest .progress-area .current-file {
margin-top: 10px;
font-size: 13px;
color: #909399;
word-break: break-all;
background: #f4f4f5;
padding: 8px 12px;
border-radius: 4px;
}
.knowledge-ingest .el-descriptions {
margin: 15px 0;
}
.knowledge-ingest .el-alert {
margin-bottom: 15px;
}
.knowledge-ingest .el-divider {
margin: 30px 0;
}

View File

@ -0,0 +1,6 @@
<!-- first.vue -->
<template>
<el-radio-button label="first" size="default">
知识库
</el-radio-button>
</template>

View File

@ -0,0 +1,6 @@
<!-- second.vue -->
<template>
<el-radio-button label="second" size="default">
图片检索
</el-radio-button>
</template>

View File

@ -12,7 +12,8 @@
<template>
<div class="login">
<el-form class="form" :model="model" :rules="rules" ref="loginForm">
<h1 class="title">Vue3 Element Admin</h1>
<!-- <h1 class="title">Vue3 Element Admin</h1>-->
<h1 class="title">凌空天行 AI大模型 应用系统</h1>
<el-form-item prop="userName">
<el-input
class="text"
@ -32,6 +33,20 @@
:placeholder="$t('login.password')"
/>
</el-form-item>
<!-- 页面结构 -->
<el-form-item prop="captcha">
<div class="captcha">
<el-input
class="text"
v-model="model.captcha"
prefix-icon="Picture"
placeholder="请输入验证码"
></el-input>
<img :src="captchaSrc" @click="refreshCaptcha" />
</div>
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
@ -60,16 +75,21 @@ import {
computed,
watch,
} from 'vue'
import { Login } from '@/api/login'
import { Login, GetValidateCode } from '@/api/login'
import { useRouter, useRoute } from 'vue-router'
import ChangeLang from '@/layout/components/Topbar/ChangeLang.vue'
import useLang from '@/i18n/useLang'
import { useApp } from '@/pinia/modules/app'
import { onMounted } from 'vue'
export default defineComponent({
components: { ChangeLang },
name: 'login',
setup() {
onMounted(() => {
state.refreshCaptcha()
})
const { proxy: ctx } = getCurrentInstance() // ctxvue2this
const router = useRouter()
const route = useRoute()
@ -100,9 +120,19 @@ export default defineComponent({
],
})
const state = reactive({
refreshCaptcha: () => {
// api
GetValidateCode().then(response => {
state.captchaSrc = response.data.codeValue
state.model.codeKey = response.data.codeKey
})
},
captchaSrc: '',
model: {
userName: 'admin',
password: '123456',
password: '111111',
captcha: '',
codeKey: '',
},
rules: getRules(),
loading: false,
@ -152,6 +182,20 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
// start
.captcha {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.captcha img {
cursor: pointer;
margin-left: 20px;
}
// end
.login {
transition: transform 1s;
transform: scale(1);

View File

@ -0,0 +1,155 @@
<template>
<div>
<div class="tools-div">
<el-button type="success" size="small" @click="addShow()">
</el-button>
</div>
<el-table
:data="list"
style="width: 100%; margin-bottom: 20px"
row-key="id"
border
default-expand-all
>
<el-table-column prop="title" label="菜单标题" />
<el-table-column prop="component" label="路由名称" />
<el-table-column prop="sortValue" label="排序" />
<el-table-column prop="status" label="状态" #default="scope">
{{ scope.row.status == 1 ? '正常' : '停用' }}
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" align="center" width="280" #default="scope">
<el-button type="success" size="small" @click="addShow(scope.row)">
添加下级节点
</el-button>
<el-button type="primary" size="small" @click="editShow(scope.row)">
修改
</el-button>
<el-button type="danger" size="small">删除</el-button>
</el-table-column>
</el-table>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="30%">
<el-form label-width="120px">
<el-form-item label="菜单标题">
<el-input v-model="sysMenu.title" />
</el-form-item>
<el-form-item label="路由名称">
<el-input v-model="sysMenu.component" />
</el-form-item>
<el-form-item label="排序">
<el-input v-model="sysMenu.sortValue" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="sysMenu.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveOrUpdate">提交</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { FindNodes, UpdateSysMenu, SaveSysMenu } from '@/api/sysMenu.js'
import { ElMessage } from 'element-plus'
//
onMounted(() => {
fetchData()
})
//
const list = ref([])
//
const defaultForm = {
id: '',
parentId: 0,
title: '',
url: '',
component: '',
icon: '',
sortValue: 1,
status: 1,
}
//
const sysMenu = ref(defaultForm)
//
const dialogTitle = ref('添加顶级菜单')
const dialogVisible = ref(false)
//
const saveOrUpdate = async () => {
let code = 0
let message = ''
if (sysMenu.value.id) {
const response = await UpdateSysMenu(sysMenu.value)
code = response.code
message = response.message
} else {
const response = await SaveSysMenu(sysMenu.value)
code = response.code
message = response.message
}
if (code == 200) {
ElMessage.success('操作成功')
dialogVisible.value = false
fetchData()
} else {
ElMessage.error(message)
}
}
//
const addShow = row => {
if (row) {
//
dialogTitle.value = '添加' + row.title + '的下一级菜单'
sysMenu.value.parentId = row.id
} else {
//
dialogTitle.value = '添加顶级菜单'
sysMenu.value = { ...defaultForm }
}
dialogVisible.value = true
}
function editShow(row) {
sysMenu.value = { ...row }
dialogVisible.value = true
}
//
const fetchData = async () => {
const { code, data, message } = await FindNodes()
list.value = data
}
</script>
<style scoped>
.search-div {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
.tools-div {
margin: 10px 0;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
</style>

View File

@ -0,0 +1,267 @@
<!--
* @Descripttion:
* @version:
* @Date: 2021-04-20 11:06:21
* @LastEditors: huzhushan@126.com
* @LastEditTime: 2022-09-24 18:18:43
* @Author: huzhushan@126.com
* @HomePage: https://huzhushan.gitee.io/vue3-element-admin
* @Github: https://github.com/huzhushan/vue3-element-admin
* @Donate: https://huzhushan.gitee.io/vue3-element-admin/donate/
-->
<template>
<div>
<div class="search-div">
<!-- 搜索表单 -->
<el-form label-width="70px" size="small">
<el-form-item label="角色名称">
<el-input
style="width: 100%"
placeholder="角色名称"
v-model="queryDto.roleName"
></el-input>
</el-form-item>
<el-row style="display: flex">
<el-button type="primary" size="small">搜索</el-button>
<el-button size="small">重置</el-button>
</el-row>
</el-form>
<!-- 添加按钮 -->
<div class="tools-div">
<el-button type="success" size="small" @click="addShow()">
</el-button>
</div>
<!--- 角色表格数据 -->
<el-table :data="list" style="width: 100%">
<el-table-column prop="roleName" label="角色名称" width="180" />
<el-table-column prop="roleCode" label="角色code" width="180" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column
label="操作"
align="center"
width="280"
#default="scope"
>
<el-button type="primary" size="small" @click="editShow(scope.row)">
修改
</el-button>
<el-button type="danger" size="small">删除</el-button>
<el-button
type="warning"
size="small"
@click="showAssignMenu(scope.row)"
>
分配菜单
</el-button>
</el-table-column>
</el-table>
<!--分页条-->
<el-pagination
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
:total="total"
/>
<!-- 页面表单 -->
<el-dialog v-model="dialogVisible" title="添加或修改角色" width="30%">
<el-form label-width="120px">
<el-form-item label="角色名称">
<el-input v-model="sysRole.roleName" />
</el-form-item>
<el-form-item label="角色Code">
<el-input v-model="sysRole.roleCode" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
<!-- 分配菜单的对话框 -->
<el-dialog v-model="dialogMenuVisible" title="分配菜单" width="40%">
<el-form label-width="80px">
<el-tree
:data="sysMenuTreeList"
show-checkbox
default-expand-all
node-key="id"
:props="defaultProps"
:check-on-click-node="true"
ref="tree"
/>
<el-form-item>
<el-button type="primary" @click="doAssign()">提交</el-button>
<el-button @click="dialogMenuVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {
GetSysRoleListByPage,
SaveSysRole,
UpdateSysRole,
GetSysRoleMenuIds,
DoAssignMenuIdToSysRole,
} from '@/api/sysRole'
import { ElMessage } from 'element-plus'
onMounted(() => {
// list
fetchData()
})
//
let total = ref(0)
//
let list = ref([])
//
const queryDto = ref({
roleName: '',
})
//
const pageParams = ref({
page: 1,
limit: 10,
})
//
const dialogVisible = ref(false)
const sysRole = ref({
roleName: '',
roleCode: '',
})
//
const defaultProps = {
children: 'children',
label: 'title',
}
const dialogMenuVisible = ref(false)
const sysMenuTreeList = ref([])
const tree = ref({})
const roleId = ref(0)
//
const doAssign = () => {
// ids
let checkedNodes = tree.value.getCheckedNodes()
// ids
let halfCheckedNodes = tree.value.getHalfCheckedNodes()
// ids
let checkedIds = checkedNodes.map(checkedNode => {
return {
id: checkedNode.id,
isHalf: 0,
}
})
// ids
let halfCheckedIds = halfCheckedNodes.map(halfCheckedNode => {
return {
id: halfCheckedNode.id,
isHalf: 1,
}
})
//
let ids = [...checkedIds, ...halfCheckedIds]
//
let assginMenuDto = {
roleId: roleId.value,
menuIdList: ids,
}
DoAssignMenuIdToSysRole(assginMenuDto).then(response => {
if (response.code == 200) {
ElMessage.success('分配菜单成功')
dialogMenuVisible.value = false
} else {
ElMessage.error('分配菜单失败')
}
})
}
//
const showAssignMenu = async row => {
roleId.value = row.id
dialogMenuVisible.value = true
//
const response = await GetSysRoleMenuIds(row.id)
sysMenuTreeList.value = response.data.sysMenuList //
tree.value.setCheckedKeys(response.data.roleMenuIds) // id
}
//
function addShow() {
sysRole.value = { ...null }
dialogVisible.value = true
}
function editShow(row) {
sysRole.value = { ...row }
dialogVisible.value = true
}
//
const submit = async () => {
let code = 0
if (sysRole.value.id) {
let response = await UpdateSysRole(sysRole.value)
code = response.code
} else {
let response = await SaveSysRole(sysRole.value)
code = response.code
}
if (code == 200) {
//
ElMessage.success('操作成功')
dialogVisible.value = false
fetchData()
} else {
ElMessage.error('操作失败')
}
}
//
function fetchData() {
// alert("")
GetSysRoleListByPage(
queryDto.value,
pageParams.value.page,
pageParams.value.limit
).then(respoonse => {
list.value = respoonse.data.list
total.value = respoonse.data.total
})
}
</script>
<style scoped>
.search-div {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
.tools-div {
margin: 10px 0;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
</style>

View File

@ -0,0 +1,343 @@
<!--
* @Descripttion:
* @version:
* @Date: 2021-04-20 11:06:21
* @LastEditors: huzhushan@126.com
* @LastEditTime: 2022-09-24 18:18:43
* @Author: huzhushan@126.com
* @HomePage: https://huzhushan.gitee.io/vue3-element-admin
* @Github: https://github.com/huzhushan/vue3-element-admin
* @Donate: https://huzhushan.gitee.io/vue3-element-admin/donate/
-->
<template>
<div>
<!---搜索表单-->
<div class="search-div">
<el-form label-width="70px" size="small">
<el-row>
<el-col :span="12">
<el-form-item label="关键字">
<el-input
v-model="queryDto.keyword"
style="width: 100%"
placeholder="用户名、姓名、手机号码"
></el-input>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="创建时间">
<el-date-picker
v-model="createTimes"
type="daterange"
range-separator="To"
start-placeholder="开始时间"
end-placeholder="结束时间"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
</el-row>
<el-row style="display: flex">
<el-button type="primary" size="small" @click="searchSysUser">
搜索
</el-button>
<el-button size="small" @click="resetData">重置</el-button>
</el-row>
</el-form>
</div>
<!--添加按钮-->
<div class="tools-div">
<el-button type="success" size="small" @click="addShow"> </el-button>
</div>
<!---数据表格-->
<el-table :data="list" style="width: 100%">
<el-table-column prop="username" label="用户名" />
<el-table-column prop="name" label="姓名" />
<el-table-column prop="phone" label="手机" />
<el-table-column prop="avatar" label="头像" #default="scope">
<img :src="scope.row.avatar" width="50" />
</el-table-column>
<el-table-column prop="description" label="描述" />
<el-table-column prop="status" label="状态" #default="scope">
{{ scope.row.status == 1 ? '正常' : '停用' }}
</el-table-column>
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" align="center" width="280" #default="scope">
<el-button type="primary" size="small" @click="editShow(scope.row)">
修改
</el-button>
<el-button type="danger" size="small">删除</el-button>
<el-button
type="warning"
size="small"
@click="showAssignRole(scope.row)"
>
分配角色
</el-button>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="pageParams.page"
v-model:page-size="pageParams.limit"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
:total="total"
/>
<el-dialog v-model="dialogVisible" title="添加或修改" width="40%">
<el-form label-width="120px">
<el-form-item label="用户名">
<el-input v-model="sysUser.username" />
</el-form-item>
<el-form-item label="密码">
<el-input type="password" show-password v-model="sysUser.password" />
</el-form-item>
<el-form-item label="姓名">
<el-input v-model="sysUser.name" />
</el-form-item>
<el-form-item label="手机">
<el-input v-model="sysUser.phone" />
</el-form-item>
<el-form-item label="头像">
<el-upload
class="avatar-uploader"
action="http://localhost:8501/admin/system/fileUpload"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:headers="headers"
>
<img v-if="sysUser.avatar" :src="sysUser.avatar" class="avatar" />
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="sysUser.description" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
<el-dialog v-model="dialogRoleVisible" title="分配角色" width="40%">
<el-form label-width="80px">
<el-form-item label="用户名">
<el-input disabled :value="sysUser.userName"></el-input>
</el-form-item>
<el-form-item label="角色列表">
<el-checkbox-group v-model="userRoleIds">
<el-checkbox
v-for="role in allRoles"
:key="role.id"
:label="role.id"
>
{{ role.roleName }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="doAssign()">提交</el-button>
<el-button @click="dialogRoleVisible = false">取消</el-button>
</el-form-item>
</el-form>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import {
GetSysUserListByPage,
UpdateSysUser,
SaveSysUser,
DoAssignRoleToUser,
} from '@/api/sysUser'
GetAllRoleList
import { GetAllRoleList } from '@/api/sysRole'
import { ElMessage } from 'element-plus'
import { useApp } from '@/pinia/modules/app'
const headers = {
token: useApp().authorization.token,
}
onMounted(() => {
fetchData()
})
//
const queryDto = ref({
keyword: '',
createTimeBegin: '',
createTimeEnd: '',
})
const createTimes = ref([])
//
const pageParams = ref({
page: 1,
limit: 10,
})
//
const sysUser = ref({
userName: '',
name: '',
phone: '',
password: '',
description: '',
avatar: '',
})
const dialogVisible = ref(false)
const dialogRoleVisible = ref(false)
// id
const userRoleIds = ref([])
const allRoles = ref([])
const userId = ref(0)
//
const doAssign = async () => {
let assginRoleDto = {
userId: userId.value, // user
roleIdList: userRoleIds.value,
}
//
let code = 0
const response = await DoAssignRoleToUser(assginRoleDto)
code = response.code
if (code == 200) {
//
ElMessage.success('操作成功')
dialogRoleVisible.value = false
fetchData()
} else {
ElMessage.error('操作失败')
}
}
//
const showAssignRole = row => {
userId.value = row.id
sysUser.value.userName = row.username
dialogRoleVisible.value = true
// id
GetAllRoleList(row.id).then(response => {
allRoles.value = response.data.allRoles
userRoleIds.value = response.data.userRoleIds
})
}
//
const handleAvatarSuccess = (response, uploadFile) => {
sysUser.value.avatar = response.data
}
const addShow = () => {
sysUser.value = { ...null }
dialogVisible.value = true
}
const editShow = row => {
sysUser.value = { ...row }
dialogVisible.value = true
}
//
const submit = async () => {
let code = 0
if (sysUser.value.id) {
let response = await UpdateSysUser(sysUser.value)
code = response.code
} else {
let response = await SaveSysUser(sysUser.value)
code = response.code
}
if (code == 200) {
//
ElMessage.success('操作成功')
dialogVisible.value = false
fetchData()
} else {
ElMessage.error('操作失败')
}
}
//
const fetchData = () => {
//
if (createTimes.value.length > 0) {
queryDto.value.createTimeBegin = createTimes.value[0]
queryDto.value.createTimeEnd = createTimes.value[1]
}
GetSysUserListByPage(
pageParams.value.page,
pageParams.value.limit,
queryDto.value
).then(res => {
list.value = res.data.list
total.value = res.data.total
})
}
//
const list = ref([])
//
const total = ref(0)
</script>
<style scoped>
.search-div {
margin-bottom: 10px;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
.tools-div {
margin: 10px 0;
padding: 10px;
border: 1px solid #ebeef5;
border-radius: 3px;
background-color: #fff;
}
</style>
<style scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@ -77,10 +77,30 @@ export default env => {
open: true,
proxy: {
'/api': {
target: 'http://dev.api.xxx.com', // 后端接口的域名
target: 'http://localhost:8501', // 后端接口的域名
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
'/admin': {
target: 'http://localhost:8501', // 后端接口的域名
changeOrigin: true,
},
// 热处理模块
'/heat-api': {
target: 'http://192.168.0.207:8000',
changeOrigin: true,
rewrite: path => path.replace(/^\/heat-api/, '') // 去掉前缀,保留原始路径
},
// 强度计算模块
'/strength-api': {
target: 'http://localhost:8501',
changeOrigin: true,
rewrite: path => path.replace(/^\/strength-api/, '')
}
},
},
esbuild: false,