fix: 调整项目架构,封装axios等

This commit is contained in:
huchengrui 2025-10-21 11:38:27 +08:00
parent 79cc312865
commit 82f485385b
42 changed files with 2421 additions and 122 deletions

View File

@ -11,13 +11,14 @@
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
"@wangeditor/editor": "^5.1.23", "@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^1.0.2", "@wangeditor/editor-for-vue": "^1.0.2",
"axios": "^1.12.2",
"core-js": "^3.8.3", "core-js": "^3.8.3",
"d3": "^7.9.0", "d3": "^7.9.0",
"echarts": "^6.0.0", "echarts": "^6.0.0",
"element-plus": "^2.11.4", "element-plus": "^2.11.4",
"pinia": "^3.0.3",
"vue": "^3.2.13", "vue": "^3.2.13",
"vue-router": "^4.0.3", "vue-router": "^4.0.3"
"vuex": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
@ -47,12 +48,15 @@
"parserOptions": { "parserOptions": {
"ecmaVersion": 2020 "ecmaVersion": 2020
}, },
"rules": {}, "rules": {
"vue/multi-word-component-names": "off"
},
"globals": { "globals": {
"defineProps": "readonly", "defineProps": "readonly",
"defineEmits": "readonly", "defineEmits": "readonly",
"defineExpose": "readonly", "defineExpose": "readonly",
"withDefaults": "readonly" "withDefaults": "readonly",
"defineOptions": "readonly"
} }
}, },
"browserslist": [ "browserslist": [

67
src/api/auth.ts Normal file
View File

@ -0,0 +1,67 @@
import { post, get } from './request'
// 登录接口
export interface LoginParams {
username: string
password: string
loginType: number // 登录类型固定为2
}
// 菜单项接口
export interface MenuItem {
id: string
name: string
code: string
icon: string
path: string
parentId: string
sort: number
type: number
children?: MenuItem[]
}
// 用户信息接口
export interface UserInfo {
userId: string
username: string
realName: string
email: string
phone: string
status: number
lastLoginIp: string
lastLoginTime: string
roles: string[]
permissions: string[]
menus: MenuItem[]
}
export interface LoginResponse {
token: string
userInfo?: UserInfo
}
export interface UserInfoResponse {
code: number
data: UserInfo
message: string
}
// 用户登录
export function login(data: LoginParams) {
return post<LoginResponse>('/api/system/auth/login', data)
}
// 获取当前用户信息
export function getUserInfo() {
return get<UserInfo>('/api/system/auth/info')
}
// 登出接口
export function logout() {
return post('/api/system/auth/logout')
}
// 刷新 token
export function refreshToken() {
return post('/api/system/auth/refresh')
}

88
src/api/content.ts Normal file
View File

@ -0,0 +1,88 @@
import { get, post, put, del } from './request'
// ==================== 新闻政策 ====================
export interface NewsPolicyItem {
id?: string
title: string
status: string
author: string
publishTime: string
content?: string
}
// 通用分页参数
export interface PageParams {
page: number
pageSize: number
keyword?: string
status?: string
[key: string]: any
}
// 通用分页响应
export interface PageResponse<T> {
list: T[]
total: number
page: number
pageSize: number
}
// 获取新闻政策列表
export function getNewsPolicyList(params: PageParams) {
return get<PageResponse<NewsPolicyItem>>('/content/news-policy/list', params)
}
// 获取新闻政策详情
export function getNewsPolicyDetail(id: string) {
return get<NewsPolicyItem>(`/content/news-policy/${id}`)
}
// 创建新闻政策
export function createNewsPolicy(data: NewsPolicyItem) {
return post('/content/news-policy', data)
}
// 更新新闻政策
export function updateNewsPolicy(id: string, data: NewsPolicyItem) {
return put(`/content/news-policy/${id}`, data)
}
// 删除新闻政策
export function deleteNewsPolicy(id: string) {
return del(`/content/news-policy/${id}`)
}
// ==================== 科技问答 ====================
export interface QAItem {
id?: string
question: string
answer: string
category: string
status: string
createTime: string
}
// 获取科技问答列表
export function getQAList(params: PageParams) {
return get<PageResponse<QAItem>>('/content/smart-qa/list', params)
}
// 获取科技问答详情
export function getQADetail(id: string) {
return get<QAItem>(`/content/smart-qa/${id}`)
}
// 创建科技问答
export function createQA(data: QAItem) {
return post('/content/smart-qa', data)
}
// 更新科技问答
export function updateQA(id: string, data: QAItem) {
return put(`/content/smart-qa/${id}`, data)
}
// 删除科技问答
export function deleteQA(id: string) {
return del(`/content/smart-qa/${id}`)
}

52
src/api/dashboard.ts Normal file
View File

@ -0,0 +1,52 @@
import { get } from './request'
// 统计数据
export interface StatsData {
news: number
qa: number
resources: number
talents: number
}
// 获取统计数据
export function getStats() {
return get<StatsData>('/dashboard/stats')
}
// 趋势数据
export interface TrendData {
dates: string[]
newsPolicy: number[]
smartQA: number[]
techResources: number[]
talentProfile: number[]
}
// 获取趋势数据
export function getTrendData(params?: { startDate?: string; endDate?: string }) {
return get<TrendData>('/dashboard/trend', params)
}
// 访问统计
export interface VisitStats {
name: string
value: number
}
// 获取访问统计
export function getVisitStats() {
return get<VisitStats[]>('/dashboard/visit-stats')
}
// 活动记录
export interface Activity {
id: string
text: string
time: string
type: string
}
// 获取最新活动
export function getRecentActivities(params?: { limit?: number }) {
return get<Activity[]>('/dashboard/activities', params)
}

11
src/api/index.ts Normal file
View File

@ -0,0 +1,11 @@
// 统一导出所有 API
export * from './auth'
export * from './content'
export * from './tech-resources'
export * from './system'
export * from './dashboard'
export * from './upload'
// 导出请求方法
export { get, post, put, del } from './request'
export type { ResponseData } from './request'

142
src/api/request.ts Normal file
View File

@ -0,0 +1,142 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
import { getEnvConfig } from '@/config/env'
// 获取环境配置
const envConfig = getEnvConfig()
// 创建 axios 实例
const service: AxiosInstance = axios.create({
baseURL: envConfig.baseURL,
timeout: envConfig.timeout,
headers: {
'Content-Type': 'application/json;charset=UTF-8'
}
})
// 请求拦截器
service.interceptors.request.use(
(config) => {
// 从 localStorage 获取 token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error: AxiosError) => {
console.error('请求错误:', error)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const res = response.data
// 根据后端返回的状态码进行处理
// 这里假设后端返回格式为 { code: number, data: any, message: string }
if (res.code !== undefined && res.code !== 200 && res.code !== 0) {
ElMessage.error(res.message || '请求失败')
// 401: 未授权,跳转到登录页
if (res.code === 401) {
localStorage.removeItem('token')
router.push('/login')
}
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
},
(error: AxiosError) => {
console.error('响应错误:', error)
if (error.response) {
const status = error.response.status
switch (status) {
case 401:
ElMessage.error('未授权,请重新登录')
localStorage.removeItem('token')
router.push('/login')
break
case 403:
ElMessage.error('拒绝访问')
break
case 404:
ElMessage.error('请求的资源不存在')
break
case 500:
ElMessage.error('服务器内部错误')
break
case 502:
ElMessage.error('网关错误')
break
case 503:
ElMessage.error('服务不可用')
break
case 504:
ElMessage.error('网关超时')
break
default:
ElMessage.error(error.message || '请求失败')
}
} else if (error.request) {
ElMessage.error('网络错误,请检查网络连接')
} else {
ElMessage.error('请求配置错误')
}
return Promise.reject(error)
}
)
// 通用请求方法
export interface ResponseData<T = unknown> {
code: number
data: T
message: string
}
// GET 请求
export function get<T = unknown>(
url: string,
params?: unknown,
config?: AxiosRequestConfig
): Promise<ResponseData<T>> {
return service.get(url, { params, ...config })
}
// POST 请求
export function post<T = unknown>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<ResponseData<T>> {
return service.post(url, data, config)
}
// PUT 请求
export function put<T = unknown>(
url: string,
data?: unknown,
config?: AxiosRequestConfig
): Promise<ResponseData<T>> {
return service.put(url, data, config)
}
// DELETE 请求
export function del<T = unknown>(
url: string,
params?: unknown,
config?: AxiosRequestConfig
): Promise<ResponseData<T>> {
return service.delete(url, { params, ...config })
}
// 导出 axios 实例
export default service

139
src/api/system.ts Normal file
View File

@ -0,0 +1,139 @@
import { get, post, put, del } from "./request";
import { PageParams, PageResponse } from "./content";
// ==================== 用户管理 ====================
export interface UserItem {
id?: string;
username: string;
realName: string;
email: string;
role: string;
status: string;
lastLogin: string;
password?: string;
}
export function getUserList(params: PageParams) {
return get<PageResponse<UserItem>>("/system/users/list", params);
}
export function getUserDetail(id: string) {
return get<UserItem>(`/system/users/${id}`);
}
export function createUser(data: UserItem) {
return post("/system/users", data);
}
export function updateUser(id: string, data: UserItem) {
return put(`/system/users/${id}`, data);
}
export function deleteUser(id: string) {
return del(`/system/users/${id}`);
}
export function toggleUserStatus(id: string, status: string) {
return put(`/system/users/${id}/status`, { status });
}
// ==================== 权限管理 ====================
export interface RoleItem {
id?: string;
roleName: string;
roleCode: string;
description: string;
permissions: string[];
status: string;
updateTime: string;
}
export function getRoleList(params: PageParams) {
return get<PageResponse<RoleItem>>("/system/roles/list", params);
}
export function getRoleDetail(id: string) {
return get<RoleItem>(`/system/roles/${id}`);
}
export function createRole(data: RoleItem) {
return post("/system/roles", data);
}
export function updateRole(id: string, data: RoleItem) {
return put(`/system/roles/${id}`, data);
}
export function deleteRole(id: string) {
return del(`/system/roles/${id}`);
}
export function updateRolePermissions(id: string, permissions: string[]) {
return put(`/system/roles/${id}/permissions`, { permissions });
}
// 获取所有权限列表
export function getPermissionList() {
return get("/system/permissions/all");
}
// ==================== 字典管理 ====================
export interface DictItem {
id?: string;
paramName: string;
paramType: string;
paramValue: string;
dataType: string;
sortOrder: number;
status: number;
flag: number;
remark?: string;
createBy?: string;
createTime?: string;
updateBy?: string;
updateTime?: string;
}
// 新增字典
export function createDict(data: DictItem) {
return post("/system/param", data);
}
// 修改字典
export function updateDict(data: DictItem) {
return put("/system/param", data);
}
// 删除字典
export function deleteDict(id: string) {
return del(`/system/param/${id}`);
}
// 分页查询字典列表
export interface DictListParams extends PageParams {
typeCode?: string;
}
export function getDictList(params: DictListParams) {
return get<PageResponse<DictItem>>("/system/param/list", params);
}
// 根据参数类型获取参数列表
export function getDictByType(typeCode: string) {
return get<DictItem[]>(`/system/param/type/${typeCode}`);
}
// 批量获取多个字典类型的字典列表
export function getBatchDictList(typeCodes: string) {
return get<Record<string, DictItem[]>>("/system/param/batch", { typeCodes });
}
// 获取字典值
export function getDictValue(params: { paramType?: string; paramName?: string }) {
return get("/system/param/value", params);
}
// 刷新字典缓存
export function refreshDictCache(typeCode?: string) {
return post("/system/param/refresh", { typeCode });
}

194
src/api/tech-resources.ts Normal file
View File

@ -0,0 +1,194 @@
import { get, post, put, del } from './request'
import { PageParams, PageResponse } from './content'
// ==================== 科技资源 ====================
export interface ResourceItem {
id?: string
name: string
type: string
organization: string
status: string
updateTime: string
description?: string
}
export function getResourceList(params: PageParams) {
return get<PageResponse<ResourceItem>>('/tech-resources/resources/list', params)
}
export function getResourceDetail(id: string) {
return get<ResourceItem>(`/tech-resources/resources/${id}`)
}
export function createResource(data: ResourceItem) {
return post('/tech-resources/resources', data)
}
export function updateResource(id: string, data: ResourceItem) {
return put(`/tech-resources/resources/${id}`, data)
}
export function deleteResource(id: string) {
return del(`/tech-resources/resources/${id}`)
}
// ==================== 人才档案 ====================
export interface TalentItem {
id?: string
name: string
title: string
field: string
organization: string
level: string
updateTime: string
description?: string
}
export function getTalentList(params: PageParams) {
return get<PageResponse<TalentItem>>('/tech-resources/talent-profile/list', params)
}
export function getTalentDetail(id: string) {
return get<TalentItem>(`/tech-resources/talent-profile/${id}`)
}
export function createTalent(data: TalentItem) {
return post('/tech-resources/talent-profile', data)
}
export function updateTalent(id: string, data: TalentItem) {
return put(`/tech-resources/talent-profile/${id}`, data)
}
export function deleteTalent(id: string) {
return del(`/tech-resources/talent-profile/${id}`)
}
// ==================== 科技项目 ====================
export interface ProjectItem {
id?: string
name: string
category: string
organization: string
fundingAmount: string
applicationYear: string
status: string
description?: string
}
export function getProjectList(params: PageParams) {
return get<PageResponse<ProjectItem>>('/tech-resources/projects/list', params)
}
export function getProjectDetail(id: string) {
return get<ProjectItem>(`/tech-resources/projects/${id}`)
}
export function createProject(data: ProjectItem) {
return post('/tech-resources/projects', data)
}
export function updateProject(id: string, data: ProjectItem) {
return put(`/tech-resources/projects/${id}`, data)
}
export function deleteProject(id: string) {
return del(`/tech-resources/projects/${id}`)
}
// ==================== 科技成果 ====================
export interface AchievementItem {
id?: string
name: string
type: string
author: string
organization: string
level: string
date: string
description?: string
}
export function getAchievementList(params: PageParams) {
return get<PageResponse<AchievementItem>>('/tech-resources/achievements/list', params)
}
export function getAchievementDetail(id: string) {
return get<AchievementItem>(`/tech-resources/achievements/${id}`)
}
export function createAchievement(data: AchievementItem) {
return post('/tech-resources/achievements', data)
}
export function updateAchievement(id: string, data: AchievementItem) {
return put(`/tech-resources/achievements/${id}`, data)
}
export function deleteAchievement(id: string) {
return del(`/tech-resources/achievements/${id}`)
}
// ==================== 科技报告 ====================
export interface ReportItem {
id?: string
title: string
type: string
author: string
department: string
status: string
publishDate: string
content?: string
}
export function getReportList(params: PageParams) {
return get<PageResponse<ReportItem>>('/tech-resources/reports/list', params)
}
export function getReportDetail(id: string) {
return get<ReportItem>(`/tech-resources/reports/${id}`)
}
export function createReport(data: ReportItem) {
return post('/tech-resources/reports', data)
}
export function updateReport(id: string, data: ReportItem) {
return put(`/tech-resources/reports/${id}`, data)
}
export function deleteReport(id: string) {
return del(`/tech-resources/reports/${id}`)
}
// ==================== 科技奖励 ====================
export interface AwardItem {
id?: string
awardName: string
category: string
awardingOrganization: string
awardTime: string
awardYear: string
winner: string
status: string
description?: string
}
export function getAwardList(params: PageParams) {
return get<PageResponse<AwardItem>>('/tech-resources/awards/list', params)
}
export function getAwardDetail(id: string) {
return get<AwardItem>(`/tech-resources/awards/${id}`)
}
export function createAward(data: AwardItem) {
return post('/tech-resources/awards', data)
}
export function updateAward(id: string, data: AwardItem) {
return put(`/tech-resources/awards/${id}`, data)
}
export function deleteAward(id: string) {
return del(`/tech-resources/awards/${id}`)
}

153
src/api/upload.ts Normal file
View File

@ -0,0 +1,153 @@
import service from './request'
import { ResponseData } from './request'
// 上传响应数据
export interface UploadResponse {
url: string // 文件访问地址
fileName: string // 文件名
fileSize: number // 文件大小(字节)
fileType: string // 文件类型
}
/**
* OSS
* @param file
* @param onProgress
* @returns Promise<ResponseData<UploadResponse>>
*/
export function uploadFile(
file: File,
onProgress?: (progressEvent: any) => void
): Promise<ResponseData<UploadResponse>> {
const formData = new FormData()
formData.append('file', file)
return service.post('/oss/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: onProgress
})
}
/**
*
* @param files
* @param onProgress
* @returns Promise<ResponseData<UploadResponse[]>>
*/
export function uploadFiles(
files: File[],
onProgress?: (progressEvent: any) => void
): Promise<ResponseData<UploadResponse[]>> {
const formData = new FormData()
files.forEach((file) => {
formData.append('files', file)
})
return service.post('/oss/upload/batch', formData, {
headers: {
'Content-Type': 'multipart/form-data'
},
onUploadProgress: onProgress
})
}
/**
*
* @param file
* @param options
* @returns Promise<ResponseData<UploadResponse>>
*/
export interface UploadImageOptions {
maxSize?: number // 最大文件大小MB默认 5MB
allowedTypes?: string[] // 允许的文件类型,默认 ['image/jpeg', 'image/png', 'image/gif', 'image/webp']
onProgress?: (progressEvent: any) => void
}
export function uploadImage(
file: File,
options: UploadImageOptions = {}
): Promise<ResponseData<UploadResponse>> {
const {
maxSize = 5,
allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
onProgress
} = options
// 验证文件类型
if (!allowedTypes.includes(file.type)) {
return Promise.reject(new Error(`只支持上传 ${allowedTypes.join(', ')} 格式的图片`))
}
// 验证文件大小
const maxSizeBytes = maxSize * 1024 * 1024
if (file.size > maxSizeBytes) {
return Promise.reject(new Error(`图片大小不能超过 ${maxSize}MB`))
}
return uploadFile(file, onProgress)
}
/**
* WordExcelPDF
* @param file
* @param options
* @returns Promise<ResponseData<UploadResponse>>
*/
export interface UploadDocumentOptions {
maxSize?: number // 最大文件大小MB默认 10MB
allowedTypes?: string[] // 允许的文件类型
onProgress?: (progressEvent: any) => void
}
export function uploadDocument(
file: File,
options: UploadDocumentOptions = {}
): Promise<ResponseData<UploadResponse>> {
const {
maxSize = 10,
allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument.presentationml.presentation'
],
onProgress
} = options
// 验证文件类型
if (!allowedTypes.includes(file.type)) {
return Promise.reject(new Error('不支持的文档格式'))
}
// 验证文件大小
const maxSizeBytes = maxSize * 1024 * 1024
if (file.size > maxSizeBytes) {
return Promise.reject(new Error(`文档大小不能超过 ${maxSize}MB`))
}
return uploadFile(file, onProgress)
}
/**
*
* @param url
* @returns Promise<ResponseData<void>>
*/
export function deleteFile(url: string): Promise<ResponseData<void>> {
return service.post('/oss/delete', { url })
}
/**
* 访
* @param url
* @param expires 3600
* @returns Promise<ResponseData<{ url: string }>>
*/
export function getFileUrl(url: string, expires = 3600): Promise<ResponseData<{ url: string }>> {
return service.post('/oss/getUrl', { url, expires })
}

View File

@ -0,0 +1,185 @@
<template>
<div class="upload-example">
<h3>文件上传示例</h3>
<!-- 示例 1: 单图片上传 -->
<div class="upload-section">
<h4>1. 单图片上传</h4>
<el-upload
class="upload-demo"
:auto-upload="false"
:on-change="handleImageChange"
:show-file-list="false"
accept="image/*"
>
<el-button type="primary">选择图片</el-button>
</el-upload>
<div v-if="imageUrl" class="preview">
<img :src="imageUrl" alt="预览图" />
<el-progress v-if="uploadProgress > 0 && uploadProgress < 100" :percentage="uploadProgress" />
</div>
</div>
<!-- 示例 2: 多文件上传 -->
<div class="upload-section">
<h4>2. 多文件上传</h4>
<el-upload
class="upload-demo"
:auto-upload="false"
:on-change="handleFilesChange"
:file-list="fileList"
multiple
>
<el-button type="primary">选择文件</el-button>
</el-upload>
<el-button type="success" @click="handleBatchUpload" :loading="batchUploading">
批量上传
</el-button>
</div>
<!-- 示例 3: 拖拽上传 -->
<div class="upload-section">
<h4>3. 拖拽上传</h4>
<el-upload
class="upload-demo"
drag
:auto-upload="false"
:on-change="handleDragChange"
accept=".pdf,.doc,.docx,.xls,.xlsx"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处<em>点击上传</em>
</div>
<template #tip>
<div class="el-upload__tip">
支持 PDFWordExcel 格式文件大小不超过 10MB
</div>
</template>
</el-upload>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { uploadImage, uploadFile, uploadFiles, uploadDocument } from '@/api/upload'
import type { UploadFile } from 'element-plus'
defineOptions({
name: 'UploadExample'
})
//
const imageUrl = ref('')
const uploadProgress = ref(0)
const handleImageChange = async (file: UploadFile) => {
if (!file.raw) return
try {
uploadProgress.value = 0
const res = await uploadImage(file.raw, {
maxSize: 5,
onProgress: (progressEvent: any) => {
uploadProgress.value = Math.round((progressEvent.loaded * 100) / progressEvent.total)
}
})
imageUrl.value = res.data.url
uploadProgress.value = 100
ElMessage.success('上传成功')
} catch (error: any) {
ElMessage.error(error.message || '上传失败')
uploadProgress.value = 0
}
}
//
const fileList = ref<UploadFile[]>([])
const batchUploading = ref(false)
const handleFilesChange = (file: UploadFile, files: UploadFile[]) => {
fileList.value = files
}
const handleBatchUpload = async () => {
if (fileList.value.length === 0) {
ElMessage.warning('请先选择文件')
return
}
batchUploading.value = true
try {
const files = fileList.value.map(f => f.raw).filter(Boolean) as File[]
const res = await uploadFiles(files, (progressEvent: any) => {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
console.log('上传进度:', progress)
})
ElMessage.success(`成功上传 ${res.data.length} 个文件`)
fileList.value = []
} catch (error: any) {
ElMessage.error(error.message || '上传失败')
} finally {
batchUploading.value = false
}
}
//
const handleDragChange = async (file: UploadFile) => {
if (!file.raw) return
try {
const res = await uploadDocument(file.raw, {
maxSize: 10,
onProgress: (progressEvent: any) => {
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
console.log('上传进度:', progress)
}
})
ElMessage.success('文档上传成功')
console.log('文件地址:', res.data.url)
} catch (error: any) {
ElMessage.error(error.message || '上传失败')
}
}
</script>
<style scoped>
.upload-example {
padding: 20px;
}
.upload-section {
margin-bottom: 40px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
}
.upload-section h4 {
margin-top: 0;
margin-bottom: 20px;
color: #303133;
}
.preview {
margin-top: 20px;
}
.preview img {
max-width: 300px;
max-height: 300px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.el-progress {
margin-top: 10px;
}
</style>

24
src/config/env.ts Normal file
View File

@ -0,0 +1,24 @@
// 环境配置
export const ENV = {
// 开发环境
development: {
baseURL: '/brain', // 开发环境使用代理
timeout: 15000
},
// 生产环境
production: {
baseURL: 'http://47.110.148.47:8090/brain',
timeout: 15000
},
// 测试环境
test: {
baseURL: 'http://47.110.148.47:8090/brain',
timeout: 15000
}
}
// 获取当前环境配置
export function getEnvConfig() {
const env = process.env.NODE_ENV || 'development'
return ENV[env as keyof typeof ENV] || ENV.development
}

View File

@ -9,7 +9,7 @@
<el-dropdown @command="handleCommand"> <el-dropdown @command="handleCommand">
<span class="user-info"> <span class="user-info">
<el-icon><User /></el-icon> <el-icon><User /></el-icon>
<span>管理员</span> <span>{{ userStore.realName || userStore.username || '管理员' }}</span>
<el-icon><ArrowDown /></el-icon> <el-icon><ArrowDown /></el-icon>
</span> </span>
<template #dropdown> <template #dropdown>
@ -92,6 +92,11 @@
<el-icon><Lock /></el-icon> <el-icon><Lock /></el-icon>
<span>权限管理</span> <span>权限管理</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/admin/dict">
<el-icon><Setting /></el-icon>
<span>字典管理</span>
</el-menu-item>
</el-menu-item-group> </el-menu-item-group>
</el-menu> </el-menu>
</aside> </aside>
@ -119,12 +124,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { useUserStore } from '@/store/user'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const userStore = useUserStore()
//
onMounted(() => {
userStore.initUserInfo()
})
const activeMenu = computed(() => { const activeMenu = computed(() => {
// //
@ -146,7 +158,8 @@ const breadcrumbConfig: Record<string, { title: string; parent?: string }> = {
'/admin/tech-reports': { title: '科技报告', parent: '科技资源' }, '/admin/tech-reports': { title: '科技报告', parent: '科技资源' },
'/admin/tech-awards': { title: '科技奖励', parent: '科技资源' }, '/admin/tech-awards': { title: '科技奖励', parent: '科技资源' },
'/admin/users': { title: '用户管理', parent: '系统管理' }, '/admin/users': { title: '用户管理', parent: '系统管理' },
'/admin/permissions': { title: '权限管理', parent: '系统管理' } '/admin/permissions': { title: '权限管理', parent: '系统管理' },
'/admin/dict': { title: '字典管理', parent: '系统管理' }
} }
// //
@ -192,8 +205,16 @@ const handleCommand = (command: string) => {
ElMessage.info('个人信息功能开发中') ElMessage.info('个人信息功能开发中')
break break
case 'logout': case 'logout':
ElMessage.success('退出登录成功') ElMessageBox.confirm('确定要退出登录吗?', '提示', {
router.push('/login') confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
await userStore.handleLogout()
router.push('/login')
}).catch(() => {
//
})
break break
} }
} }

View File

@ -1,7 +1,9 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import store from './store'
// Pinia
import { createPinia } from 'pinia'
// Element Plus // Element Plus
import ElementPlus from 'element-plus' import ElementPlus from 'element-plus'
@ -9,10 +11,11 @@ import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue' import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia()
// 注册所有图标 // 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) { for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component) app.component(key, component)
} }
app.use(store).use(router).use(ElementPlus).mount('#app') app.use(pinia).use(router).use(ElementPlus).mount('#app')

