前端优化与配置更新:\n- 统一 .btn--primary 样式为全局,移除各视图重true\n- 新增 roles 常量与标签映射,路由/导航/侧边栏/页面统一引用\n- 删除未使用的 fetch 封装,统一使用 axios 客户端\n- 执行日志表格抽为 ExecLogsTable 组件,降低视图复杂度\n- 认证逻辑收紧:仅后端返回 token 视为登录,恢复需同时存在 token\n- 开发环境固定端口 5173,HMR 指向 localhost:5173\n- 移除 defineProps 多余导入,消除编译提示

pull/46/head
hnu202326010131 2 months ago
parent a7cdeccae5
commit 2f6fe59a08

@ -1 +1 @@
VITE_API_TARGET=https://ellis-investigations-harley-think.trycloudflare.com
VITE_API_TARGET=https://zqpdbakhjwcq.sealoshzh.site

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -1,162 +0,0 @@
// node_modules/@vue/devtools-api/lib/esm/env.js
function getDevtoolsGlobalHook() {
return getTarget().__VUE_DEVTOOLS_GLOBAL_HOOK__;
}
function getTarget() {
return typeof navigator !== "undefined" && typeof window !== "undefined" ? window : typeof globalThis !== "undefined" ? globalThis : {};
}
var isProxyAvailable = typeof Proxy === "function";
// node_modules/@vue/devtools-api/lib/esm/const.js
var HOOK_SETUP = "devtools-plugin:setup";
var HOOK_PLUGIN_SETTINGS_SET = "plugin:settings:set";
// node_modules/@vue/devtools-api/lib/esm/time.js
var supported;
var perf;
function isPerformanceSupported() {
var _a;
if (supported !== void 0) {
return supported;
}
if (typeof window !== "undefined" && window.performance) {
supported = true;
perf = window.performance;
} else if (typeof globalThis !== "undefined" && ((_a = globalThis.perf_hooks) === null || _a === void 0 ? void 0 : _a.performance)) {
supported = true;
perf = globalThis.perf_hooks.performance;
} else {
supported = false;
}
return supported;
}
function now() {
return isPerformanceSupported() ? perf.now() : Date.now();
}
// node_modules/@vue/devtools-api/lib/esm/proxy.js
var ApiProxy = class {
constructor(plugin, hook) {
this.target = null;
this.targetQueue = [];
this.onQueue = [];
this.plugin = plugin;
this.hook = hook;
const defaultSettings = {};
if (plugin.settings) {
for (const id in plugin.settings) {
const item = plugin.settings[id];
defaultSettings[id] = item.defaultValue;
}
}
const localSettingsSaveId = `__vue-devtools-plugin-settings__${plugin.id}`;
let currentSettings = Object.assign({}, defaultSettings);
try {
const raw = localStorage.getItem(localSettingsSaveId);
const data = JSON.parse(raw);
Object.assign(currentSettings, data);
} catch (e) {
}
this.fallbacks = {
getSettings() {
return currentSettings;
},
setSettings(value) {
try {
localStorage.setItem(localSettingsSaveId, JSON.stringify(value));
} catch (e) {
}
currentSettings = value;
},
now() {
return now();
}
};
if (hook) {
hook.on(HOOK_PLUGIN_SETTINGS_SET, (pluginId, value) => {
if (pluginId === this.plugin.id) {
this.fallbacks.setSettings(value);
}
});
}
this.proxiedOn = new Proxy({}, {
get: (_target, prop) => {
if (this.target) {
return this.target.on[prop];
} else {
return (...args) => {
this.onQueue.push({
method: prop,
args
});
};
}
}
});
this.proxiedTarget = new Proxy({}, {
get: (_target, prop) => {
if (this.target) {
return this.target[prop];
} else if (prop === "on") {
return this.proxiedOn;
} else if (Object.keys(this.fallbacks).includes(prop)) {
return (...args) => {
this.targetQueue.push({
method: prop,
args,
resolve: () => {
}
});
return this.fallbacks[prop](...args);
};
} else {
return (...args) => {
return new Promise((resolve) => {
this.targetQueue.push({
method: prop,
args,
resolve
});
});
};
}
}
});
}
async setRealTarget(target) {
this.target = target;
for (const item of this.onQueue) {
this.target.on[item.method](...item.args);
}
for (const item of this.targetQueue) {
item.resolve(await this.target[item.method](...item.args));
}
}
};
// node_modules/@vue/devtools-api/lib/esm/index.js
function setupDevtoolsPlugin(pluginDescriptor, setupFn) {
const descriptor = pluginDescriptor;
const target = getTarget();
const hook = getDevtoolsGlobalHook();
const enableProxy = isProxyAvailable && descriptor.enableEarlyProxy;
if (hook && (target.__VUE_DEVTOOLS_PLUGIN_API_AVAILABLE__ || !enableProxy)) {
hook.emit(HOOK_SETUP, pluginDescriptor, setupFn);
} else {
const proxy = enableProxy ? new ApiProxy(descriptor, hook) : null;
const list = target.__VUE_DEVTOOLS_PLUGINS__ = target.__VUE_DEVTOOLS_PLUGINS__ || [];
list.push({
pluginDescriptor: descriptor,
setupFn,
proxy
});
if (proxy) {
setupFn(proxy.proxiedTarget);
}
}
}
export {
setupDevtoolsPlugin
};
//# sourceMappingURL=chunk-AYVSL3LM.js.map

