feat: 监控端

This commit is contained in:
zouzhiwen 2026-04-16 11:56:10 +08:00
parent 6838d454b0
commit d4b629cc25
13 changed files with 147 additions and 37 deletions

View File

@ -1,2 +1,3 @@
# 开发环境 — 留空,走 vite.config.ts 中的 proxy
VITE_API_BASE=
VITE_ENTRY_OPTIONS=ops,monitor

View File

@ -1,2 +1,3 @@
# 生产环境
VITE_API_BASE=https://api.baoshi56.com
VITE_ENTRY_OPTIONS=ops,monitor

View File

@ -1,2 +1,3 @@
# 测试环境SIT
VITE_API_BASE=https://sit-api.baoshi56.com
VITE_API_BASE=https://api.baoshi56.com
VITE_ENTRY_OPTIONS=monitor

9
.gitignore vendored
View File

@ -13362,4 +13362,11 @@ node_modules/vue-tsc/LICENSE
node_modules/vue-tsc/package.json
node_modules/vue-tsc/README.md
node_modules/vue-tsc/bin/vue-tsc.js
/dist/*
/dist/*
/node_modules/fsevents/fsevents.d.ts
/node_modules/fsevents/fsevents.js
/node_modules/fsevents/fsevents.node
/node_modules/fsevents/LICENSE
/node_modules/fsevents/package.json
/node_modules/fsevents/README.md
/node_modules/*

View File

@ -11,12 +11,12 @@
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"pinia": "^2.3.0",
"@element-plus/icons-vue": "^2.3.1",
"axios": "^1.7.9",
"element-plus": "^2.9.1",
"@element-plus/icons-vue": "^2.3.1"
"pinia": "^2.3.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.2.1",

View File

@ -73,6 +73,7 @@ export interface ChainQueryParams {
type?: string
businessNo?: string
sourceContainer?: string
targetCode?: string
startTime?: string
endTime?: string
}

View File

@ -15,7 +15,7 @@
active-text-color="#ffffff"
class="aside-menu"
>
<el-menu-item index="/dashboard">
<el-menu-item v-if="!isMonitorMode" index="/dashboard">
<el-icon><Monitor /></el-icon>
<template #title>首页</template>
</el-menu-item>
@ -25,13 +25,13 @@
</el-menu-item>
<el-menu-item index="/elevator">
<el-icon><SetUp /></el-icon>
<template #title>提升机管理</template>
<template #title>提升机查看</template>
</el-menu-item>
<el-menu-item index="/vendor/load">
<el-menu-item v-if="!isMonitorMode" index="/vendor/load">
<el-icon><DataLine /></el-icon>
<template #title>厂商负载</template>
</el-menu-item>
<el-menu-item index="/generate">
<el-menu-item v-if="!isMonitorMode" index="/generate">
<el-icon><Plus /></el-icon>
<template #title>任务生成</template>
</el-menu-item>
@ -191,7 +191,14 @@ function refreshCurrentTab() {
refreshKeys.value[route.path] = (refreshKeys.value[route.path] ?? 0) + 1
}
const keepAliveNames = ['DashboardView', 'TaskChainList', 'TaskChainDetail', 'ElevatorList', 'VendorLoadView', 'TaskGenerateView']
const userStore = useUserStore()
const isMonitorMode = computed(() => userStore.isMonitorMode())
const keepAliveNames = computed(() => {
if (isMonitorMode.value) {
return ['TaskChainList', 'TaskChainDetail', 'ElevatorList']
}
return ['DashboardView', 'TaskChainList', 'TaskChainDetail', 'ElevatorList', 'VendorLoadView', 'TaskGenerateView']
})
// watch watch immediate storage
const activePath = tabsStore.restoreFromStorage()
@ -304,16 +311,15 @@ onMounted(() => {
onUnmounted(() => {
document.removeEventListener('click', hideTabMenu)
})
const userStore = useUserStore()
const isCollapse = ref(false)
const tabs = [
{ path: '/dashboard', label: '首页', icon: Monitor },
const tabs = computed(() => [
...(!isMonitorMode.value ? [{ path: '/dashboard', label: '首页', icon: Monitor }] : []),
{ path: '/task/chain', label: '任务链', icon: List },
{ path: '/elevator', label: '提升机', icon: SetUp },
{ path: '/vendor/load', label: '厂商负载', icon: DataLine },
{ path: '/generate', label: '生成', icon: Plus },
]
...(!isMonitorMode.value ? [{ path: '/vendor/load', label: '厂商负载', icon: DataLine }] : []),
...(!isMonitorMode.value ? [{ path: '/generate', label: '生成', icon: Plus }] : []),
])
const activeMenu = computed(() => {
const path = route.path

View File

@ -1,4 +1,12 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
const monitorBlockedPaths = ['/dashboard', '/vendor/load', '/generate']
function getHomePath() {
const userStore = useUserStore()
return userStore.isMonitorMode() ? '/task/chain' : '/dashboard'
}
const router = createRouter({
history: createWebHashHistory(),
@ -12,7 +20,7 @@ const router = createRouter({
{
path: '/',
component: () => import('@/layouts/AdminLayout.vue'),
redirect: '/dashboard',
redirect: () => getHomePath(),
children: [
{
path: 'dashboard',
@ -56,11 +64,14 @@ const router = createRouter({
})
router.beforeEach((to, _from, next) => {
const userStore = useUserStore()
const token = localStorage.getItem('token')
if (!to.meta.public && !token) {
next('/login')
} else if (to.path === '/login' && token) {
next('/')
next(getHomePath())
} else if (!to.meta.public && userStore.isMonitorMode() && monitorBlockedPaths.includes(to.path)) {
next('/task/chain')
} else {
next()
}

View File

@ -3,6 +3,8 @@ import { ref } from 'vue'
import { login as loginApi } from '@/api/auth'
import router from '@/router'
export type EntryMode = 'ops' | 'monitor'
export interface UserInfo {
id: number
name: string
@ -12,6 +14,7 @@ export interface UserInfo {
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const user = ref<UserInfo | null>(parseStoredUser())
const entryMode = ref<EntryMode>(parseStoredEntryMode())
function parseStoredUser(): UserInfo | null {
try {
@ -22,6 +25,11 @@ export const useUserStore = defineStore('user', () => {
}
}
function parseStoredEntryMode(): EntryMode {
const stored = localStorage.getItem('entryMode')
return stored === 'monitor' ? 'monitor' : 'ops'
}
async function login(username: string, password: string) {
const res = await loginApi(username, password)
const data = res.data
@ -35,6 +43,15 @@ export const useUserStore = defineStore('user', () => {
localStorage.setItem('user', JSON.stringify(user.value))
}
function setEntryMode(mode: EntryMode) {
entryMode.value = mode
localStorage.setItem('entryMode', mode)
}
function isMonitorMode(): boolean {
return entryMode.value === 'monitor'
}
function logout() {
token.value = ''
user.value = null
@ -47,5 +64,5 @@ export const useUserStore = defineStore('user', () => {
return !!token.value
}
return { token, user, login, logout, isLoggedIn }
return { token, user, entryMode, login, logout, isLoggedIn, setEntryMode, isMonitorMode }
})

View File

@ -5,7 +5,7 @@
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>提升机管理</span>
<span>提升机查看</span>
<el-button type="primary" size="small" :loading="refreshing" @click="loadData">
刷新
</el-button>
@ -23,7 +23,7 @@
{{ elevator.enable ? '已启用' : '已禁用' }}
</el-tag>
</div>
<div class="elevator-actions">
<div v-if="!isMonitorMode" class="elevator-actions">
<el-button
v-if="elevator.enable"
type="warning" size="small" plain
@ -120,7 +120,7 @@
<!-- ========== 移动端 ========== -->
<template v-else>
<div class="m-elevator-header">
<span class="m-elevator-title">提升机管理</span>
<span class="m-elevator-title">提升机查看</span>
<el-button type="primary" size="small" round :loading="refreshing" @click="loadData">刷新</el-button>
</div>
@ -198,7 +198,7 @@
</div>
</div>
<div class="m-elevator-card-actions">
<div v-if="!isMonitorMode" class="m-elevator-card-actions">
<el-button
v-if="elevator.enable"
type="warning" size="small" plain round
@ -231,11 +231,14 @@ import {
type ElevatorVO,
} from '@/api/elevator'
import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useDevice } from '@/composables/useDevice'
import { useUserStore } from '@/stores/user'
const { isMobile } = useDevice()
const userStore = useUserStore()
const isMonitorMode = computed(() => userStore.isMonitorMode())
const router = useRouter()
const elevators = ref<ElevatorVO[]>([])
const refreshing = ref(false)

View File

@ -30,6 +30,20 @@
:prefix-icon="Lock"
/>
</el-form-item>
<el-form-item>
<div class="entry-mode-wrap">
<span class="entry-mode-label">进入端口</span>
<el-radio-group v-model="entryMode">
<el-radio-button
v-for="item in availableEntryModes"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</el-radio-button>
</el-radio-group>
</div>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@ -66,9 +80,10 @@
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import type { EntryMode } from '@/stores/user'
import { Lock, User } from '@element-plus/icons-vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { reactive, ref } from 'vue'
import { computed, reactive, ref, watchEffect } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
@ -88,6 +103,23 @@ const rules: FormRules = {
const tokenDialogVisible = ref(false)
const manualToken = ref('')
const allEntryModes: Array<{ value: EntryMode; label: string }> = [
{ value: 'ops', label: '运维端' },
{ value: 'monitor', label: '监控端' },
]
const envEntryModes = (import.meta.env.VITE_ENTRY_OPTIONS as string | undefined) || 'ops,monitor'
const parsedEntryModes = envEntryModes.split(',').map((item) => item.trim()).filter((item) => item === 'ops' || item === 'monitor') as EntryMode[]
const availableEntryModes = computed(() => {
const modeSet = new Set(parsedEntryModes.length ? parsedEntryModes : ['ops', 'monitor'])
return allEntryModes.filter((item) => modeSet.has(item.value))
})
const entryMode = ref<EntryMode>(userStore.entryMode)
watchEffect(() => {
if (!availableEntryModes.value.some((item) => item.value === entryMode.value)) {
entryMode.value = availableEntryModes.value[0]?.value ?? 'ops'
}
})
function handleSetToken() {
const token = manualToken.value.trim()
@ -95,6 +127,7 @@ function handleSetToken() {
ElMessage.warning('Token 不能为空')
return
}
userStore.setEntryMode(entryMode.value)
localStorage.setItem('token', token)
tokenDialogVisible.value = false
ElMessage.success('Token 已设置')
@ -107,6 +140,7 @@ async function handleLogin() {
loading.value = true
try {
userStore.setEntryMode(entryMode.value)
await userStore.login(form.username, form.password)
ElMessage.success('登录成功')
router.push('/')
@ -179,4 +213,15 @@ async function handleLogin() {
width: 100%;
font-size: 13px;
}
.entry-mode-wrap {
width: 100%;
}
.entry-mode-label {
display: block;
margin-bottom: 8px;
font-size: 13px;
color: #606266;
}
</style>

View File

@ -18,7 +18,7 @@
{{ chainStatusMap[chain.status] || chain.status }}
</el-tag>
</div>
<div class="card-header-actions">
<div v-if="!isMonitorMode" class="card-header-actions">
<el-button
v-if="canCancel(chain.status)"
type="danger" size="small" :loading="canceling"
@ -94,14 +94,14 @@
{{ taskStatusMap[task.status] || task.status }}
</el-tag>
<el-button
v-if="canMockCallback(task)"
v-if="!isMonitorMode && canMockCallback(task)"
text type="success" size="small"
@click="openMockCallback(task)"
>模拟回调</el-button>
<el-button text type="danger" size="small" @click="handleDeleteTask(task)">
<el-button v-if="!isMonitorMode" text type="danger" size="small" @click="handleDeleteTask(task)">
删除
</el-button>
<el-dropdown trigger="click" @command="(cmd: string) => handleForceStatus(task, cmd)">
<el-dropdown v-if="!isMonitorMode" trigger="click" @command="(cmd: string) => handleForceStatus(task, cmd)">
<el-button text type="primary" size="small">变更状态</el-button>
<template #dropdown>
<el-dropdown-menu>
@ -172,7 +172,7 @@
<span class="m-chain-type">{{ typeMap[chain.type] || chain.type }}</span>
</div>
<div class="m-chain-code-mobile">{{ chain.code }}</div>
<div class="m-status-actions">
<div v-if="!isMonitorMode" class="m-status-actions">
<el-button
v-if="canCancel(chain.status)"
type="danger" size="small" round :loading="canceling"
@ -282,7 +282,7 @@
<el-tag size="small" type="info">{{ task.type }}</el-tag>
<el-tag size="small" type="info">{{ task.vendor }}</el-tag>
</div>
<el-icon class="m-task-more" @click="openTaskActions(task)"><MoreFilled /></el-icon>
<el-icon v-if="!isMonitorMode" class="m-task-more" @click="openTaskActions(task)"><MoreFilled /></el-icon>
</div>
<div class="m-task-code">{{ task.code }}</div>
@ -320,6 +320,7 @@
<!-- 移动端子任务操作面板 -->
<el-drawer
v-if="!isMonitorMode"
v-model="taskActionDrawerVisible"
direction="btt"
size="auto"
@ -351,6 +352,7 @@
<!-- 移动端变更状态选择 -->
<el-drawer
v-if="!isMonitorMode"
v-model="statusPickerVisible"
direction="btt"
size="auto"
@ -442,11 +444,12 @@
<script setup lang="ts">
defineOptions({ name: 'TaskChainDetail' })
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, MoreFilled, VideoPlay, Switch, Delete } from '@element-plus/icons-vue'
import { useDevice } from '@/composables/useDevice'
import { useUserStore } from '@/stores/user'
import { chainStatusMap, getChainStatusTag } from '@/constants/chainStatus'
import { taskStatusMap, getTaskStatusTag } from '@/constants/taskStatus'
import {
@ -456,6 +459,8 @@ import {
} from '@/api/task'
const { isMobile } = useDevice()
const userStore = useUserStore()
const isMonitorMode = computed(() => userStore.isMonitorMode())
const route = useRoute()
const loading = ref(false)
const executing = ref(false)

View File

@ -23,6 +23,9 @@
<el-form-item label="源容器">
<el-input v-model="query.sourceContainer" placeholder="请输入" clearable style="width: 150px" />
</el-form-item>
<el-form-item label="目标编码">
<el-input v-model="query.targetCode" placeholder="请输入" clearable style="width: 150px" />
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
@ -46,7 +49,7 @@
<template #header>
<div class="card-header">
<span>任务链列表</span>
<div>
<div v-if="!isMonitorMode">
<el-button type="success" size="small" @click="handleScan">手动扫描执行</el-button>
</div>
</div>
@ -97,7 +100,7 @@
<template #default="{ row }">
<el-button text type="primary" size="small" @click="goDetail(row.code)">详情</el-button>
<el-button
v-if="row.status === 'CREATED' || row.status === 'READY'"
v-if="!isMonitorMode && (row.status === 'CREATED' || row.status === 'READY')"
text type="warning" size="small"
@click="handleExecute(row)"
>
@ -139,7 +142,7 @@
</div>
<!-- 手动扫描执行 -->
<el-button type="success" size="small" class="m-scan-btn" @click="handleScan">
<el-button v-if="!isMonitorMode" type="success" size="small" class="m-scan-btn" @click="handleScan">
手动扫描执行
</el-button>
@ -201,7 +204,7 @@
<span class="m-chain-time">{{ row.createTime }}</span>
<div class="m-chain-actions" @click.stop>
<el-button
v-if="row.status === 'CREATED' || row.status === 'READY'"
v-if="!isMonitorMode && (row.status === 'CREATED' || row.status === 'READY')"
type="warning"
size="small"
@click="handleExecute(row)"
@ -261,6 +264,9 @@
<el-form-item label="源容器">
<el-input v-model="query.sourceContainer" placeholder="请输入" clearable />
</el-form-item>
<el-form-item label="目标编码">
<el-input v-model="query.targetCode" placeholder="请输入" clearable />
</el-form-item>
</el-form>
<div class="m-drawer-footer">
<el-button @click="handleReset">重置</el-button>
@ -280,8 +286,11 @@ import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useDevice } from '@/composables/useDevice'
import { chainStatusMap, getChainStatusTag } from '@/constants/chainStatus'
import { useUserStore } from '@/stores/user'
const { isMobile } = useDevice()
const userStore = useUserStore()
const isMonitorMode = computed(() => userStore.isMonitorMode())
const router = useRouter()
const tableRef = ref<TableInstance>()
const loading = ref(false)
@ -350,6 +359,7 @@ const query = reactive({
type: '',
businessNo: '',
sourceContainer: '',
targetCode: '',
})
const activeFilterCount = computed(() => {
@ -357,6 +367,7 @@ const activeFilterCount = computed(() => {
if (query.type) n++
if (query.businessNo) n++
if (query.sourceContainer) n++
if (query.targetCode) n++
if (dateRange.value?.length) n++
return n
})
@ -424,6 +435,7 @@ function handleReset() {
query.type = ''
query.businessNo = ''
query.sourceContainer = ''
query.targetCode = ''
dateRange.value = []
handleSearch()
}