View File

@ -5,11 +5,6 @@ import LoginPage from '@/views/LoginPage.vue'
// 后台管理系统组件 // 后台管理系统组件
import AdminLayout from '@/layouts/AdminLayout.vue' import AdminLayout from '@/layouts/AdminLayout.vue'
import AdminDashboard from '@/views/admin/AdminDashboard.vue'
import NewsPolicyAdmin from '@/views/admin/NewsPolicyAdmin.vue'
import SmartQAAdmin from '@/views/admin/SmartQAAdmin.vue'
import TechResourcesAdmin from '@/views/admin/TechResourcesAdmin.vue'
import TalentProfileAdmin from '@/views/admin/TalentProfileAdmin.vue'
const routes: Array<RouteRecordRaw> = [ const routes: Array<RouteRecordRaw> = [
{ {
@ -27,141 +22,159 @@ const routes: Array<RouteRecordRaw> = [
component: AdminLayout, component: AdminLayout,
redirect: '/admin/dashboard', redirect: '/admin/dashboard',
children: [ children: [
// 数据概览
{ {
path: 'dashboard', path: 'dashboard',
name: 'admin-dashboard', name: 'admin-dashboard',
component: AdminDashboard component: () => import('@/views/admin/dashboard/index.vue')
}, },
// 内容管理
{ {
path: 'news-policy', path: 'news-policy',
name: 'admin-news-policy', name: 'admin-news-policy',
component: NewsPolicyAdmin component: () => import('@/views/admin/content/news-policy.vue')
}, },
{ {
path: 'news-policy/create', path: 'news-policy/create',
name: 'admin-news-policy-create', name: 'admin-news-policy-create',
component: () => import('@/views/admin/NewsPolicyForm.vue') component: () => import('@/views/admin/content/news-policy-form.vue')
}, },
{ {
path: 'news-policy/edit/:id', path: 'news-policy/edit/:id',
name: 'admin-news-policy-edit', name: 'admin-news-policy-edit',
component: () => import('@/views/admin/NewsPolicyForm.vue') component: () => import('@/views/admin/content/news-policy-form.vue')
}, },
{ {
path: 'smart-qa', path: 'smart-qa',
name: 'admin-smart-qa', name: 'admin-smart-qa',
component: SmartQAAdmin component: () => import('@/views/admin/content/smart-qa.vue')
}, },
{ {
path: 'smart-qa/create', path: 'smart-qa/create',
name: 'admin-smart-qa-create', name: 'admin-smart-qa-create',
component: () => import('@/views/admin/SmartQAForm.vue') component: () => import('@/views/admin/content/smart-qa-form.vue')
}, },
{ {
path: 'smart-qa/edit/:id', path: 'smart-qa/edit/:id',
name: 'admin-smart-qa-edit', name: 'admin-smart-qa-edit',
component: () => import('@/views/admin/SmartQAForm.vue') component: () => import('@/views/admin/content/smart-qa-form.vue')
}, },
// 科技资源
{ {
path: 'tech-resources', path: 'tech-resources',
name: 'admin-tech-resources', name: 'admin-tech-resources',
component: TechResourcesAdmin component: () => import('@/views/admin/tech-resources/resources.vue')
}, },
{ {
path: 'tech-resources/create', path: 'tech-resources/create',
name: 'admin-tech-resources-create', name: 'admin-tech-resources-create',
component: () => import('@/views/admin/TechResourcesForm.vue') component: () => import('@/views/admin/tech-resources/resources-form.vue')
}, },
{ {
path: 'tech-resources/edit/:id', path: 'tech-resources/edit/:id',
name: 'admin-tech-resources-edit', name: 'admin-tech-resources-edit',
component: () => import('@/views/admin/TechResourcesForm.vue') component: () => import('@/views/admin/tech-resources/resources-form.vue')
}, },
{ {
path: 'talent-profile', path: 'talent-profile',
name: 'admin-talent-profile', name: 'admin-talent-profile',
component: TalentProfileAdmin component: () => import('@/views/admin/tech-resources/talent-profile.vue')
}, },
{ {
path: 'talent-profile/create', path: 'talent-profile/create',
name: 'admin-talent-profile-create', name: 'admin-talent-profile-create',
component: () => import('@/views/admin/TalentProfileForm.vue') component: () => import('@/views/admin/tech-resources/talent-profile-form.vue')
}, },
{ {
path: 'talent-profile/edit/:id', path: 'talent-profile/edit/:id',
name: 'admin-talent-profile-edit', name: 'admin-talent-profile-edit',
component: () => import('@/views/admin/TalentProfileForm.vue') component: () => import('@/views/admin/tech-resources/talent-profile-form.vue')
}, },
{ {
path: 'tech-projects', path: 'tech-projects',
name: 'admin-tech-projects', name: 'admin-tech-projects',
component: () => import('@/views/admin/TechProjectsAdmin.vue') component: () => import('@/views/admin/tech-resources/projects.vue')
}, },
{ {
path: 'tech-projects/create', path: 'tech-projects/create',
name: 'admin-tech-projects-create', name: 'admin-tech-projects-create',
component: () => import('@/views/admin/TechProjectsForm.vue') component: () => import('@/views/admin/tech-resources/projects-form.vue')
}, },
{ {
path: 'tech-projects/edit/:id', path: 'tech-projects/edit/:id',
name: 'admin-tech-projects-edit', name: 'admin-tech-projects-edit',
component: () => import('@/views/admin/TechProjectsForm.vue') component: () => import('@/views/admin/tech-resources/projects-form.vue')
}, },
{ {
path: 'tech-achievements', path: 'tech-achievements',
name: 'admin-tech-achievements', name: 'admin-tech-achievements',
component: () => import('@/views/admin/TechAchievementsAdmin.vue') component: () => import('@/views/admin/tech-resources/achievements.vue')
}, },
{ {
path: 'tech-achievements/create', path: 'tech-achievements/create',
name: 'admin-tech-achievements-create', name: 'admin-tech-achievements-create',
component: () => import('@/views/admin/TechAchievementsForm.vue') component: () => import('@/views/admin/tech-resources/achievements-form.vue')
}, },
{ {
path: 'tech-achievements/edit/:id', path: 'tech-achievements/edit/:id',
name: 'admin-tech-achievements-edit', name: 'admin-tech-achievements-edit',
component: () => import('@/views/admin/TechAchievementsForm.vue') component: () => import('@/views/admin/tech-resources/achievements-form.vue')
}, },
{ {
path: 'tech-reports', path: 'tech-reports',
name: 'admin-tech-reports', name: 'admin-tech-reports',
component: () => import('@/views/admin/TechReportsAdmin.vue') component: () => import('@/views/admin/tech-resources/reports.vue')
}, },
{ {
path: 'tech-reports/create', path: 'tech-reports/create',
name: 'admin-tech-reports-create', name: 'admin-tech-reports-create',
component: () => import('@/views/admin/TechReportsForm.vue') component: () => import('@/views/admin/tech-resources/reports-form.vue')
}, },
{ {
path: 'tech-reports/edit/:id', path: 'tech-reports/edit/:id',
name: 'admin-tech-reports-edit', name: 'admin-tech-reports-edit',
component: () => import('@/views/admin/TechReportsForm.vue') component: () => import('@/views/admin/tech-resources/reports-form.vue')
}, },
{ {
path: 'tech-awards', path: 'tech-awards',
name: 'admin-tech-awards', name: 'admin-tech-awards',
component: () => import('@/views/admin/TechAwardsAdmin.vue') component: () => import('@/views/admin/tech-resources/awards.vue')
}, },
{ {
path: 'tech-awards/create', path: 'tech-awards/create',
name: 'admin-tech-awards-create', name: 'admin-tech-awards-create',
component: () => import('@/views/admin/TechAwardsForm.vue') component: () => import('@/views/admin/tech-resources/awards-form.vue')
}, },
{ {
path: 'tech-awards/edit/:id', path: 'tech-awards/edit/:id',
name: 'admin-tech-awards-edit', name: 'admin-tech-awards-edit',
component: () => import('@/views/admin/TechAwardsForm.vue') component: () => import('@/views/admin/tech-resources/awards-form.vue')
}, },
// 系统管理
{ {
path: 'users', path: 'users',
name: 'admin-users', name: 'admin-users',
component: () => import('@/views/admin/UsersAdmin.vue') component: () => import('@/views/admin/system/users.vue')
}, },
{ {
path: 'permissions', path: 'permissions',
name: 'admin-permissions', name: 'admin-permissions',
component: () => import('@/views/admin/PermissionsAdmin.vue') component: () => import('@/views/admin/system/permissions.vue')
} },
{
path: 'dict',
name: 'admin-dict',
component: () => import('@/views/admin/system/dict.vue')
},
{
path: 'test-proxy',
name: 'admin-test-proxy',
component: () => import('@/views/admin/test-proxy.vue')
},
] ]
} }
] ]
@ -171,4 +184,33 @@ const router = createRouter({
routes routes
}) })
// 路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
// 如果访问登录页面
if (to.path === '/login') {
// 如果已经登录,重定向到后台首页
if (token) {
next('/admin/dashboard')
} else {
next()
}
return
}
// 如果访问后台页面
if (to.path.startsWith('/admin')) {
// 检查是否已登录
if (!token) {
next('/login')
} else {
next()
}
return
}
next()
})
export default router export default router

