feat: initialize independent Sense product skeleton (#21)

This commit is contained in:
QiuSW
2026-08-12 16:49:17 +08:00
parent be2c1e3c81
commit d2169990d3
33 changed files with 9140 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
<template>
<router-view />
</template>
<script setup>
</script>
+55
View File
@@ -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>
+14
View File
@@ -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>
+19
View File
@@ -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')
+16
View File
@@ -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
+45
View File
@@ -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
+11
View File
@@ -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 })
+25
View File
@@ -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; }