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