View File

@ -1,14 +0,0 @@
import { createStore } from 'vuex'
export default createStore({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})

140
src/store/user.ts Normal file
View File

@ -0,0 +1,140 @@
import { defineStore } from 'pinia'
import { getUserInfo, logout, type UserInfo, type MenuItem } from '@/api/auth'
import { ElMessage } from 'element-plus'
export const useUserStore = defineStore('user', {
state: () => ({
userInfo: null as UserInfo | null,
token: '' as string
}),
getters: {
isLoggedIn: (state) => !!state.token,
username: (state) => state.userInfo?.username || '',
realName: (state) => state.userInfo?.realName || '',
roles: (state) => state.userInfo?.roles || [],
permissions: (state) => state.userInfo?.permissions || [],
menus: (state) => state.userInfo?.menus || []
},
actions: {
// 初始化用户信息
initUserInfo() {
const savedToken = localStorage.getItem('token')
const savedUserInfo = localStorage.getItem('userInfo')
if (savedToken) {
this.token = savedToken
}
if (savedUserInfo) {
try {
this.userInfo = JSON.parse(savedUserInfo)
} catch (error) {
console.error('解析用户信息失败:', error)
localStorage.removeItem('userInfo')
}
}
},
// 设置用户信息
setUserInfo(info: UserInfo) {
this.userInfo = info
localStorage.setItem('userInfo', JSON.stringify(info))
},
// 设置 token
setToken(newToken: string) {
this.token = newToken
localStorage.setItem('token', newToken)
},
// 获取用户信息
async fetchUserInfo() {
try {
const res = await getUserInfo()
this.setUserInfo(res.data)
return res.data
} catch (error) {
console.error('获取用户信息失败:', error)
throw error
}
},
// 登出
async handleLogout() {
try {
await logout()
} catch (error) {
console.error('登出接口调用失败:', error)
} finally {
// 清除本地数据
this.clearUserData()
ElMessage.success('已退出登录')
}
},
// 清除用户数据
clearUserData() {
this.userInfo = null
this.token = ''
localStorage.removeItem('token')
localStorage.removeItem('userInfo')
},
// 检查权限
hasPermission(permission: string): boolean {
return this.permissions.includes(permission)
},
// 检查角色
hasRole(role: string): boolean {
return this.roles.includes(role)
},
// 获取菜单树
getMenuTree(): MenuItem[] {
return this.buildMenuTree(this.menus)
},
// 构建菜单树
buildMenuTree(menuList: MenuItem[]): MenuItem[] {
const menuMap = new Map<string, MenuItem>()
const rootMenus: MenuItem[] = []
// 创建菜单映射
menuList.forEach(menu => {
menuMap.set(menu.id, { ...menu, children: [] })
})
// 构建树结构
menuList.forEach(menu => {
const menuItem = menuMap.get(menu.id)!
if (menu.parentId && menuMap.has(menu.parentId)) {
const parent = menuMap.get(menu.parentId)!
if (!parent.children) {
parent.children = []
}
parent.children.push(menuItem)
} else {
rootMenus.push(menuItem)
}
})
// 按 sort 排序
const sortMenus = (menus: MenuItem[]) => {
menus.sort((a, b) => a.sort - b.sort)
menus.forEach(menu => {
if (menu.children && menu.children.length > 0) {
sortMenus(menu.children)
}
})
}
sortMenus(rootMenus)
return rootMenus
}
}
})

