feat: initialize independent Sense product skeleton (#21)
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<section class="page-container">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<h1>工作台</h1>
|
||||
<p>查看设备、视频接入和服务状态,按提示处理异常。</p>
|
||||
</div>
|
||||
<el-tag type="success" effect="light">Sense 独立运行</el-tag>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col v-for="item in overview" :key="item.label" :xs="24" :sm="12" :lg="6">
|
||||
<el-card shadow="never" class="overview-card">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
<small>{{ item.description }}</small>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card shadow="never" class="next-step-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>开始使用</span>
|
||||
<el-tag type="info">按步骤配置</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<el-steps :active="0" finish-status="success" simple>
|
||||
<el-step title="添加设备" />
|
||||
<el-step title="检查视频" />
|
||||
<el-step title="配置区域" />
|
||||
</el-steps>
|
||||
<el-empty description="功能模块会按工单逐步启用,不显示框架演示页面。" :image-size="92" />
|
||||
</el-card>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const overview = [
|
||||
{ label: '设备总数', value: '—', description: '添加设备后显示' },
|
||||
{ label: '视频正常', value: '—', description: '接入验证后显示' },
|
||||
{ label: '需要处理', value: '—', description: '异常状态集中提示' },
|
||||
{ label: '区域配置', value: '—', description: '发布后显示版本' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overview-card { margin-bottom: 16px; }
|
||||
.overview-card :deep(.el-card__body) { display: flex; flex-direction: column; gap: 8px; }
|
||||
.overview-card strong { color: #1d2129; font-size: 28px; }
|
||||
.overview-card small { color: #86909c; }
|
||||
.next-step-card { margin-top: 4px; }
|
||||
.card-header { display: flex; align-items: center; justify-content: space-between; }
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>Sense 管理端需要启用 JavaScript。</noscript>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import './theme.css'
|
||||
|
||||
const app = createApp(App)
|
||||
Object.entries(ElementPlusIconsVue).forEach(([name, component]) => {
|
||||
app.component(name, component)
|
||||
})
|
||||
app.use(ElementPlus)
|
||||
app.use(store)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api/v1',
|
||||
timeout: 15000,
|
||||
withCredentials: true,
|
||||
headers: { 'X-Product': 'sense' }
|
||||
})
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => Promise.reject(error.response?.data?.error || error)
|
||||
)
|
||||
|
||||
export default request
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
|
||||
import AppLayout from '../layout/AppLayout.vue'
|
||||
import Dashboard from './Dashboard.vue'
|
||||
|
||||
const featureContext = require.context('../', true, /router\/modules\/sense-.*\.js$/)
|
||||
const featureRoutes = featureContext.keys().flatMap((key) => {
|
||||
const module = featureContext(key).default
|
||||
return Array.isArray(module) ? module : [module]
|
||||
})
|
||||
|
||||
const publicRoutes = featureRoutes.filter((route) => route.meta?.public)
|
||||
const protectedRoutes = featureRoutes.filter((route) => !route.meta?.public)
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
...publicRoutes,
|
||||
{
|
||||
path: '/',
|
||||
component: AppLayout,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'sense-dashboard',
|
||||
component: Dashboard,
|
||||
meta: { title: '工作台', icon: 'Odometer', order: 0 }
|
||||
},
|
||||
...protectedRoutes
|
||||
]
|
||||
},
|
||||
{ path: '/:pathMatch(.*)*', redirect: '/' }
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to) => {
|
||||
if (to.meta.public) return true
|
||||
const session = window.sessionStorage.getItem('sense_session')
|
||||
const hasIdentityModule = router.hasRoute('sense-login')
|
||||
if (hasIdentityModule && !session) return { name: 'sense-login', query: { redirect: to.fullPath } }
|
||||
return true
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { createStore } from 'vuex'
|
||||
|
||||
const moduleContext = require.context('../', true, /store\/modules\/sense-.*\.js$/)
|
||||
const modules = {}
|
||||
moduleContext.keys().forEach((key) => {
|
||||
const name = key.split('/').pop().replace('.js', '')
|
||||
modules[name] = moduleContext(key).default
|
||||
})
|
||||
|
||||
export default createStore({ modules })
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
:root {
|
||||
--sense-bg: #f2f3f5;
|
||||
--sense-sidebar: #001529;
|
||||
--sense-primary: #1890ff;
|
||||
--sense-text: #1d2129;
|
||||
--sense-muted: #86909c;
|
||||
--sense-border: #e5e6eb;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #app { min-width: 1080px; min-height: 100%; margin: 0; }
|
||||
body {
|
||||
background: var(--sense-bg);
|
||||
color: var(--sense-text);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
.page-container { padding: 20px; }
|
||||
.page-heading { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
|
||||
.page-heading h1 { margin: 0 0 6px; font-size: 22px; font-weight: 600; }
|
||||
.page-heading p { margin: 0; color: var(--sense-muted); font-size: 14px; }
|
||||
.search-toolbar { margin-bottom: 16px; padding: 16px 16px 0; background: #fff; border: 1px solid var(--sense-border); border-radius: 4px; }
|
||||
.table-container { padding: 16px; background: #fff; border: 1px solid var(--sense-border); border-radius: 4px; }
|
||||
.table-actions { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
|
||||
.pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<el-container class="app-shell">
|
||||
<el-aside :width="collapsed ? '64px' : '208px'" class="app-sidebar">
|
||||
<div class="brand" :class="{ collapsed }">
|
||||
<span class="brand-mark">S</span>
|
||||
<span v-if="!collapsed" class="brand-name">Sense</span>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="$route.path"
|
||||
router
|
||||
background-color="#001529"
|
||||
text-color="rgba(255,255,255,.72)"
|
||||
active-text-color="#fff"
|
||||
:collapse="collapsed"
|
||||
:collapse-transition="false"
|
||||
>
|
||||
<el-menu-item v-for="item in navigation" :key="item.path" :index="item.path">
|
||||
<el-icon><component :is="item.icon || 'Menu'" /></el-icon>
|
||||
<template #title>{{ item.title }}</template>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<el-container>
|
||||
<el-header class="app-header">
|
||||
<el-button text class="collapse-button" :aria-label="collapsed ? '展开菜单' : '收起菜单'" @click="collapsed = !collapsed">
|
||||
<el-icon size="20"><Expand v-if="collapsed" /><Fold v-else /></el-icon>
|
||||
</el-button>
|
||||
<div class="header-title">视频感知管理</div>
|
||||
<div class="header-actions">
|
||||
<el-tag type="success" effect="plain">服务正常</el-tag>
|
||||
<el-dropdown>
|
||||
<el-button text>
|
||||
<el-icon><User /></el-icon>
|
||||
<span class="user-name">当前用户</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item disabled>个人信息</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<div class="tab-bar">
|
||||
<el-tag closable :disable-transitions="true" type="primary" effect="plain">{{ $route.meta.title || '工作台' }}</el-tag>
|
||||
</div>
|
||||
<el-main class="app-main"><router-view /></el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const collapsed = ref(false)
|
||||
const navigation = computed(() => {
|
||||
const root = router.getRoutes().find((route) => route.path === '/')
|
||||
return (root?.children || [])
|
||||
.filter((route) => route.meta?.title && !route.meta?.hidden)
|
||||
.map((route) => ({
|
||||
path: route.path ? `/${route.path}`.replace('//', '/') : '/',
|
||||
title: route.meta.title,
|
||||
icon: route.meta.icon,
|
||||
order: route.meta.order || 0
|
||||
}))
|
||||
.sort((left, right) => left.order - right.order)
|
||||
})
|
||||
|
||||
function logout() {
|
||||
window.sessionStorage.removeItem('sense_session')
|
||||
if (router.hasRoute('sense-login')) router.push({ name: 'sense-login' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-shell { min-height: 100vh; }
|
||||
.app-sidebar { background: var(--sense-sidebar); transition: width .18s ease; }
|
||||
.brand { display: flex; height: 56px; align-items: center; gap: 10px; padding: 0 18px; color: #fff; }
|
||||
.brand.collapsed { justify-content: center; padding: 0; }
|
||||
.brand-mark { display: inline-flex; width: 28px; height: 28px; align-items: center; justify-content: center; border-radius: 6px; background: var(--sense-primary); font-weight: 700; }
|
||||
.brand-name { font-size: 18px; font-weight: 600; letter-spacing: .4px; }
|
||||
.app-sidebar :deep(.el-menu) { border-right: 0; }
|
||||
.app-sidebar :deep(.el-menu-item.is-active) { background: var(--sense-primary); }
|
||||
.app-header { display: flex; height: 56px; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--sense-border); background: #fff; }
|
||||
.collapse-button { margin-right: 8px; }
|
||||
.header-title { font-size: 16px; font-weight: 500; }
|
||||
.header-actions { display: flex; align-items: center; gap: 12px; margin-left: auto; }
|
||||
.user-name { margin: 0 6px; }
|
||||
.tab-bar { display: flex; height: 40px; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--sense-border); background: #fff; }
|
||||
.app-main { padding: 0; background: var(--sense-bg); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user