提交
This commit is contained in:
@@ -26,4 +26,3 @@ echo ============================================
|
||||
echo packup success!
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
|
||||
@@ -63,10 +63,12 @@ export interface ShareInfo {
|
||||
* @param file 文件对象
|
||||
* @param folderId 目标文件夹ID, 0=根目录
|
||||
*/
|
||||
export function uploadFile(file: File, folderId: string = '0') {
|
||||
export function uploadFile(file: File, folderId: string = '0', bizId?: string) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request.post<any, { data: FileInfo }>(`/file/upload?folder_id=${folderId}`, formData, {
|
||||
let url = `/file/upload?folder_id=${folderId}`
|
||||
if (bizId) url += `&biz_id=${encodeURIComponent(bizId)}`
|
||||
return request.post<any, { data: FileInfo }>(url, formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,19 +7,23 @@ import { ElMessage } from 'element-plus'
|
||||
import router from '../router'
|
||||
import { getConfig } from '../config'
|
||||
|
||||
// 创建 axios 实例
|
||||
const cfg = getConfig()
|
||||
// 创建 axios 实例(延迟读取 config, 确保 loadConfig 已完成)
|
||||
const request = axios.create({
|
||||
baseURL: cfg.apiTarget ? `${cfg.apiTarget}/api` : '/api',
|
||||
baseURL: '/api',
|
||||
timeout: 30000,
|
||||
})
|
||||
|
||||
/**
|
||||
* 请求拦截器
|
||||
* 自动注入 JWT Token 到请求头
|
||||
* 1. 动态设置 baseURL(支持运行时配置)
|
||||
* 2. 自动注入 JWT Token 到请求头
|
||||
*/
|
||||
request.interceptors.request.use(
|
||||
(config) => {
|
||||
const cfg = getConfig()
|
||||
if (cfg.apiTarget) {
|
||||
config.baseURL = `${cfg.apiTarget}/api`
|
||||
}
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.token = token
|
||||
@@ -50,15 +54,22 @@ request.interceptors.response.use(
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
const { status, data } = error.response
|
||||
const url = error.config?.url || ''
|
||||
|
||||
switch (status) {
|
||||
case 401: {
|
||||
// 只有之前有Token(已登录过)才提示"已过期"
|
||||
// 登录接口的401直接显示错误信息(如密码错误)
|
||||
if (url.includes('/auth/login') || url.includes('/auth/app-login')) {
|
||||
ElMessage.error(data?.message || '用户名或密码错误')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
// 其他接口的401: token过期, 跳转登录页
|
||||
const hadToken = !!localStorage.getItem('token')
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('nickname')
|
||||
localStorage.removeItem('role')
|
||||
localStorage.removeItem('roles')
|
||||
if (hadToken) {
|
||||
ElMessage.error('登录已过期, 请重新登录')
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* 后台: 管理员系统管理
|
||||
*/
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { isMobile } from '../utils/device'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
@@ -12,31 +14,41 @@ const router = createRouter({
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Login.vue'),
|
||||
component: () => isMobile()
|
||||
? import('../views/MobileLogin.vue')
|
||||
: import('../views/Login.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'FileManager',
|
||||
component: () => import('../views/FileManager.vue'),
|
||||
component: () => isMobile()
|
||||
? import('../views/MobileFileManager.vue')
|
||||
: import('../views/FileManager.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/s/:code',
|
||||
name: 'ShareView',
|
||||
component: () => import('../views/ShareView.vue'),
|
||||
component: () => isMobile()
|
||||
? import('../views/MobileShareView.vue')
|
||||
: import('../views/ShareView.vue'),
|
||||
meta: { requiresAuth: false },
|
||||
},
|
||||
{
|
||||
path: '/trash',
|
||||
name: 'TrashView',
|
||||
component: () => import('../views/TrashView.vue'),
|
||||
component: () => isMobile()
|
||||
? import('../views/MobileTrash.vue')
|
||||
: import('../views/TrashView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/shares',
|
||||
name: 'SharesView',
|
||||
component: () => import('../views/SharesView.vue'),
|
||||
component: () => isMobile()
|
||||
? import('../views/MobileShares.vue')
|
||||
: import('../views/SharesView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
|
||||
@@ -59,6 +71,7 @@ const router = createRouter({
|
||||
{ path: 'system-log', name: 'AdminSystemLog', component: () => import('../views/admin/SystemLog.vue') },
|
||||
{ path: 'blacklist', name: 'AdminBlacklist', component: () => import('../views/admin/IPBlacklist.vue') },
|
||||
{ path: 'backup', name: 'AdminBackup', component: () => import('../views/admin/BackupManage.vue') },
|
||||
{ path: 'license', name: 'AdminLicense', component: () => import('../views/admin/LicenseInfo.vue') },
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -66,11 +79,27 @@ const router = createRouter({
|
||||
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const token = localStorage.getItem('token')
|
||||
const role = localStorage.getItem('role')
|
||||
|
||||
if (to.meta.requiresAuth && !token) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && token) {
|
||||
next('/')
|
||||
} else if (to.meta.requiresAdmin) {
|
||||
// 管理员判断: localStorage role 为 admin, 或者已加载的角色列表含 admin
|
||||
const isAdmin = role === 'admin' ||
|
||||
(() => {
|
||||
try {
|
||||
const roles = JSON.parse(localStorage.getItem('roles') || '[]')
|
||||
return roles.some((r: any) => r.name === 'admin')
|
||||
} catch { return false }
|
||||
})()
|
||||
if (!isAdmin) {
|
||||
ElMessage.error('需要管理员权限')
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ export const useUserStore = defineStore('user', () => {
|
||||
const data: UserPermissionVO = res.data
|
||||
roles.value = data.roles || []
|
||||
permissions.value = data.permissions || []
|
||||
// 缓存角色到 localStorage, 供路由守卫快速判断
|
||||
localStorage.setItem('roles', JSON.stringify(roles.value))
|
||||
} catch (error) {
|
||||
console.error('加载权限失败:', error)
|
||||
}
|
||||
@@ -104,6 +106,7 @@ export const useUserStore = defineStore('user', () => {
|
||||
localStorage.removeItem('username')
|
||||
localStorage.removeItem('nickname')
|
||||
localStorage.removeItem('role')
|
||||
localStorage.removeItem('roles')
|
||||
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
@@ -195,12 +195,19 @@
|
||||
|
||||
<!-- 上传对话框 -->
|
||||
<el-dialog v-model="showUploadDialog" title="上传文件" width="520" :close-on-click-modal="false">
|
||||
<el-upload drag :auto-upload="false" :file-list="uploadFiles" :on-change="handleUploadChange"
|
||||
:on-remove="handleUploadRemove" multiple>
|
||||
<el-icon class="el-icon--upload" :size="48"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">拖拽文件到此处,或 <em>点击选择</em></div>
|
||||
<template #tip><div class="el-upload__tip">支持批量上传,单文件最大 200MB</div></template>
|
||||
</el-upload>
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="业务编号">
|
||||
<el-input v-model="uploadBizId" placeholder="留空则不设置" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="文件">
|
||||
<el-upload drag :auto-upload="false" :file-list="uploadFiles" :on-change="handleUploadChange"
|
||||
:on-remove="handleUploadRemove" multiple style="width:100%">
|
||||
<el-icon class="el-icon--upload" :size="48"><UploadFilled /></el-icon>
|
||||
<div class="el-upload__text">拖拽文件到此处,或 <em>点击选择</em></div>
|
||||
<template #tip><div class="el-upload__tip">支持批量上传,单文件最大 200MB</div></template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showUploadDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="uploading" @click="handleUpload">
|
||||
@@ -308,6 +315,7 @@
|
||||
<div class="info-name" :title="infoData.name">{{ infoData.name }}</div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="ID">{{ infoData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="业务ID">{{ infoData.biz_id || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建者">{{ infoData.owner_name || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型" v-if="!infoData.isFolder">
|
||||
{{ infoData.extension ? infoData.extension.toUpperCase() + ' 文件' : '-' }}
|
||||
@@ -358,6 +366,7 @@ import {
|
||||
getStorageInfo, initChunkUpload, uploadChunk, confirmChunk, mergeChunks,
|
||||
type FileInfo, type ShareInfo, type StorageInfo, type ChunkInitResult,
|
||||
} from '../api/file'
|
||||
import { getConfig } from '../config'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const fileStore = useFileStore()
|
||||
@@ -374,6 +383,7 @@ const showUploadDialog = ref(false)
|
||||
const uploadFiles = ref<UploadFile[]>([])
|
||||
const uploading = ref(false)
|
||||
const uploadProgress = ref(0)
|
||||
const uploadBizId = ref('')
|
||||
|
||||
const showCreateFolderDialog = ref(false)
|
||||
const newFolderName = ref('')
|
||||
@@ -474,7 +484,9 @@ function handleRowDblClick(row: any) { row.isFolder ? handleEnterFolder(row) : h
|
||||
async function handleDownload(file: FileInfo) {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const res = await fetch(`/api/file/${file.id}/download`, {
|
||||
const cfg = getConfig()
|
||||
const base = cfg.apiTarget ? `${cfg.apiTarget}/api` : '/api'
|
||||
const res = await fetch(`${base}/file/${file.id}/download`, {
|
||||
headers: { token }
|
||||
})
|
||||
if (!res.ok) {
|
||||
@@ -542,7 +554,7 @@ async function handleUpload() {
|
||||
await chunkedUpload(file, fileStore.currentFolderId)
|
||||
} else {
|
||||
// 小文件: 普通上传
|
||||
await uploadFile(file, fileStore.currentFolderId)
|
||||
await uploadFile(file, fileStore.currentFolderId, uploadBizId.value || undefined)
|
||||
}
|
||||
ok++
|
||||
} catch (e: any) {
|
||||
@@ -554,6 +566,7 @@ async function handleUpload() {
|
||||
uploading.value = false
|
||||
showUploadDialog.value = false
|
||||
uploadFiles.value = []
|
||||
uploadBizId.value = ''
|
||||
if (ok > 0) { ElMessage.success(`成功上传 ${ok} 个文件`); fileStore.refresh() }
|
||||
}
|
||||
|
||||
@@ -691,8 +704,7 @@ async function handleCreateShare() {
|
||||
|
||||
function copyShareUrl() {
|
||||
if (shareResult.value) {
|
||||
navigator.clipboard.writeText(shareResult.value.share_url)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
copyText(shareResult.value.share_url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,6 +734,23 @@ async function showInfo(row: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function copyText(text: string) {
|
||||
// 兼容 HTTP 环境(clipboard API 需要 HTTPS)
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => ElMessage.success('已复制'))
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
ElMessage.success('已复制')
|
||||
}
|
||||
}
|
||||
|
||||
async function handleChangePassword() {
|
||||
const f = passwordForm.value
|
||||
if (!f.old_password) { ElMessage.warning('请输入当前密码'); return }
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
<router-link to="/admin/backup" class="nav-item" active-class="active">
|
||||
<el-icon><Download /></el-icon><span>备份管理</span>
|
||||
</router-link>
|
||||
<router-link to="/admin/license" class="nav-item" active-class="active">
|
||||
<el-icon><Key /></el-icon><span>产品授权</span>
|
||||
</router-link>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
@@ -86,7 +89,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import {
|
||||
Setting, DataAnalysis, User, Lock, OfficeBuilding, Box, Connection, Back, Link, Notebook, WarningFilled, Monitor,
|
||||
Setting, DataAnalysis, User, Lock, OfficeBuilding, Box, Connection, Back, Link, Notebook, WarningFilled, Monitor, Key,
|
||||
} from '@element-plus/icons-vue'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
|
||||
@@ -105,6 +108,7 @@ const titleMap: Record<string, string> = {
|
||||
'/admin/system-log': '系统日志',
|
||||
'/admin/blacklist': 'IP黑名单',
|
||||
'/admin/backup': '备份管理',
|
||||
'/admin/license': '产品授权',
|
||||
}
|
||||
|
||||
const currentTitle = computed(() => titleMap[route.path] || '管理后台')
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
<el-table-column prop="app_id" label="AppID" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<el-text type="primary" style="font-family: monospace">{{ row.app_id }}</el-text>
|
||||
<el-button link type="primary" size="small" style="margin-left:4px" @click="copyText(row.app_id)">
|
||||
<el-icon><CopyDocument /></el-icon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="授权用户" min-width="200">
|
||||
@@ -163,6 +166,7 @@ String token = AesUtil.encrypt(plaintext, appSecret); // AES-CBC</pre>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { CopyDocument } from '@element-plus/icons-vue'
|
||||
import request from '../../api/request'
|
||||
|
||||
interface AppUser { user_id: number; username: string; nickname: string }
|
||||
@@ -230,10 +234,25 @@ async function handleCreate() {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function copyText(text: string) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(text).then(() => ElMessage.success('已复制'))
|
||||
} else {
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = text
|
||||
ta.style.position = 'fixed'
|
||||
ta.style.left = '-9999px'
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
ElMessage.success('已复制')
|
||||
}
|
||||
}
|
||||
|
||||
function copySecret() {
|
||||
if (createdApp.value?.app_secret) {
|
||||
navigator.clipboard.writeText(createdApp.value.app_secret)
|
||||
ElMessage.success('已复制')
|
||||
copyText(createdApp.value.app_secret)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user