File diff suppressed because one or more lines are too long

@ -1,10 +0,0 @@
var __defProp = Object.defineProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
export {
__export
};
//# sourceMappingURL=chunk-PZ5AY32C.js.map

@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

@ -1,348 +0,0 @@
import {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBaseVNode,
createBlock,
createCommentVNode,
createElementBlock,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
} from "./chunk-5J33X7OV.js";
import "./chunk-PZ5AY32C.js";
export {
BaseTransition,
BaseTransitionPropsValidators,
Comment,
DeprecationTypes,
EffectScope,
ErrorCodes,
ErrorTypeStrings,
Fragment,
KeepAlive,
ReactiveEffect,
Static,
Suspense,
Teleport,
Text,
TrackOpTypes,
Transition,
TransitionGroup,
TriggerOpTypes,
VueElement,
assertNumber,
callWithAsyncErrorHandling,
callWithErrorHandling,
camelize,
capitalize,
cloneVNode,
compatUtils,
compile,
computed,
createApp,
createBlock,
createCommentVNode,
createElementBlock,
createBaseVNode as createElementVNode,
createHydrationRenderer,
createPropsRestProxy,
createRenderer,
createSSRApp,
createSlots,
createStaticVNode,
createTextVNode,
createVNode,
customRef,
defineAsyncComponent,
defineComponent,
defineCustomElement,
defineEmits,
defineExpose,
defineModel,
defineOptions,
defineProps,
defineSSRCustomElement,
defineSlots,
devtools,
effect,
effectScope,
getCurrentInstance,
getCurrentScope,
getCurrentWatcher,
getTransitionRawChildren,
guardReactiveProps,
h,
handleError,
hasInjectionContext,
hydrate,
hydrateOnIdle,
hydrateOnInteraction,
hydrateOnMediaQuery,
hydrateOnVisible,
initCustomFormatter,
initDirectivesForSSR,
inject,
isMemoSame,
isProxy,
isReactive,
isReadonly,
isRef,
isRuntimeOnly,
isShallow,
isVNode,
markRaw,
mergeDefaults,
mergeModels,
mergeProps,
nextTick,
nodeOps,
normalizeClass,
normalizeProps,
normalizeStyle,
onActivated,
onBeforeMount,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onErrorCaptured,
onMounted,
onRenderTracked,
onRenderTriggered,
onScopeDispose,
onServerPrefetch,
onUnmounted,
onUpdated,
onWatcherCleanup,
openBlock,
patchProp,
popScopeId,
provide,
proxyRefs,
pushScopeId,
queuePostFlushCb,
reactive,
readonly,
ref,
registerRuntimeCompiler,
render,
renderList,
renderSlot,
resolveComponent,
resolveDirective,
resolveDynamicComponent,
resolveFilter,
resolveTransitionHooks,
setBlockTracking,
setDevtoolsHook,
setTransitionHooks,
shallowReactive,
shallowReadonly,
shallowRef,
ssrContextKey,
ssrUtils,
stop,
toDisplayString,
toHandlerKey,
toHandlers,
toRaw,
toRef,
toRefs,
toValue,
transformVNodeArgs,
triggerRef,
unref,
useAttrs,
useCssModule,
useCssVars,
useHost,
useId,
useModel,
useSSRContext,
useShadowRoot,
useSlots,
useTemplateRef,
useTransitionState,
vModelCheckbox,
vModelDynamic,
vModelRadio,
vModelSelect,
vModelText,
vShow,
version,
warn,
watch,
watchEffect,
watchPostEffect,
watchSyncEffect,
withAsyncContext,
withCtx,
withDefaults,
withDirectives,
withKeys,
withMemo,
withModifiers,
withScopeId
};
//# sourceMappingURL=vue.js.map

