fix: 调整项目架构,封装axios等
This commit is contained in:
parent
79cc312865
commit
82f485385b
12
package.json
12
package.json
|
|
@ -11,13 +11,14 @@
|
|||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^1.0.2",
|
||||
"axios": "^1.12.2",
|
||||
"core-js": "^3.8.3",
|
||||
"d3": "^7.9.0",
|
||||
"echarts": "^6.0.0",
|
||||
"element-plus": "^2.11.4",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.2.13",
|
||||
"vue-router": "^4.0.3",
|
||||
"vuex": "^4.0.0"
|
||||
"vue-router": "^4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
|
|
@ -47,12 +48,15 @@
|
|||
"parserOptions": {
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"rules": {},
|
||||
"rules": {
|
||||
"vue/multi-word-component-names": "off"
|
||||
},
|
||||
"globals": {
|
||||
"defineProps": "readonly",
|
||||
"defineEmits": "readonly",
|
||||
"defineExpose": "readonly",
|
||||
"withDefaults": "readonly"
|
||||
"withDefaults": "readonly",
|
||||
"defineOptions": "readonly"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
@ -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 });
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文档(Word、Excel、PDF 等)
|
||||
* @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 })
|
||||
}
|
||||
|
|
@ -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">
|
||||
支持 PDF、Word、Excel 格式,文件大小不超过 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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
<el-dropdown @command="handleCommand">
|
||||
<span class="user-info">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>管理员</span>
|
||||
<span>{{ userStore.realName || userStore.username || '管理员' }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
|
|
@ -92,6 +92,11 @@
|
|||
<el-icon><Lock /></el-icon>
|
||||
<span>权限管理</span>
|
||||
</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>
|
||||
</aside>
|
||||
|
|
@ -119,12 +124,19 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
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 router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 初始化用户信息
|
||||
onMounted(() => {
|
||||
userStore.initUserInfo()
|
||||
})
|
||||
|
||||
const activeMenu = computed(() => {
|
||||
// 对于子页面,返回父级菜单路径
|
||||
|
|
@ -146,7 +158,8 @@ const breadcrumbConfig: Record<string, { title: string; parent?: string }> = {
|
|||
'/admin/tech-reports': { title: '科技报告', parent: '科技资源' },
|
||||
'/admin/tech-awards': { 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('个人信息功能开发中')
|
||||
break
|
||||
case 'logout':
|
||||
ElMessage.success('退出登录成功')
|
||||
router.push('/login')
|
||||
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
await userStore.handleLogout()
|
||||
router.push('/login')
|
||||
}).catch(() => {
|
||||
// 用户取消
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
// Pinia
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
// 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'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(store).use(router).use(ElementPlus).mount('#app')
|
||||
app.use(pinia).use(router).use(ElementPlus).mount('#app')
|
||||
|
|
|
|||
|
|
@ -5,11 +5,6 @@ import LoginPage from '@/views/LoginPage.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> = [
|
||||
{
|
||||
|
|
@ -27,141 +22,159 @@ const routes: Array<RouteRecordRaw> = [
|
|||
component: AdminLayout,
|
||||
redirect: '/admin/dashboard',
|
||||
children: [
|
||||
// 数据概览
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'admin-dashboard',
|
||||
component: AdminDashboard
|
||||
component: () => import('@/views/admin/dashboard/index.vue')
|
||||
},
|
||||
|
||||
// 内容管理
|
||||
{
|
||||
path: 'news-policy',
|
||||
name: 'admin-news-policy',
|
||||
component: NewsPolicyAdmin
|
||||
component: () => import('@/views/admin/content/news-policy.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-news-policy-edit',
|
||||
component: () => import('@/views/admin/NewsPolicyForm.vue')
|
||||
component: () => import('@/views/admin/content/news-policy-form.vue')
|
||||
},
|
||||
{
|
||||
path: 'smart-qa',
|
||||
name: 'admin-smart-qa',
|
||||
component: SmartQAAdmin
|
||||
component: () => import('@/views/admin/content/smart-qa.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-smart-qa-edit',
|
||||
component: () => import('@/views/admin/SmartQAForm.vue')
|
||||
component: () => import('@/views/admin/content/smart-qa-form.vue')
|
||||
},
|
||||
|
||||
// 科技资源
|
||||
{
|
||||
path: 'tech-resources',
|
||||
name: 'admin-tech-resources',
|
||||
component: TechResourcesAdmin
|
||||
component: () => import('@/views/admin/tech-resources/resources.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-tech-resources-edit',
|
||||
component: () => import('@/views/admin/TechResourcesForm.vue')
|
||||
component: () => import('@/views/admin/tech-resources/resources-form.vue')
|
||||
},
|
||||
{
|
||||
path: 'talent-profile',
|
||||
name: 'admin-talent-profile',
|
||||
component: TalentProfileAdmin
|
||||
component: () => import('@/views/admin/tech-resources/talent-profile.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-talent-profile-edit',
|
||||
component: () => import('@/views/admin/TalentProfileForm.vue')
|
||||
component: () => import('@/views/admin/tech-resources/talent-profile-form.vue')
|
||||
},
|
||||
{
|
||||
path: 'tech-projects',
|
||||
name: 'admin-tech-projects',
|
||||
component: () => import('@/views/admin/TechProjectsAdmin.vue')
|
||||
component: () => import('@/views/admin/tech-resources/projects.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-tech-projects-edit',
|
||||
component: () => import('@/views/admin/TechProjectsForm.vue')
|
||||
component: () => import('@/views/admin/tech-resources/projects-form.vue')
|
||||
},
|
||||
{
|
||||
path: 'tech-achievements',
|
||||
name: 'admin-tech-achievements',
|
||||
component: () => import('@/views/admin/TechAchievementsAdmin.vue')
|
||||
component: () => import('@/views/admin/tech-resources/achievements.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-tech-achievements-edit',
|
||||
component: () => import('@/views/admin/TechAchievementsForm.vue')
|
||||
component: () => import('@/views/admin/tech-resources/achievements-form.vue')
|
||||
},
|
||||
{
|
||||
path: 'tech-reports',
|
||||
name: 'admin-tech-reports',
|
||||
component: () => import('@/views/admin/TechReportsAdmin.vue')
|
||||
component: () => import('@/views/admin/tech-resources/reports.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-tech-reports-edit',
|
||||
component: () => import('@/views/admin/TechReportsForm.vue')
|
||||
component: () => import('@/views/admin/tech-resources/reports-form.vue')
|
||||
},
|
||||
{
|
||||
path: 'tech-awards',
|
||||
name: 'admin-tech-awards',
|
||||
component: () => import('@/views/admin/TechAwardsAdmin.vue')
|
||||
component: () => import('@/views/admin/tech-resources/awards.vue')
|
||||
},
|
||||
{
|
||||
path: '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',
|
||||
name: 'admin-tech-awards-edit',
|
||||
component: () => import('@/views/admin/TechAwardsForm.vue')
|
||||
component: () => import('@/views/admin/tech-resources/awards-form.vue')
|
||||
},
|
||||
|
||||
// 系统管理
|
||||
{
|
||||
path: 'users',
|
||||
name: 'admin-users',
|
||||
component: () => import('@/views/admin/UsersAdmin.vue')
|
||||
component: () => import('@/views/admin/system/users.vue')
|
||||
},
|
||||
{
|
||||
path: '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
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
import { createStore } from 'vuex'
|
||||
|
||||
export default createStore({
|
||||
state: {
|
||||
},
|
||||
getters: {
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
actions: {
|
||||
},
|
||||
modules: {
|
||||
}
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -2,8 +2,8 @@
|
|||
<div class="login-container">
|
||||
<div class="login-box">
|
||||
<div class="login-header">
|
||||
<h1>科技大脑·后台管理系统</h1>
|
||||
<p>请登录您的账户</p>
|
||||
<h1>科技管理系统</h1>
|
||||
<p>欢迎登录后台管理系统</p>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
|
|
@ -11,7 +11,7 @@
|
|||
:model="loginForm"
|
||||
:rules="loginRules"
|
||||
class="login-form"
|
||||
@submit.prevent="handleLogin"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<el-form-item prop="username">
|
||||
<el-input
|
||||
|
|
@ -30,7 +30,6 @@
|
|||
size="large"
|
||||
prefix-icon="Lock"
|
||||
show-password
|
||||
@keyup.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
|
|
@ -38,18 +37,14 @@
|
|||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="login-button"
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
class="login-button"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="login-tips">
|
||||
<p>默认账户:admin / 123456</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -57,117 +52,157 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { login, getUserInfo, type LoginParams } from '@/api/auth'
|
||||
|
||||
defineOptions({
|
||||
name: 'LoginPage'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
const loading = ref(false)
|
||||
|
||||
const loginForm = reactive({
|
||||
// 登录表单数据
|
||||
const loginForm = reactive<LoginParams>({
|
||||
username: '',
|
||||
password: ''
|
||||
password: '',
|
||||
loginType: 2 // 固定为2
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const loginRules: FormRules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '用户名长度在 2 到 20 个字符', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ 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) {
|
||||
loading.value = true
|
||||
try {
|
||||
console.log('开始登录...', loginForm)
|
||||
|
||||
// 模拟登录请求
|
||||
setTimeout(() => {
|
||||
if (loginForm.username === 'admin' && loginForm.password === '123456') {
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/admin')
|
||||
} else {
|
||||
ElMessage.error('用户名或密码错误')
|
||||
// 调用登录接口
|
||||
const loginRes = await login(loginForm)
|
||||
console.log('登录响应:', loginRes)
|
||||
|
||||
// 保存 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
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 测试用的默认账号(可以删除)
|
||||
const setTestAccount = () => {
|
||||
loginForm.username = 'admin'
|
||||
loginForm.password = '123456'
|
||||
}
|
||||
|
||||
// 开发环境下自动填充测试账号
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// setTestAccount() // 取消注释可自动填充测试账号
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.login-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-box {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
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 {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
color: #303133;
|
||||
font-size: 24px;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login-form .el-form-item {
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.login-tips {
|
||||
text-align: center;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #f0f2f5;
|
||||
}
|
||||
|
||||
.login-tips p {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.login-box {
|
||||
padding: 30px 20px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 20px;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -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` - 权限管理
|
||||
|
|
@ -169,6 +169,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'NewsPolicyForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -85,7 +85,10 @@
|
|||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'NewsPolicyAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
|
|
@ -118,6 +118,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'SmartQAForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -74,6 +74,10 @@ import { ref, reactive, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'SmartQAAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface QAItem {
|
||||
|
|
@ -113,6 +113,10 @@
|
|||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import * as echarts from 'echarts'
|
||||
|
||||
defineOptions({
|
||||
name: 'AdminDashboard'
|
||||
})
|
||||
|
||||
interface Activity {
|
||||
id: string
|
||||
text: string
|
||||
|
|
@ -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>
|
||||
|
|
@ -87,6 +87,10 @@
|
|||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'PermissionsAdmin'
|
||||
})
|
||||
|
||||
interface RoleItem {
|
||||
id: string
|
||||
roleName: string
|
||||
|
|
@ -166,10 +170,12 @@ const handleAdd = () => {
|
|||
|
||||
const handleEdit = (row: RoleItem) => {
|
||||
ElMessage.info('编辑功能开发中')
|
||||
console.log(row)
|
||||
}
|
||||
|
||||
const handlePermissions = (row: RoleItem) => {
|
||||
ElMessage.info('权限配置功能开发中')
|
||||
console.log(row)
|
||||
}
|
||||
|
||||
const handleDelete = (row: RoleItem) => {
|
||||
|
|
@ -89,6 +89,10 @@
|
|||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'UsersAdmin'
|
||||
})
|
||||
|
||||
interface UserItem {
|
||||
id: string
|
||||
username: string
|
||||
|
|
@ -168,6 +172,7 @@ const handleAdd = () => {
|
|||
|
||||
const handleEdit = (row: UserItem) => {
|
||||
ElMessage.info('编辑功能开发中')
|
||||
console.log(row)
|
||||
}
|
||||
|
||||
const handleToggleStatus = (row: UserItem) => {
|
||||
|
|
@ -187,6 +187,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechAchievementsForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -77,6 +77,10 @@ import { ref, reactive, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechAchievementsAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface AchievementItem {
|
||||
|
|
@ -170,6 +170,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechAwardsForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -95,6 +95,10 @@ import { ref, reactive, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechAwardsAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface AwardItem {
|
||||
|
|
@ -181,6 +181,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechProjectsForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -83,6 +83,10 @@ import { ref, reactive, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechProjectsAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface ProjectItem {
|
||||
|
|
@ -243,6 +243,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechReportsForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -77,6 +77,10 @@ import { ref, reactive, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechReportsAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface ReportItem {
|
||||
|
|
@ -111,6 +111,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechResourcesForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -76,6 +76,10 @@ import { ref, reactive, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'TechResourcesAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface ResourceItem {
|
||||
|
|
@ -252,6 +252,10 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
defineOptions({
|
||||
name: 'TalentProfileForm'
|
||||
})
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import RichTextEditor from '@/components/RichTextEditor.vue'
|
||||
|
|
@ -77,6 +77,10 @@ import { ref, reactive, onMounted } from 'vue'
|
|||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'TalentProfileAdmin'
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface TalentItem {
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,4 +1,17 @@
|
|||
const { defineConfig } = require('@vue/cli-service')
|
||||
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
159
yarn.lock
|
|
@ -1778,11 +1778,38 @@
|
|||
optionalDependencies:
|
||||
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"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz"
|
||||
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":
|
||||
version "9.1.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "8.4.1"
|
||||
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"
|
||||
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:
|
||||
version "4.1.0"
|
||||
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"
|
||||
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:
|
||||
version "7.2.0"
|
||||
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"
|
||||
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:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.npmmirror.com/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz"
|
||||
|
|
@ -3263,6 +3323,11 @@ delaunator@5:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.0.0"
|
||||
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:
|
||||
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:
|
||||
version "0.10.64"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz"
|
||||
integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
|
||||
|
|
@ -3993,6 +4068,17 @@ fork-ts-checker-webpack-plugin@^6.4.0:
|
|||
semver "^7.3.2"
|
||||
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:
|
||||
version "0.2.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
|
|
@ -4188,11 +4274,18 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "2.8.9"
|
||||
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"
|
||||
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:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.npmmirror.com/is-wsl/-/is-wsl-2.2.0.tgz"
|
||||
|
|
@ -4983,7 +5086,7 @@ mime-match@^1.0.2:
|
|||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
|
|
@ -5037,6 +5140,11 @@ minipass@^3.1.1:
|
|||
dependencies:
|
||||
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:
|
||||
version "2.2.3"
|
||||
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"
|
||||
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:
|
||||
version "0.2.1"
|
||||
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"
|
||||
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:
|
||||
version "4.2.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.2"
|
||||
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"
|
||||
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:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz"
|
||||
|
|
@ -6385,6 +6515,11 @@ spdy@^4.0.2:
|
|||
select-hose "^2.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:
|
||||
version "1.0.3"
|
||||
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"
|
||||
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:
|
||||
version "5.5.0"
|
||||
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/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:
|
||||
version "2.4.4"
|
||||
resolved "https://registry.npmmirror.com/watchpack/-/watchpack-2.4.4.tgz"
|
||||
|
|
|
|||
Loading…
Reference in New Issue