223
src/utils/upload.ts Normal file
View File

@ -0,0 +1,223 @@
import { ElMessage } from "element-plus";
import { uploadImage, uploadDocument, uploadFile } from "@/api/upload";
/**
*
* @param bytes
* @returns
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}
/**
*
* @param filename
* @returns
*/
export function getFileExtension(filename: string): string {
return filename
.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2)
.toLowerCase();
}
/**
*
* @param file
* @param allowedTypes
* @returns
*/
export function validateFileType(file: File, allowedTypes: string[]): boolean {
return allowedTypes.includes(file.type);
}
/**
*
* @param file
* @param maxSizeMB MB
* @returns
*/
export function validateFileSize(file: File, maxSizeMB: number): boolean {
return file.size <= maxSizeMB * 1024 * 1024;
}
/**
*
* @param file
* @param options
* @returns Promise<string> URL
*/
export interface UploadOptions {
type?: "image" | "document" | "file";
maxSize?: number;
allowedTypes?: string[];
onProgress?: (progress: number) => void;
onSuccess?: (url: string) => void;
onError?: (error: Error) => void;
}
export async function handleUpload(
file: File,
options: UploadOptions = {}
): Promise<string> {
const {
type = "file",
maxSize,
allowedTypes,
onProgress,
onSuccess,
onError,
} = options;
try {
// 进度回调包装
const progressCallback = onProgress
? (progressEvent: any) => {
const progress = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
onProgress(progress);
}
: undefined;
let res;
// 根据类型选择上传方法
switch (type) {
case "image":
res = await uploadImage(file, {
maxSize,
allowedTypes,
onProgress: progressCallback,
});
break;
case "document":
res = await uploadDocument(file, {
maxSize,
allowedTypes,
onProgress: progressCallback,
});
break;
default:
res = await uploadFile(file, progressCallback);
}
const url = res.data.url;
if (onSuccess) {
onSuccess(url);
}
return url;
} catch (error: any) {
if (onError) {
onError(error);
} else {
ElMessage.error(error.message || "上传失败");
}
throw error;
}
}
/**
*
* @param file
* @param maxWidth
* @param quality 0-1
* @returns Promise<File>
*/
export function compressImage(
file: File,
maxWidth = 1920,
quality = 0.8
): Promise<File> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = (e) => {
const img = new Image();
img.src = e.target?.result as string;
img.onload = () => {
const canvas = document.createElement("canvas");
let width = img.width;
let height = img.height;
// 计算缩放比例
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx?.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (blob) {
const compressedFile = new File([blob], file.name, {
type: file.type,
lastModified: Date.now(),
});
resolve(compressedFile);
} else {
reject(new Error("图片压缩失败"));
}
},
file.type,
quality
);
};
img.onerror = () => {
reject(new Error("图片加载失败"));
};
};
reader.onerror = () => {
reject(new Error("文件读取失败"));
};
});
}
/**
* Base64 File
* @param base64 base64
* @param filename
* @returns File
*/
export function base64ToFile(base64: string, filename: string): File {
const arr = base64.split(",");
const mime = arr[0].match(/:(.*?);/)?.[1] || "image/png";
const bstr = atob(arr[1]);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new File([u8arr], filename, { type: mime });
}
/**
* File Base64
* @param file
* @returns Promise<string> base64
*/
export function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
}

