This commit is contained in:
2026-08-10 22:05:18 +08:00
commit 1e294cb4ae
18 changed files with 3922 additions and 0 deletions
+39
View File
@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs
+23
View File
@@ -0,0 +1,23 @@
ARG VERSION=0.0.0
# 构建阶段使用 Node.js
FROM registry.cn-heyuan.aliyuncs.com/vector-flow/node:20-alpine AS build-stage
# 设置工作目录
ARG VERSION
WORKDIR /app
# 复制 package.json 和 package-lock.json
COPY ./ ./
# 安装依赖
RUN npm ci --registry=https://registry.npmmirror.com
RUN npm pkg set version=${VERSION}
# 构建应用
RUN npm run build
# 生产阶段使用 Nginx
FROM registry.cn-heyuan.aliyuncs.com/vector-flow/nginx:stable-alpine AS production-stage
# 复制构建好的文件到 Nginx 服务目录
COPY --from=build-stage /app/dist /usr/share/nginx/html
# 复制自定义 Nginx 配置文件
COPY ./nginx.conf /etc/nginx/conf.d/default.conf
# 暴露 80 端口
EXPOSE 80
# 运行 Nginx
CMD ["nginx", "-g", "daemon off;"]
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SimApiVueTemplate</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+12
View File
@@ -0,0 +1,12 @@
server {
listen 80;
server_name localhost;
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}
+3371
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -0,0 +1,35 @@
{
"name": "SimApiVueTemplate",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite --host 0.0.0.0 --port 15106",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@simcu/simapi": "^3.2608.6",
"element-plus": "^2.14.2",
"pinia": "^3.0.4",
"vue": "^3.5.38",
"vue-router": "^5.1.0"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.13.2",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/tsconfig": "^0.9.1",
"npm-run-all2": "^9.0.2",
"typescript": "~6.0.0",
"vite": "^8.0.16",
"vite-plugin-vue-devtools": "^8.1.2",
"vue-tsc": "^3.3.5"
},
"engines": {
"node": "^22.18.0 || >=24.12.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"debug": true,
"endpoints": {
"default": "http://localhost:25209"
}
}
+21
View File
@@ -0,0 +1,21 @@
<template>
<router-view />
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { useSimApi } from '@simcu/simapi/pinia'
const router = useRouter()
const api = useSimApi()
api.setBusinessCallback(401, () => {
api.logout(null)
router.replace('/login')
})
api.setBusinessCallback('common', (data: any) => {
ElMessage.error(data?.message || '请求失败')
})
</script>
+23
View File
@@ -0,0 +1,23 @@
import {createApp} from 'vue'
import {createPinia} from 'pinia'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import {useSimApi} from '@simcu/simapi/pinia'
import './style.css'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router.ts'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, {locale: zhCn})
const simApi = useSimApi()
await simApi.loadFromFile()
const config = await simApi.getConfig()
if (config.ProductName) {
document.title = `${config.ProductName}`
}
app.mount('#app')
+38
View File
@@ -0,0 +1,38 @@
import {createRouter, createWebHistory} from 'vue-router'
import {useSimApi} from '@simcu/simapi/pinia'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: {requiresAuth: false},
},
{
path: '/',
component: () => import('@/views/Layout.vue'),
meta: {requiresAuth: true},
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
}
]
}
]
})
router.beforeEach(async (to) => {
const api = useSimApi()
if (to.meta.requiresAuth === false) {
return
}
if (!api.getToken()) {
return '/login'
}
})
export default router
+10
View File
@@ -0,0 +1,10 @@
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
}
.header-card {
margin-bottom: 16px;
}
+33
View File
@@ -0,0 +1,33 @@
<template>
<el-card shadow="never">
<template #header>
<h3 class="card-title">系统信息</h3>
</template>
<el-descriptions :column="2" border>
<el-descriptions-item label="后端版本">{{ versionInfo.apiAppFull || '-' }}</el-descriptions-item>
<el-descriptions-item label="SimApi 版本">{{ versionInfo.apiSimApi || '-' }}</el-descriptions-item>
<el-descriptions-item label="前端版本">{{ versionInfo.uiApp || '-' }}</el-descriptions-item>
<el-descriptions-item label="SimApi UI 版本">{{ versionInfo.uiSimApi || '-' }}</el-descriptions-item>
</el-descriptions>
</el-card>
</template>
<script setup lang="ts">
import {ref, onMounted} from 'vue'
import {useSimApi} from '@simcu/simapi/pinia'
const api = useSimApi()
const versionInfo = ref<Record<string, object>>({})
onMounted(async () => {
const conf = await api.getConfig();
versionInfo.value = conf.Versions as Record<string, object>;
})
</script>
<style scoped>
.card-title {
margin: 0;
font-size: 16px;
}
</style>
+109
View File
@@ -0,0 +1,109 @@
<template>
<el-container style="height: 100vh">
<el-aside width="220px" class="sidebar">
<div class="logo">
<span>{{ appName }}</span>
</div>
<el-menu
:default-active="activeMenu"
router
background-color="#001529"
text-color="#ffffff80"
active-text-color="#ffffff"
>
<el-menu-item index="/">
<el-icon>
<HomeFilled/>
</el-icon>
<span>控制台</span>
</el-menu-item>
<el-menu-item index="/apps">
<el-icon>
<Grid/>
</el-icon>
<span>应用管理</span>
</el-menu-item>
<el-menu-item index="/users">
<el-icon>
<User/>
</el-icon>
<span>用户管理</span>
</el-menu-item>
</el-menu>
<div class="sidebar-footer">
<el-button text type="danger" style="width: 100%" @click="handleLogout">
退出登录
</el-button>
</div>
</el-aside>
<el-main class="main-area">
<router-view/>
</el-main>
</el-container>
</template>
<script setup lang="ts">
import {computed, onMounted, ref} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {HomeFilled, Grid, User} from '@element-plus/icons-vue'
import {useSimApi} from '@simcu/simapi/pinia'
import {ElMessage} from 'element-plus'
const route = useRoute()
const router = useRouter()
const api = useSimApi()
const appName = ref('LemonAuth')
const activeMenu = computed(() => route.path)
async function handleLogout() {
api.logout(null)
router.replace('/login')
}
onMounted(async () => {
api.query('/user/info').catch(() => {
ElMessage.warning('登录态验证失败')
})
appName.value = document.title;
})
</script>
<style scoped>
.sidebar {
background-color: #001529;
display: flex;
flex-direction: column;
overflow: hidden;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-weight: 600;
letter-spacing: 2px;
border-bottom: 1px solid #ffffff1a;
}
.el-menu {
flex: 1;
border-right: none;
}
.sidebar-footer {
padding: 12px;
border-top: 1px solid #ffffff1a;
}
.main-area {
background: #f0f2f5;
padding: 24px;
overflow-y: auto;
}
</style>
+113
View File
@@ -0,0 +1,113 @@
<template>
<div class="login-wrapper">
<el-card class="login-card">
<template #header>
<div class="login-header">{{ appName }} 系统登录</div>
</template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="0"
@keyup.enter="handleLogin"
>
<el-form-item prop="username">
<el-input
v-model="form.username"
placeholder="用户名"
:prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="密码"
show-password
:prefix-icon="Lock"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="submitting"
style="width: 100%"
@click="handleLogin"
>
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts">
import {onMounted, reactive, ref} from 'vue'
import {useRouter} from 'vue-router'
import {User, Lock} from '@element-plus/icons-vue'
import {useSimApi} from '@simcu/simapi/pinia'
import type {FormInstance, FormRules} from 'element-plus'
const router = useRouter()
const api = useSimApi()
const appName = ref<string>("")
const formRef = ref<FormInstance>()
const submitting = ref(false)
const form = reactive({
username: '',
password: '',
})
const rules: FormRules = {
username: [{required: true, message: '请输入用户名', trigger: 'blur'}],
password: [{required: true, message: '请输入密码', trigger: 'blur'}],
}
async function handleLogin() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
const res = await api.query<string>('/auth/login/admin', {
username: form.username,
password: form.password,
})
if (res.data) {
api.setToken(res.data)
router.replace('/')
}
} catch {
// 错误由 businessCallback 统一处理
} finally {
submitting.value = false
}
}
onMounted(async () => {
appName.value = document.title;
})
</script>
<style scoped>
.login-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
background: #f0f2f5;
}
.login-card {
width: 400px;
}
.login-header {
text-align: center;
font-size: 18px;
font-weight: 600;
}
</style>
+18
View File
@@ -0,0 +1,18 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
// Extra safety for array and object lookups, but may have false positives.
"noUncheckedIndexedAccess": true,
// Path mapping for cleaner imports.
"paths": {
"@/*": ["./src/*"]
},
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}
+27
View File
@@ -0,0 +1,27 @@
// TSConfig for modules that run in Node.js environment via either transpilation or type-stripping.
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
// Most tools use transpilation instead of Node.js's native type-stripping.
// Bundler mode provides a smoother developer experience.
"module": "preserve",
"moduleResolution": "bundler",
// Include Node.js types and avoid accidentally including other `@types/*` packages.
"types": ["node"],
// Disable emitting output during `vue-tsc --build`, which is used for type-checking only.
"noEmit": true,
// `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking.
// Specified here to keep it out of the root directory.
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo"
}
}
+20
View File
@@ -0,0 +1,20 @@
import { fileURLToPath, URL } from 'node:url'
import { readFileSync } from 'node:fs'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'))
export default defineConfig({
plugins: [vue(), vueDevTools()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
define: {
AppVersion: JSON.stringify(pkg.version),
},
})