Template
first
This commit is contained in:
+39
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Generated
+3371
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"debug": true,
|
||||||
|
"endpoints": {
|
||||||
|
"default": "http://localhost:25209"
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -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
@@ -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')
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user