View File

@ -2,8 +2,8 @@
<div class="login-container"> <div class="login-container">
<div class="login-box"> <div class="login-box">
<div class="login-header"> <div class="login-header">
<h1>科技大脑·后台管理系统</h1> <h1>科技管理系统</h1>
<p>请登录您的账户</p> <p>欢迎登录后台管理系统</p>
</div> </div>
<el-form <el-form
@ -11,7 +11,7 @@
:model="loginForm" :model="loginForm"
:rules="loginRules" :rules="loginRules"
class="login-form" class="login-form"
@submit.prevent="handleLogin" @keyup.enter="handleLogin"
> >
<el-form-item prop="username"> <el-form-item prop="username">
<el-input <el-input
@ -30,7 +30,6 @@
size="large" size="large"
prefix-icon="Lock" prefix-icon="Lock"
show-password show-password
@keyup.enter="handleLogin"
/> />
</el-form-item> </el-form-item>
@ -38,18 +37,14 @@
<el-button <el-button
type="primary" type="primary"
size="large" size="large"
class="login-button"
:loading="loading" :loading="loading"
@click="handleLogin" @click="handleLogin"
class="login-button"
> >
{{ loading ? '登录中...' : '登录' }} {{ loading ? '登录中...' : '登录' }}
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="login-tips">
<p>默认账户admin / 123456</p>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -57,117 +52,157 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive } from 'vue' import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import { login, getUserInfo, type LoginParams } from '@/api/auth'
defineOptions({
name: 'LoginPage'
})
const router = useRouter() const router = useRouter()
const loginFormRef = ref<FormInstance>() const loginFormRef = ref<FormInstance>()
const loading = ref(false) const loading = ref(false)
const loginForm = reactive({ //
const loginForm = reactive<LoginParams>({
username: '', username: '',
password: '' password: '',
loginType: 2 // 2
}) })
//
const loginRules: FormRules = { const loginRules: FormRules = {
username: [ username: [
{ required: true, message: '请输入用户名', trigger: 'blur' } { required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
], ],
password: [ password: [
{ required: true, message: '请输入密码', trigger: 'blur' }, { required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码长度不能少于6位', trigger: 'blur' } { min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
] ]
} }
const handleLogin = () => { //
loginFormRef.value?.validate((valid) => { const handleLogin = async () => {
if (!loginFormRef.value) return
await loginFormRef.value.validate(async (valid) => {
if (valid) { if (valid) {
loading.value = true loading.value = true
try {
// console.log('开始登录...', loginForm)
setTimeout(() => {
if (loginForm.username === 'admin' && loginForm.password === '123456') { //
ElMessage.success('登录成功') const loginRes = await login(loginForm)
router.push('/admin') console.log('登录响应:', loginRes)
} else {
ElMessage.error('用户名或密码错误') // token
if (loginRes.data.token) {
localStorage.setItem('token', loginRes.data.token)
console.log('Token 已保存:', loginRes.data.token)
} }
//
try {
const userInfoRes = await getUserInfo()
console.log('用户信息:', userInfoRes)
//
localStorage.setItem('userInfo', JSON.stringify(userInfoRes.data))
ElMessage.success('登录成功')
//
router.push('/admin/dashboard')
} catch (userInfoError) {
console.error('获取用户信息失败:', userInfoError)
ElMessage.warning('登录成功,但获取用户信息失败')
//
router.push('/admin/dashboard')
}
} catch (error: any) {
console.error('登录失败:', error)
ElMessage.error(error.message || '登录失败,请检查用户名和密码')
} finally {
loading.value = false loading.value = false
}, 1000) }
} }
}) })
} }
//
const setTestAccount = () => {
loginForm.username = 'admin'
loginForm.password = '123456'
}
//
if (process.env.NODE_ENV === 'development') {
// setTestAccount() //
}
</script> </script>
<style scoped> <style scoped>
.login-container { .login-container {
min-height: 100vh; min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px; padding: 20px;
} }
.login-box { .login-box {
background: #ffffff;
border-radius: 16px;
padding: 40px;
width: 100%; width: 100%;
max-width: 400px; max-width: 400px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); background: white;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
padding: 40px;
} }
.login-header { .login-header {
text-align: center; text-align: center;
margin-bottom: 30px; margin-bottom: 40px;
} }
.login-header h1 { .login-header h1 {
color: #303133; color: #303133;
font-size: 24px; font-size: 28px;
font-weight: 600; font-weight: 600;
margin-bottom: 8px; margin: 0 0 10px;
} }
.login-header p { .login-header p {
color: #909399; color: #909399;
font-size: 14px; font-size: 14px;
margin: 0;
} }
.login-form { .login-form {
margin-bottom: 20px; width: 100%;
} }
.login-form .el-form-item { .login-form .el-form-item {
margin-bottom: 20px; margin-bottom: 24px;
} }
.login-button { .login-button {
width: 100%; width: 100%;
height: 48px; height: 48px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 500;
}
.login-tips {
text-align: center;
padding-top: 20px;
border-top: 1px solid #f0f2f5;
}
.login-tips p {
color: #909399;
font-size: 12px;
} }
/* 响应式设计 */
@media (max-width: 480px) { @media (max-width: 480px) {
.login-box { .login-box {
padding: 30px 20px; padding: 30px 20px;
} }
.login-header h1 { .login-header h1 {
font-size: 20px; font-size: 24px;
} }
} }
</style> </style>

67
src/views/admin/README.md Normal file
View File

@ -0,0 +1,67 @@
# 后台管理系统页面结构
## 文件夹组织
```
src/views/admin/
├── dashboard/ # 数据概览
│ └── index.vue # 数据概览主页
├── content/ # 内容管理
│ ├── news-policy.vue # 新闻政策列表页
│ ├── news-policy-form.vue # 新闻政策表单页
│ ├── smart-qa.vue # 科技问答列表页
│ └── smart-qa-form.vue # 科技问答表单页
├── tech-resources/ # 科技资源
│ ├── resources.vue # 科技资源列表页
│ ├── resources-form.vue # 科技资源表单页
│ ├── talent-profile.vue # 人才档案列表页
│ ├── talent-profile-form.vue # 人才档案表单页
│ ├── projects.vue # 科技项目列表页
│ ├── projects-form.vue # 科技项目表单页
│ ├── achievements.vue # 科技成果列表页
│ ├── achievements-form.vue # 科技成果表单页
│ ├── reports.vue # 科技报告列表页
│ ├── reports-form.vue # 科技报告表单页
│ ├── awards.vue # 科技奖励列表页
│ └── awards-form.vue # 科技奖励表单页
└── system/ # 系统管理
├── users.vue # 用户管理页
└── permissions.vue # 权限管理页
```
## 路由结构
### 数据概览
- `/admin/dashboard` - 数据概览主页
### 内容管理
- `/admin/news-policy` - 新闻政策列表
- `/admin/news-policy/create` - 新增新闻政策
- `/admin/news-policy/edit/:id` - 编辑新闻政策
- `/admin/smart-qa` - 科技问答列表
- `/admin/smart-qa/create` - 新增科技问答
- `/admin/smart-qa/edit/:id` - 编辑科技问答
### 科技资源
- `/admin/tech-resources` - 科技资源列表
- `/admin/tech-resources/create` - 新增科技资源
- `/admin/tech-resources/edit/:id` - 编辑科技资源
- `/admin/talent-profile` - 人才档案列表
- `/admin/talent-profile/create` - 新增人才档案
- `/admin/talent-profile/edit/:id` - 编辑人才档案
- `/admin/tech-projects` - 科技项目列表
- `/admin/tech-projects/create` - 新增科技项目
- `/admin/tech-projects/edit/:id` - 编辑科技项目
- `/admin/tech-achievements` - 科技成果列表
- `/admin/tech-achievements/create` - 新增科技成果
- `/admin/tech-achievements/edit/:id` - 编辑科技成果
- `/admin/tech-reports` - 科技报告列表
- `/admin/tech-reports/create` - 新增科技报告
- `/admin/tech-reports/edit/:id` - 编辑科技报告
- `/admin/tech-awards` - 科技奖励列表
- `/admin/tech-awards/create` - 新增科技奖励
- `/admin/tech-awards/edit/:id` - 编辑科技奖励
### 系统管理
- `/admin/users` - 用户管理
- `/admin/permissions` - 权限管理

View File

@ -169,6 +169,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue' import { ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'NewsPolicyForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -85,7 +85,10 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
defineOptions({
name: 'NewsPolicyAdmin'
})
const router = useRouter() const router = useRouter()

View File

@ -118,6 +118,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue' import { ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'SmartQAForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -74,6 +74,10 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'SmartQAAdmin'
})
const router = useRouter() const router = useRouter()
interface QAItem { interface QAItem {

View File

@ -113,6 +113,10 @@
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick } from 'vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
defineOptions({
name: 'AdminDashboard'
})
interface Activity { interface Activity {
id: string id: string
text: string text: string

View File

@ -0,0 +1,364 @@
<template>
<div class="dict-admin">
<div class="page-header">
<h2>字典管理</h2>
</div>
<div class="search-section">
<div class="search-form">
<el-form :inline="true" :model="searchForm" class="search-form-inline">
<el-form-item label="字典类型">
<el-input
v-model="searchForm.typeCode"
placeholder="请输入字典类型"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="action-buttons">
<el-button type="primary" @click="handleAdd">新增字典</el-button>
<el-button type="success" @click="handleRefreshCache">刷新缓存</el-button>
</div>
</div>
<div class="table-section">
<el-table :data="tableData" style="width: 100%" stripe border v-loading="loading">
<el-table-column prop="paramType" label="字典类型" width="150" align="center" />
<el-table-column prop="paramName" label="字典名称" width="150" align="center" />
<el-table-column prop="paramValue" label="字典值" min-width="150" align="center" />
<el-table-column prop="dataType" label="数据类型" width="120" align="center" />
<el-table-column prop="sortOrder" label="排序" width="80" align="center" />
<el-table-column prop="status" label="状态" width="100" align="center">
<template #default="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'">
{{ scope.row.status === 0 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="200" show-overflow-tooltip />
<el-table-column label="操作" width="180" align="center" fixed="right">
<template #default="scope">
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination-section">
<el-pagination
v-model:current-page="pagination.currentPage"
v-model:page-size="pagination.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="pagination.total"
layout="total, sizes, prev, pager, next, jumper"
background
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="isEdit ? '编辑字典' : '新增字典'"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="字典类型" prop="paramType">
<el-input v-model="formData.paramType" placeholder="请输入字典类型编码" />
</el-form-item>
<el-form-item label="字典名称" prop="paramName">
<el-input v-model="formData.paramName" placeholder="请输入字典名称" />
</el-form-item>
<el-form-item label="字典值" prop="paramValue">
<el-input v-model="formData.paramValue" placeholder="请输入字典值" />
</el-form-item>
<el-form-item label="数据类型" prop="dataType">
<el-select v-model="formData.dataType" placeholder="请选择数据类型">
<el-option label="字符串" value="string" />
<el-option label="数字" value="number" />
<el-option label="布尔" value="boolean" />
</el-select>
</el-form-item>
<el-form-item label="排序" prop="sortOrder">
<el-input-number v-model="formData.sortOrder" :min="0" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="0">启用</el-radio>
<el-radio :label="1">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="formData.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
import {
getDictList,
createDict,
updateDict,
deleteDict,
refreshDictCache
} from '@/api/system'
import type { DictItem } from '@/api/system'
defineOptions({
name: 'DictAdmin'
})
const searchForm = reactive({
typeCode: ''
})
const tableData = ref<DictItem[]>([])
const loading = ref(false)
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
})
//
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref<FormInstance>()
const submitLoading = ref(false)
const formData = reactive<DictItem>({
paramName: '',
paramType: '',
paramValue: '',
dataType: 'string',
sortOrder: 0,
status: 0,
flag: 0,
remark: ''
})
const formRules: FormRules = {
paramType: [{ required: true, message: '请输入字典类型', trigger: 'blur' }],
paramName: [{ required: true, message: '请输入字典名称', trigger: 'blur' }],
paramValue: [{ required: true, message: '请输入字典值', trigger: 'blur' }],
dataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }]
}
//
const fetchData = async () => {
loading.value = true
try {
const res = await getDictList({
page: pagination.currentPage,
pageSize: pagination.pageSize,
typeCode: searchForm.typeCode
})
tableData.value = res.data.list
pagination.total = res.data.total
} catch (error) {
console.error('获取数据失败', error)
} finally {
loading.value = false
}
}
//
const handleSearch = () => {
pagination.currentPage = 1
fetchData()
}
//
const handleReset = () => {
searchForm.typeCode = ''
pagination.currentPage = 1
fetchData()
}
//
const handleAdd = () => {
isEdit.value = false
dialogVisible.value = true
}
//
const handleEdit = (row: DictItem) => {
isEdit.value = true
Object.assign(formData, row)
dialogVisible.value = true
}
//
const handleDelete = (row: DictItem) => {
ElMessageBox.confirm(
`确定要删除字典"${row.paramName}"吗?删除后无法恢复!`,
'删除确认',
{
confirmButtonText: '确定删除',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
await deleteDict(row.id!)
ElMessage.success('删除成功')
fetchData()
} catch (error) {
console.error('删除失败', error)
}
}).catch(() => {
ElMessage.info('已取消删除')
})
}
//
const handleRefreshCache = () => {
ElMessageBox.confirm(
'确定要刷新字典缓存吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(async () => {
try {
await refreshDictCache()
ElMessage.success('缓存刷新成功')
} catch (error) {
console.error('刷新缓存失败', error)
}
})
}
//
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
if (isEdit.value) {
await updateDict(formData)
ElMessage.success('更新成功')
} else {
await createDict(formData)
ElMessage.success('创建成功')
}
dialogVisible.value = false
fetchData()
} catch (error) {
console.error('提交失败', error)
} finally {
submitLoading.value = false
}
}
})
}
//
const handleDialogClose = () => {
formRef.value?.resetFields()
Object.assign(formData, {
paramName: '',
paramType: '',
paramValue: '',
dataType: 'string',
sortOrder: 0,
status: 0,
flag: 0,
remark: ''
})
}
//
const handleSizeChange = (size: number) => {
pagination.pageSize = size
pagination.currentPage = 1
fetchData()
}
const handleCurrentChange = (page: number) => {
pagination.currentPage = page
fetchData()
}
onMounted(() => {
fetchData()
})
</script>
<style scoped>
.dict-admin {
background: #ffffff;
border-radius: 8px;
padding: 20px;
}
.page-header {
margin-bottom: 20px;
}
.page-header h2 {
color: #303133;
font-size: 24px;
font-weight: 600;
margin: 0;
}
.search-section {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.search-form-inline {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.table-section {
margin-bottom: 20px;
}
.pagination-section {
display: flex;
justify-content: flex-end;
}
</style>

View File

@ -87,6 +87,10 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'PermissionsAdmin'
})
interface RoleItem { interface RoleItem {
id: string id: string
roleName: string roleName: string
@ -166,10 +170,12 @@ const handleAdd = () => {
const handleEdit = (row: RoleItem) => { const handleEdit = (row: RoleItem) => {
ElMessage.info('编辑功能开发中') ElMessage.info('编辑功能开发中')
console.log(row)
} }
const handlePermissions = (row: RoleItem) => { const handlePermissions = (row: RoleItem) => {
ElMessage.info('权限配置功能开发中') ElMessage.info('权限配置功能开发中')
console.log(row)
} }
const handleDelete = (row: RoleItem) => { const handleDelete = (row: RoleItem) => {

View File

@ -89,6 +89,10 @@
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'UsersAdmin'
})
interface UserItem { interface UserItem {
id: string id: string
username: string username: string
@ -168,6 +172,7 @@ const handleAdd = () => {
const handleEdit = (row: UserItem) => { const handleEdit = (row: UserItem) => {
ElMessage.info('编辑功能开发中') ElMessage.info('编辑功能开发中')
console.log(row)
} }
const handleToggleStatus = (row: UserItem) => { const handleToggleStatus = (row: UserItem) => {

View File

@ -187,6 +187,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'TechAchievementsForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -77,6 +77,10 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'TechAchievementsAdmin'
})
const router = useRouter() const router = useRouter()
interface AchievementItem { interface AchievementItem {

View File

@ -170,6 +170,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'TechAwardsForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -95,6 +95,10 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'TechAwardsAdmin'
})
const router = useRouter() const router = useRouter()
interface AwardItem { interface AwardItem {

View File

@ -181,6 +181,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'TechProjectsForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -83,6 +83,10 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'TechProjectsAdmin'
})
const router = useRouter() const router = useRouter()
interface ProjectItem { interface ProjectItem {

View File

@ -243,6 +243,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue' import { ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'TechReportsForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -77,6 +77,10 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'TechReportsAdmin'
})
const router = useRouter() const router = useRouter()
interface ReportItem { interface ReportItem {

View File

@ -111,6 +111,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'TechResourcesForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -76,6 +76,10 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'TechResourcesAdmin'
})
const router = useRouter() const router = useRouter()
interface ResourceItem { interface ResourceItem {

View File

@ -252,6 +252,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted, nextTick } from 'vue' import { ref, reactive, onMounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
defineOptions({
name: 'TalentProfileForm'
})
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import RichTextEditor from '@/components/RichTextEditor.vue' import RichTextEditor from '@/components/RichTextEditor.vue'

View File

@ -77,6 +77,10 @@ import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
defineOptions({
name: 'TalentProfileAdmin'
})
const router = useRouter() const router = useRouter()
interface TalentItem { interface TalentItem {

View File

@ -0,0 +1,133 @@
<template>
<div class="test-proxy">
<h2>代理测试页面</h2>
<div class="test-section">
<h3>环境信息</h3>
<p>当前环境: {{ currentEnv }}</p>
<p>Base URL: {{ baseURL }}</p>
<p>完整请求地址: {{ fullURL }}</p>
</div>
<div class="test-section">
<h3>测试请求</h3>
<el-button type="primary" @click="testRequest" :loading="loading">
测试 API 请求
</el-button>
<div v-if="result" class="result">
<h4>请求结果:</h4>
<pre>{{ result }}</pre>
</div>
<div v-if="error" class="error">
<h4>错误信息:</h4>
<pre>{{ error }}</pre>
</div>
</div>
<div class="test-section">
<h3>说明</h3>
<ul>
<li>如果看到跨域错误说明代理没有生效</li>
<li>请求应该发送到: http://localhost:8080/brain/...</li>
<li>而不是: http://47.110.148.47:8090/brain/...</li>
<li>如果代理正常请求会成功或返回后端错误非跨域错误</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { getEnvConfig } from '@/config/env'
import { getDictList } from '@/api/system'
defineOptions({
name: 'TestProxy'
})
const loading = ref(false)
const result = ref('')
const error = ref('')
const envConfig = getEnvConfig()
const currentEnv = process.env.NODE_ENV
const baseURL = envConfig.baseURL
const fullURL = computed(() => `${baseURL}/system/param/list`)
const testRequest = async () => {
loading.value = true
result.value = ''
error.value = ''
try {
console.log('发起测试请求...')
console.log('Base URL:', baseURL)
console.log('完整地址:', fullURL.value)
const res = await getDictList({
page: 1,
pageSize: 10
})
result.value = JSON.stringify(res, null, 2)
console.log('请求成功:', res)
} catch (err: any) {
error.value = err.message || '请求失败'
console.error('请求失败:', err)
//
if (err.message === 'Network Error' || err.code === 'ERR_NETWORK') {
error.value += '\n\n这可能是跨域错误请检查\n1. 开发服务器是否重启\n2. 代理配置是否正确\n3. baseURL 是否使用相对路径'
}
} finally {
loading.value = false
}
}
</script>
<style scoped>
.test-proxy {
padding: 20px;
max-width: 800px;
}
.test-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
}
.result {
margin-top: 20px;
padding: 15px;
background: #f0f9ff;
border: 1px solid #0ea5e9;
border-radius: 4px;
}
.error {
margin-top: 20px;
padding: 15px;
background: #fef2f2;
border: 1px solid #ef4444;
border-radius: 4px;
color: #dc2626;
}
pre {
white-space: pre-wrap;
word-break: break-all;
}
ul {
margin: 10px 0;
padding-left: 20px;
}
li {
margin: 5px 0;
}
</style>

View File

@ -1,4 +1,17 @@
const { defineConfig } = require('@vue/cli-service') const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({ module.exports = defineConfig({
transpileDependencies: true transpileDependencies: true,
devServer: {
port: 8080,
proxy: {
'/brain': {
target: 'http://47.110.148.47:8090',
changeOrigin: true,
ws: true,
pathRewrite: {
'^/brain': '/brain'
}
}
}
}
}) })

159
yarn.lock
View File

@ -1778,11 +1778,38 @@
optionalDependencies: optionalDependencies:
prettier "^1.18.2 || ^2.0.0" prettier "^1.18.2 || ^2.0.0"
"@vue/devtools-api@^6.0.0-beta.11", "@vue/devtools-api@^6.6.4": "@vue/devtools-api@^6.6.4":
version "6.6.4" version "6.6.4"
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz" resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz"
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
"@vue/devtools-api@^7.7.2":
version "7.7.7"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz#5ef5f55f60396220725a273548c0d7ee983d5d34"
integrity sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==
dependencies:
"@vue/devtools-kit" "^7.7.7"
"@vue/devtools-kit@^7.7.7":
version "7.7.7"
resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz#41a64f9526e9363331c72405544df020ce2e3641"
integrity sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==
dependencies:
"@vue/devtools-shared" "^7.7.7"
birpc "^2.3.0"
hookable "^5.5.3"
mitt "^3.0.1"
perfect-debounce "^1.0.0"
speakingurl "^14.0.1"
superjson "^2.2.2"
"@vue/devtools-shared@^7.7.7":
version "7.7.7"
resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz#ff14aa8c1262ebac8c0397d3b09f767cd489750c"
integrity sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==
dependencies:
rfdc "^1.4.1"
"@vue/eslint-config-typescript@^9.1.0": "@vue/eslint-config-typescript@^9.1.0":
version "9.1.0" version "9.1.0"
resolved "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-9.1.0.tgz" resolved "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-9.1.0.tgz"
@ -2243,6 +2270,11 @@ async@^3.2.6:
resolved "https://registry.npmmirror.com/async/-/async-3.2.6.tgz" resolved "https://registry.npmmirror.com/async/-/async-3.2.6.tgz"
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
at-least-node@^1.0.0: at-least-node@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz" resolved "https://registry.npmmirror.com/at-least-node/-/at-least-node-1.0.0.tgz"
@ -2260,6 +2292,15 @@ autoprefixer@^10.2.4:
picocolors "^1.1.1" picocolors "^1.1.1"
postcss-value-parser "^4.2.0" postcss-value-parser "^4.2.0"
axios@^1.12.2:
version "1.12.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.4"
proxy-from-env "^1.1.0"
babel-loader@^8.2.2: babel-loader@^8.2.2:
version "8.4.1" version "8.4.1"
resolved "https://registry.npmmirror.com/babel-loader/-/babel-loader-8.4.1.tgz" resolved "https://registry.npmmirror.com/babel-loader/-/babel-loader-8.4.1.tgz"
@ -2331,6 +2372,11 @@ binary-extensions@^2.0.0:
resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz" resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
birpc@^2.3.0:
version "2.6.1"
resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.6.1.tgz#c73463590928897e80f3263d9fbb7da63515014b"
integrity sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==
bl@^4.1.0: bl@^4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz" resolved "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz"
@ -2638,6 +2684,13 @@ colorette@^2.0.10:
resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz" resolved "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz"
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
commander@7, commander@^7.2.0: commander@7, commander@^7.2.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz" resolved "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz"
@ -2727,6 +2780,13 @@ cookie@0.7.1:
resolved "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz" resolved "https://registry.npmmirror.com/cookie/-/cookie-0.7.1.tgz"
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w== integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
copy-anything@^3.0.2:
version "3.0.5"
resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0"
integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==
dependencies:
is-what "^4.1.8"
copy-webpack-plugin@^9.0.1: copy-webpack-plugin@^9.0.1:
version "9.1.0" version "9.1.0"
resolved "https://registry.npmmirror.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz" resolved "https://registry.npmmirror.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz"
@ -3263,6 +3323,11 @@ delaunator@5:
dependencies: dependencies:
robust-predicates "^3.0.2" robust-predicates "^3.0.2"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
depd@2.0.0: depd@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz" resolved "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz"
@ -3513,6 +3578,16 @@ es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
dependencies: dependencies:
es-errors "^1.3.0" es-errors "^1.3.0"
es-set-tostringtag@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
dependencies:
es-errors "^1.3.0"
get-intrinsic "^1.2.6"
has-tostringtag "^1.0.2"
hasown "^2.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14: es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14:
version "0.10.64" version "0.10.64"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714"
@ -3969,7 +4044,7 @@ flatted@^3.2.9:
resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz" resolved "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz"
integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg== integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
follow-redirects@^1.0.0: follow-redirects@^1.0.0, follow-redirects@^1.15.6:
version "1.15.11" version "1.15.11"
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz" resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz"
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
@ -3993,6 +4068,17 @@ fork-ts-checker-webpack-plugin@^6.4.0:
semver "^7.3.2" semver "^7.3.2"
tapable "^1.0.0" tapable "^1.0.0"
form-data@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
es-set-tostringtag "^2.1.0"
hasown "^2.0.2"
mime-types "^2.1.12"
forwarded@0.2.0: forwarded@0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz" resolved "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz"
@ -4053,7 +4139,7 @@ get-caller-file@^2.0.5:
resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz" resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz" resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
@ -4188,11 +4274,18 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
dependencies: dependencies:
es-define-property "^1.0.0" es-define-property "^1.0.0"
has-symbols@^1.1.0: has-symbols@^1.0.3, has-symbols@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz" resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
dependencies:
has-symbols "^1.0.3"
hash-sum@^1.0.2: hash-sum@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz" resolved "https://registry.npmmirror.com/hash-sum/-/hash-sum-1.0.2.tgz"
@ -4220,6 +4313,11 @@ highlight.js@^10.7.1:
resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz" resolved "https://registry.npmmirror.com/highlight.js/-/highlight.js-10.7.3.tgz"
integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A== integrity sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==
hookable@^5.5.3:
version "5.5.3"
resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.9" version "2.8.9"
resolved "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz" resolved "https://registry.npmmirror.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz"
@ -4549,6 +4647,11 @@ is-url@^1.2.4:
resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52" resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww== integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
is-what@^4.1.8:
version "4.1.16"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==
is-wsl@^2.1.1, is-wsl@^2.2.0: is-wsl@^2.1.1, is-wsl@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz" resolved "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz"
@ -4983,7 +5086,7 @@ mime-match@^1.0.2:
dependencies: dependencies:
wildcard "^1.1.0" wildcard "^1.1.0"
mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35" version "2.1.35"
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz" resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@ -5037,6 +5140,11 @@ minipass@^3.1.1:
dependencies: dependencies:
yallist "^4.0.0" yallist "^4.0.0"
mitt@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
module-alias@^2.2.2: module-alias@^2.2.2:
version "2.2.3" version "2.2.3"
resolved "https://registry.npmmirror.com/module-alias/-/module-alias-2.2.3.tgz" resolved "https://registry.npmmirror.com/module-alias/-/module-alias-2.2.3.tgz"
@ -5428,6 +5536,11 @@ path-type@^4.0.0:
resolved "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz" resolved "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
perfect-debounce@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
picocolors@^0.2.1: picocolors@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-0.2.1.tgz" resolved "https://registry.npmmirror.com/picocolors/-/picocolors-0.2.1.tgz"
@ -5443,6 +5556,13 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz" resolved "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pinia@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/pinia/-/pinia-3.0.3.tgz#f412019bdeb2f45e85927b432803190343e12d89"
integrity sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==
dependencies:
"@vue/devtools-api" "^7.7.2"
pkg-dir@^4.1.0: pkg-dir@^4.1.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz" resolved "https://registry.npmmirror.com/pkg-dir/-/pkg-dir-4.2.0.tgz"
@ -5785,6 +5905,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0" forwarded "0.2.0"
ipaddr.js "1.9.1" ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
pseudomap@^1.0.2: pseudomap@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz" resolved "https://registry.npmmirror.com/pseudomap/-/pseudomap-1.0.2.tgz"
@ -5997,6 +6122,11 @@ reusify@^1.0.4:
resolved "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz" resolved "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz"
integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
rfdc@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rimraf@^3.0.2: rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz" resolved "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz"
@ -6385,6 +6515,11 @@ spdy@^4.0.2:
select-hose "^2.0.0" select-hose "^2.0.0"
spdy-transport "^3.0.0" spdy-transport "^3.0.0"
speakingurl@^14.0.1:
version "14.0.1"
resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
sprintf-js@~1.0.2: sprintf-js@~1.0.2:
version "1.0.3" version "1.0.3"
resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz" resolved "https://registry.npmmirror.com/sprintf-js/-/sprintf-js-1.0.3.tgz"
@ -6495,6 +6630,13 @@ stylehacks@^5.1.1:
browserslist "^4.21.4" browserslist "^4.21.4"
postcss-selector-parser "^6.0.4" postcss-selector-parser "^6.0.4"
superjson@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173"
integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==
dependencies:
copy-anything "^3.0.2"
supports-color@^5.3.0: supports-color@^5.3.0:
version "5.5.0" version "5.5.0"
resolved "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz" resolved "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz"
@ -6870,13 +7012,6 @@ vue@^3.2.13:
"@vue/server-renderer" "3.5.22" "@vue/server-renderer" "3.5.22"
"@vue/shared" "3.5.22" "@vue/shared" "3.5.22"
vuex@^4.0.0:
version "4.1.0"
resolved "https://registry.npmmirror.com/vuex/-/vuex-4.1.0.tgz"
integrity sha512-hmV6UerDrPcgbSy9ORAtNXDr9M4wlNP4pEFKye4ujJF8oqgFFuxDCdOLS3eNoRTtq5O3hoBDh9Doj1bQMYHRbQ==
dependencies:
"@vue/devtools-api" "^6.0.0-beta.11"
watchpack@^2.4.0, watchpack@^2.4.4: watchpack@^2.4.0, watchpack@^2.4.4:
version "2.4.4" version "2.4.4"
resolved "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.4.tgz" resolved "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.4.tgz"