@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

@ -37,6 +37,8 @@ body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial
.sidebar__link { padding: 8px 10px; border-radius: 6px }
.sidebar__link--active { background: #f1f5f9 }
.btn { padding: 8px 12px; border: 1px solid #e5e7eb; border-radius: 6px }
.btn--primary { background: #2563eb; color: #fff; border-color: #2563eb }
.btn--primary:disabled { opacity: 0.6; cursor: not-allowed }
.u-hidden { display: none }
.dashboard__table { width: 100%; border-collapse: collapse }
.dashboard__table th, .dashboard__table td { border-bottom: 1px solid #e5e7eb; padding: 8px 10px; text-align: left }

@ -0,0 +1,44 @@
<template>
<table class="dashboard__table">
<thead>
<tr>
<th>执行ID</th>
<th>故障ID</th>
<th>命令类型</th>
<th>状态</th>
<th>开始时间</th>
<th>结束时间</th>
<th>退出码</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="r in records" :key="r.id" class="dashboard__table-row" :class="{ 'row--selected': selectedId===r.id }" @click="$emit('select', r)">
<td>{{ r.id }}</td>
<td>{{ r.faultId }}</td>
<td>{{ r.cmdType }}</td>
<td><span :class="statusClass(r.status)">{{ r.status }}</span></td>
<td>{{ r.start }}</td>
<td>{{ r.end || '-' }}</td>
<td>{{ r.code ?? '-' }}</td>
<td>
<button class="btn u-text-sm" type="button" @click.stop="$emit('edit', r)">编辑</button>
<button class="btn u-text-sm u-ml-1" type="button" @click.stop="$emit('delete', r.id)">删除</button>
</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
type RecordItem = { id:string; faultId:string; cmdType:string; status:'running'|'success'|'failed'; start:string; end:string|''; code:number|null }
defineProps<{ records: RecordItem[]; selectedId: string }>()
function statusClass(s:'running'|'success'|'failed'){ return s==='running'?'status--running': s==='success'?'status--success':'status--failed' }
</script>
<style scoped>
.row--selected{ background:#eef2ff }
.status--running{ color:#2563eb }
.status--success{ color:#16a34a }
.status--failed{ color:#dc2626 }
</style>

@ -5,9 +5,9 @@
<nav class="header__nav" role="navigation" aria-label="">
<RouterLink class="header__nav-item" :class="{ 'header__nav-item--active': isActive('/cluster-list') }" to="/cluster-list">集群列表</RouterLink>
<RouterLink class="header__nav-item" :class="{ 'header__nav-item--active': isActive('/logs') }" to="/logs">日志查询</RouterLink>
<RouterLink v-if="can(['admin','operator'])" class="header__nav-item" :class="{ 'header__nav-item--active': isActive('/diagnosis') }" to="/diagnosis"></RouterLink>
<RouterLink v-if="can([Roles.admin, Roles.operator])" class="header__nav-item" :class="{ 'header__nav-item--active': isActive('/diagnosis') }" to="/diagnosis"></RouterLink>
<RouterLink class="header__nav-item" :class="{ 'header__nav-item--active': isActive('/exec-logs') }" to="/exec-logs">执行日志</RouterLink>
<div class="header__dropdown" v-if="can(['admin','operator'])">
<div class="header__dropdown" v-if="can([Roles.admin, Roles.operator])">
<button class="header__nav-item header__dropdown-trigger" type="button" aria-haspopup="true" :aria-expanded="configOpen ? 'true' : 'false'" @click.stop.prevent="toggleConfig">
系统配置
<i class="fas fa-chevron-down header__dropdown-icon" :class="{ 'icon-rot': configOpen }" aria-hidden="true"></i>
@ -45,6 +45,7 @@ import { RouterLink } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useAuthStore } from '../stores/auth'
import { useUIStore } from '../stores/ui'
import { Roles } from '../constants/roles'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()

@ -15,11 +15,12 @@ import { useRoute, RouterLink } from 'vue-router'
import { computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useAuthStore } from '../stores/auth'
import { Roles } from '../constants/roles'
const route = useRoute()
const auth = useAuthStore()
const { role } = storeToRefs(auth)
function isActive(p: string) { return route.path === p }
const isAdmin = computed(() => role.value === 'admin')
const isAdmin = computed(() => role.value === Roles.admin)
</script>
<style scoped>

@ -0,0 +1,15 @@
export const Roles = {
admin: 'admin',
operator: 'operator',
observer: 'observer'
} as const
export type Role = typeof Roles[keyof typeof Roles]
export const AllRoles: Role[] = [Roles.admin, Roles.operator, Roles.observer]
export const RoleLabel: Record<Role, string> = {
admin: '管理员',
operator: '操作员',
observer: '观察员'
}

@ -1,22 +1,23 @@
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { Roles, AllRoles } from '../constants/roles'
const routes: RouteRecordRaw[] = [
{ path: '/', redirect: '/cluster-list' },
{ path: '/login', name: 'login', component: () => import('../views/Login.vue'), meta: { requiresAuth: false } },
{ path: '/register', name: 'register', component: () => import('../views/Register.vue'), meta: { requiresAuth: false } },
{ path: '/cluster-list', name: 'cluster-list', component: () => import('../views/ClusterList.vue'), meta: { requiresAuth: true, roles: ['admin','operator','observer'] } },
{ path: '/dashboard', name: 'dashboard', component: () => import('../views/Dashboard.vue'), meta: { requiresAuth: true, roles: ['admin','operator','observer'] } },
{ path: '/logs', name: 'logs', component: () => import('../views/Logs.vue'), meta: { requiresAuth: true, roles: ['admin','operator','observer'] } },
{ path: '/diagnosis', name: 'diagnosis', component: () => import('../views/Diagnosis.vue'), meta: { requiresAuth: true, roles: ['admin','operator'] } },
{ path: '/exec-logs', name: 'exec-logs', component: () => import('../views/ExecLogs.vue'), meta: { requiresAuth: true, roles: ['admin','operator','observer'] } },
{ path: '/alert-config', name: 'alert-config', component: () => import('../views/AlertConfig.vue'), meta: { requiresAuth: true, roles: ['admin','operator'] } },
{ path: '/profile', name: 'profile', component: () => import('../views/Profile.vue'), meta: { requiresAuth: true, roles: ['admin','operator','observer'] } },
{ path: '/account', name: 'account', component: () => import('../views/Account.vue'), meta: { requiresAuth: true, roles: ['admin','operator','observer'] } },
{ path: '/user-management', name: 'user-management', component: () => import('../views/UserManagement.vue'), meta: { requiresAuth: true, roles: ['admin'] } },
{ path: '/role-assignment', name: 'role-assignment', component: () => import('../views/RoleAssignment.vue'), meta: { requiresAuth: true, roles: ['admin'] } },
{ path: '/permission-policy', name: 'permission-policy', component: () => import('../views/PermissionPolicy.vue'), meta: { requiresAuth: true, roles: ['admin'] } },
{ path: '/audit-logs', name: 'audit-logs', component: () => import('../views/AuditLogs.vue'), meta: { requiresAuth: true, roles: ['admin'] } }
{ path: '/cluster-list', name: 'cluster-list', component: () => import('../views/ClusterList.vue'), meta: { requiresAuth: true, roles: AllRoles } },
{ path: '/dashboard', name: 'dashboard', component: () => import('../views/Dashboard.vue'), meta: { requiresAuth: true, roles: AllRoles } },
{ path: '/logs', name: 'logs', component: () => import('../views/Logs.vue'), meta: { requiresAuth: true, roles: AllRoles } },
{ path: '/diagnosis', name: 'diagnosis', component: () => import('../views/Diagnosis.vue'), meta: { requiresAuth: true, roles: [Roles.admin, Roles.operator] } },
{ path: '/exec-logs', name: 'exec-logs', component: () => import('../views/ExecLogs.vue'), meta: { requiresAuth: true, roles: AllRoles } },
{ path: '/alert-config', name: 'alert-config', component: () => import('../views/AlertConfig.vue'), meta: { requiresAuth: true, roles: [Roles.admin, Roles.operator] } },
{ path: '/profile', name: 'profile', component: () => import('../views/Profile.vue'), meta: { requiresAuth: true, roles: AllRoles } },
{ path: '/account', name: 'account', component: () => import('../views/Account.vue'), meta: { requiresAuth: true, roles: AllRoles } },
{ path: '/user-management', name: 'user-management', component: () => import('../views/UserManagement.vue'), meta: { requiresAuth: true, roles: [Roles.admin] } },
{ path: '/role-assignment', name: 'role-assignment', component: () => import('../views/RoleAssignment.vue'), meta: { requiresAuth: true, roles: [Roles.admin] } },
{ path: '/permission-policy', name: 'permission-policy', component: () => import('../views/PermissionPolicy.vue'), meta: { requiresAuth: true, roles: [Roles.admin] } },
{ path: '/audit-logs', name: 'audit-logs', component: () => import('../views/AuditLogs.vue'), meta: { requiresAuth: true, roles: [Roles.admin] } }
]
const router = createRouter({ history: createWebHashHistory(), routes })

@ -1,14 +0,0 @@
export type RequestInitEx = { method?: string; headers?: Record<string,string>; body?: any; timeout?: number; token?: string }
export async function request(url: string, init: RequestInitEx = {}) {
const controller = new AbortController()
const id = setTimeout(() => controller.abort(), init.timeout || 10000)
const headers: Record<string,string> = { 'Content-Type': 'application/json', ...(init.headers || {}) }
if (init.token) headers['Authorization'] = 'Bearer ' + init.token
const res = await fetch(url, { method: init.method || 'GET', headers, body: init.body ? JSON.stringify(init.body) : undefined, signal: controller.signal })
clearTimeout(id)
let data: unknown = undefined
try { data = await res.json() } catch {}
if (!res.ok) throw { status: res.status, data }
return data as any
}

@ -6,7 +6,7 @@ type User = { username: string; role: 'admin'|'operator'|'observer' }
export const useAuthStore = defineStore('auth', {
state: () => ({ user: null as User|null, token: null as string|null }),
getters: {
isAuthenticated: (s) => !!s.user,
isAuthenticated: (s) => !!(s.user && s.token),
role: (s) => s.user?.role || null,
defaultPage: (s) => {
const r = s.user?.role
@ -20,8 +20,15 @@ export const useAuthStore = defineStore('auth', {
restore() {
const rawUser = localStorage.getItem('cm_user')
const rawToken = localStorage.getItem('cm_token')
if (rawUser) this.user = JSON.parse(rawUser)
if (rawToken) this.token = rawToken
if (rawUser && rawToken) {
this.user = JSON.parse(rawUser)
this.token = rawToken
} else {
this.user = null
this.token = null
localStorage.removeItem('cm_user')
localStorage.removeItem('cm_token')
}
},
persist() {
if (this.user) localStorage.setItem('cm_user', JSON.stringify(this.user))
@ -33,23 +40,19 @@ export const useAuthStore = defineStore('auth', {
try {
const r = await api.post('/v1/user/login', { username, password })
const role = username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : username === 'obs' ? 'observer' : 'observer'
const token = r?.data?.token
if (!token) {
return { ok: false, message: '登录失败' }
}
this.user = { username, role }
this.token = r?.data?.token || null
this.token = token
this.persist()
return { ok: true, role }
} catch (e: any) {
const d = e?.response?.data
if (!e?.response) {
const demo = { admin: 'admin123', ops: 'ops123', obs: 'obs123' } as const
const pass = demo[username as keyof typeof demo]
if (pass && password === pass) {
const role = username === 'admin' ? 'admin' : username === 'ops' ? 'operator' : 'observer'
this.user = { username, role }
this.token = null
this.persist()
return { ok: true, role }
}
return { ok: false, message: '网络异常,后端不可用' }
}
const d = e?.response?.data
const message = d?.detail === 'invalid_credentials' ? '账号或密码错误' : d?.detail === 'inactive_user' ? '账号未激活' : '登录失败'
return { ok: false, message }
}

@ -53,5 +53,4 @@ function save(){
.layout__card-body{ padding:16px }
.layout__grid{ display:grid; gap:16px }
.layout__grid--3{ grid-template-columns: 1fr 1fr 1fr }
.btn--primary{ background:#2563eb; color:#fff; border-color:#2563eb }
</style>

@ -113,7 +113,6 @@ function edit(r:any){ open.value=true; form.name=r.name; form.cond=r.cond; form.
.layout__grid{ display:grid; gap:16px }
.layout__grid--3{ grid-template-columns: 1fr 1fr 1fr }
.form-grid{ display:grid; grid-template-columns: repeat(4, 1fr); gap:12px }
.btn--primary{ background:#2563eb; color:#fff; border-color:#2563eb }
.level--info{ color:#2563eb }
.level--warn{ color:#f59e0b }
.level--error{ color:#dc2626 }

@ -183,6 +183,4 @@ const previewLogs = computed(() => {
.chat-progress{ display:flex; align-items:center; gap:8px; margin-top:8px; color:#6b7280 }
.progress-bar{ flex:1; height:6px; background:#e5e7eb; border-radius:999px; overflow:hidden }
.progress-fill{ height:100%; background:#2563eb }
.btn--primary{ background:#2563eb; color:#fff; border-color:#2563eb }
.btn--primary:disabled{ opacity:0.6; cursor:not-allowed }
</style>

@ -16,35 +16,7 @@
<article class="layout__card">
<div class="layout__card-header"><h3 class="layout__card-title">执行记录</h3></div>
<div class="layout__card-body u-p-0">
<table class="dashboard__table">
<thead>
<tr>
<th>执行ID</th>
<th>故障ID</th>
<th>命令类型</th>
<th>状态</th>
<th>开始时间</th>
<th>结束时间</th>
<th>退出码</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="r in records" :key="r.id" class="dashboard__table-row" :class="{ 'row--selected': selected===r.id }" @click="select(r)">
<td>{{ r.id }}</td>
<td>{{ r.faultId }}</td>
<td>{{ r.cmdType }}</td>
<td><span :class="statusClass(r.status)">{{ r.status }}</span></td>
<td>{{ r.start }}</td>
<td>{{ r.end || '-' }}</td>
<td>{{ r.code ?? '-' }}</td>
<td>
<button class="btn u-text-sm" type="button" @click.stop="editRow(r)">编辑</button>
<button class="btn u-text-sm u-ml-1" type="button" @click.stop="del(r.id)">删除</button>
</td>
</tr>
</tbody>
</table>
<ExecLogsTable :records="records" :selected-id="selected" @select="select" @edit="editRow" @delete="del" />
</div>
</article>
@ -76,6 +48,7 @@
import { reactive, ref, onMounted } from 'vue'
import api from '../lib/api'
import { useAuthStore } from '../stores/auth'
import ExecLogsTable from '../components/ExecLogsTable.vue'
type RecordItem = { id:string; faultId:string; cmdType:string; status:'running'|'success'|'failed'; start:string; end:string|''; code:number|null }
const auth = useAuthStore()
const records = reactive<RecordItem[]>([])
@ -161,7 +134,6 @@ onMounted(()=>{ load() })
.exec-header{ display:flex; justify-content:space-between; align-items:center }
.layout__page-subtitle{ color:#6b7280; font-size:13px }
.header-actions{ display:flex; align-items:center }
.btn--primary{ background:#2563eb; color:#fff; border-color:#2563eb }
.row--selected{ background:#eef2ff }
.layout__card{ background:#ffffff; border:1px solid #e5e7eb; border-radius:12px; box-shadow:0 8px 24px rgba(16,24,40,0.06) }
.layout__card-header{ padding:12px 16px; border-bottom:1px solid #e5e7eb }

@ -154,8 +154,6 @@ const summary = computed(() => {
.layout__card-body { padding: 16px }
.layout__grid { display: grid; gap: 16px }
.layout__grid--3 { grid-template-columns: 1fr 1fr 1fr }
.btn--primary { background: #2563eb; color: #fff; border-color: #2563eb }
.btn--primary:disabled { opacity: 0.6; cursor: not-allowed }
.btn-link { background: transparent; border-color: transparent; color: #2563eb }
.filter-actions { display: flex; justify-content: flex-end; align-items: center; }
</style>

@ -34,6 +34,7 @@ import { ref, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useAuthStore } from '../stores/auth'
import api from '../lib/api'
import { RoleLabel } from '../constants/roles'
const auth = useAuthStore()
const { user, token } = storeToRefs(auth)
const username = ref('admin')
@ -47,13 +48,13 @@ onMounted(async () => {
username.value = name
email.value = u?.email || `${name}@example.com`
const roleKey = u?.role || user.value?.role || 'admin'
roleName.value = roleKey==='admin'?'管理员': roleKey==='operator'?'操作员':'观察员'
roleName.value = RoleLabel[roleKey as keyof typeof RoleLabel] || '观察员'
}catch(e:any){
const name = user.value?.username || 'admin'
username.value = name
email.value = `${name}@example.com`
const roleKey = user.value?.role || 'admin'
roleName.value = roleKey==='admin'?'管理员': roleKey==='operator'?'操作员':'观察员'
roleName.value = RoleLabel[roleKey as keyof typeof RoleLabel] || '观察员'
}
})
</script>

@ -53,6 +53,7 @@
import { ref, onMounted, computed } from 'vue'
import api from '../lib/api'
import { useAuthStore } from '../stores/auth'
import { RoleLabel, Roles } from '../constants/roles'
const auth = useAuthStore()
const users = ref<string[]>([])
const user = ref('')
@ -60,7 +61,7 @@ const role = ref('operator')
const msg = ref('')
const loading = ref(false)
const msgClass = computed(() => (msg.value ? (msg.value.startsWith('成功') ? 'u-text-green-600' : 'u-text-error') : 'u-text-gray-600'))
const roleLabel = computed(() => (role.value === 'admin' ? '管理员' : role.value === 'operator' ? '操作员' : role.value === 'observer' ? '观察员' : '未选择'))
const roleLabel = computed(() => RoleLabel[(role.value || '') as keyof typeof RoleLabel] || '未选择')
onMounted(async () => {
try {
const r = await api.get('/v1/users', { headers: auth.token ? { Authorization: `Bearer ${auth.token}` } : undefined })
@ -100,8 +101,6 @@ async function onSave() {
.layout__card-body { padding: 16px }
.layout__grid { display: grid; gap: 16px }
.layout__grid--2 { grid-template-columns: 1fr 1fr }
.btn--primary { background: #2563eb; color: #fff; border-color: #2563eb }
.btn--primary:disabled { opacity: 0.6; cursor: not-allowed }
.u-text-error { color: #dc2626 }
.u-text-green-600 { color: #16a34a }
.u-text-gray-600 { color: #4b5563 }

@ -5,9 +5,9 @@ export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
const target = env.VITE_API_TARGET || "http://localhost:8000";
const devHost = env.VITE_DEV_HOST || "0.0.0.0";
const devPort = Number(env.VITE_DEV_PORT || 5173);
const hmrHost = env.VITE_HMR_HOST || devHost;
const hmrPort = Number(env.VITE_HMR_PORT || devPort);
const devPort = 5173;
const hmrHost = "localhost";
const hmrPort = 5173;
const allowedHostsEnv = (env.VITE_ALLOWED_HOSTS || "").split(",").map((s) => s.trim()).filter(Boolean);
return {
plugins: [vue()],

Loading…
Cancel
Save