feat: 配置环境变量

This commit is contained in:
zhanghao 2026-06-29 11:17:08 +08:00
parent deab8f583b
commit fc2d8691e3
7 changed files with 155 additions and 48 deletions

2
.env
View File

@ -1 +1,3 @@
VITE_DEVICE_ID = 'agv_src2200' VITE_DEVICE_ID = 'agv_src2200'
VITE_SERVICE_PORT = 3000

View File

@ -13,15 +13,23 @@ RUN pnpm run build
FROM node:18-alpine FROM node:18-alpine
WORKDIR /app WORKDIR /app
# 只拷贝生产需要的文件,保持镜像最小
COPY package.json package-lock.json pnpm-lock.yaml ./
RUN npm ci --omit=dev && npm cache clean --force
# 拷贝 Express 服务代码
COPY server/ ./server/
# 从构建阶段拷贝前端产物
COPY --from=builder /app/dist ./dist COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server ./server
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --prod --frozen-lockfile
# 暴露端口(与 Express 监听端口一致) # 暴露端口(与 Express 监听端口一致)
EXPOSE 3000 EXPOSE 3000
# 环境变量
ENV NODE_ENV=production
ENV VITE_SERVICE_PORT=3000
# 启动 Express 服务(使用 pnpm start 或 node server/index.js # 启动 Express 服务(使用 pnpm start 或 node server/index.js
CMD ["pnpm", "start"] CMD ["pnpm", "start"]
# 或直接 CMD ["node", "server/index.js"]

View File

@ -10,7 +10,7 @@ services:
- "3000:3000" # 映射端口 - "3000:3000" # 映射端口
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- PORT=3000 # 传给 Express 的环境变量 - VITE_SERVICE_PORT=3000 # 传给 Express 的环境变量
restart: unless-stopped # 容器意外停止后自动重启 restart: unless-stopped # 容器意外停止后自动重启
# 如果需要挂载日志或配置,可以加 volumes # 如果需要挂载日志或配置,可以加 volumes
# volumes: # volumes:

View File

@ -7,9 +7,9 @@
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"dev:client": "vite", "dev:client": "vite",
"dev:server": "node server/index.js", "dev:server": "node server/index.js",
"start": "node server/index.js", "start": "cross-env NODE_ENV=production node server/index.js",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "npm run build && cross-env NODE_ENV=production node server/index.js"
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "^2.3.2", "@element-plus/icons-vue": "^2.3.2",
@ -17,6 +17,7 @@
"@grpc/proto-loader": "^0.8.1", "@grpc/proto-loader": "^0.8.1",
"concurrently": "^10.0.3", "concurrently": "^10.0.3",
"cors": "^2.8.6", "cors": "^2.8.6",
"dotenv": "^17.4.2",
"element-plus": "^2.14.2", "element-plus": "^2.14.2",
"express": "^5.2.1", "express": "^5.2.1",
"fast-glob": "^3.3.3", "fast-glob": "^3.3.3",
@ -27,6 +28,7 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^6.0.7", "@vitejs/plugin-vue": "^6.0.7",
"cross-env": "^10.1.0",
"sass": "^1.101.0", "sass": "^1.101.0",
"unplugin-auto-import": "^21.0.0", "unplugin-auto-import": "^21.0.0",
"unplugin-vue-components": "^32.1.0", "unplugin-vue-components": "^32.1.0",

71
pnpm-lock.yaml generated
View File

@ -23,6 +23,9 @@ importers:
cors: cors:
specifier: ^2.8.6 specifier: ^2.8.6
version: 2.8.6 version: 2.8.6
dotenv:
specifier: ^17.4.2
version: 17.4.2
element-plus: element-plus:
specifier: ^2.14.2 specifier: ^2.14.2
version: 2.14.2(vue@3.5.38) version: 2.14.2(vue@3.5.38)
@ -48,6 +51,9 @@ importers:
'@vitejs/plugin-vue': '@vitejs/plugin-vue':
specifier: ^6.0.7 specifier: ^6.0.7
version: 6.0.7(vite@8.1.0(@types/node@26.0.0)(sass@1.101.0)(yaml@2.9.0))(vue@3.5.38) version: 6.0.7(vite@8.1.0(@types/node@26.0.0)(sass@1.101.0)(yaml@2.9.0))(vue@3.5.38)
cross-env:
specifier: ^10.1.0
version: 10.1.0
sass: sass:
specifier: ^1.101.0 specifier: ^1.101.0
version: 1.101.0 version: 1.101.0
@ -125,6 +131,9 @@ packages:
'@emnapi/wasi-threads@1.2.2': '@emnapi/wasi-threads@1.2.2':
resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==}
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
'@floating-ui/core@1.7.5': '@floating-ui/core@1.7.5':
resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==}
@ -709,6 +718,15 @@ packages:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
cross-env@10.1.0:
resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
engines: {node: '>=20'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-select@4.3.0: css-select@4.3.0:
resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==}
@ -844,6 +862,10 @@ packages:
domutils@3.2.2: domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
dotenv@17.4.2:
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
engines: {node: '>=12'}
dunder-proto@1.0.1: dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -1314,6 +1336,9 @@ packages:
isarray@2.0.5: isarray@2.0.5:
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
isobject@2.1.0: isobject@2.1.0:
resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==} resolution: {integrity: sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -1616,6 +1641,10 @@ packages:
resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==} resolution: {integrity: sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-to-regexp@8.4.2: path-to-regexp@8.4.2:
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
@ -1861,6 +1890,14 @@ packages:
setprototypeof@1.2.0: setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
shell-quote@1.8.4: shell-quote@1.8.4:
resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -2247,6 +2284,11 @@ packages:
resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -2342,6 +2384,8 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@epic-web/invariant@1.0.0': {}
'@floating-ui/core@1.7.5': '@floating-ui/core@1.7.5':
dependencies: dependencies:
'@floating-ui/utils': 0.2.11 '@floating-ui/utils': 0.2.11
@ -2913,6 +2957,17 @@ snapshots:
object-assign: 4.1.1 object-assign: 4.1.1
vary: 1.1.2 vary: 1.1.2
cross-env@10.1.0:
dependencies:
'@epic-web/invariant': 1.0.0
cross-spawn: 7.0.6
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
css-select@4.3.0: css-select@4.3.0:
dependencies: dependencies:
boolbase: 1.0.0 boolbase: 1.0.0
@ -3067,6 +3122,8 @@ snapshots:
domelementtype: 2.3.0 domelementtype: 2.3.0
domhandler: 5.0.3 domhandler: 5.0.3
dotenv@17.4.2: {}
dunder-proto@1.0.1: dunder-proto@1.0.1:
dependencies: dependencies:
call-bind-apply-helpers: 1.0.2 call-bind-apply-helpers: 1.0.2
@ -3644,6 +3701,8 @@ snapshots:
isarray@2.0.5: {} isarray@2.0.5: {}
isexe@2.0.0: {}
isobject@2.1.0: isobject@2.1.0:
dependencies: dependencies:
isarray: 1.0.0 isarray: 1.0.0
@ -3918,6 +3977,8 @@ snapshots:
pascalcase@0.1.1: {} pascalcase@0.1.1: {}
path-key@3.1.1: {}
path-to-regexp@8.4.2: {} path-to-regexp@8.4.2: {}
pathe@0.2.0: {} pathe@0.2.0: {}
@ -4223,6 +4284,12 @@ snapshots:
setprototypeof@1.2.0: {} setprototypeof@1.2.0: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
shell-quote@1.8.4: {} shell-quote@1.8.4: {}
side-channel-list@1.0.1: side-channel-list@1.0.1:
@ -4714,6 +4781,10 @@ snapshots:
gopd: 1.2.0 gopd: 1.2.0
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
which@2.0.2:
dependencies:
isexe: 2.0.0
wrap-ansi@7.0.0: wrap-ansi@7.0.0:
dependencies: dependencies:
ansi-styles: 4.3.0 ansi-styles: 4.3.0

View File

@ -5,6 +5,7 @@ import grpc from '@grpc/grpc-js'
import protoLoader from '@grpc/proto-loader' import protoLoader from '@grpc/proto-loader'
import path from 'path' import path from 'path'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import 'dotenv/config';
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
@ -241,8 +242,27 @@ app.post('/api/agv/stopRobot', async (req, res) => {
} }
}) })
// ========== 启动 ========== const isProd = process.env.NODE_ENV === 'production';
const PORT = 3000 const PORT = process.env.VITE_SERVICE_PORT || 3000;
// ============ 生产环境:托管 Vite 构建产物 ============
if (isProd) {
const distPath = resolve(__dirname, '../dist');
// 托管静态文件
app.use(express.static(distPath));
// SPA 回退:所有非 API 请求返回 index.html
app.get('*', (req, res) => {
res.sendFile(resolve(distPath, 'index.html'));
});
app.post('*', (req, res) => {
res.sendFile(resolve(distPath, 'index.html'));
});
}
// ========== 启动 =========
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`gRPC 桥接服务已启动: http://localhost:${PORT}`) console.log(`gRPC 桥接服务已启动: http://localhost:${PORT}`)

View File

@ -1,4 +1,4 @@
import { defineConfig } from 'vite' import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
@ -6,7 +6,10 @@ import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import path from 'path' import path from 'path'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig(({ mode, command }) => {
const env = loadEnv(mode, process.cwd())
const { VITE_SERVICE_PORT } = env
return {
resolve: { resolve: {
alias: { alias: {
'@': path.resolve(__dirname, 'src') '@': path.resolve(__dirname, 'src')
@ -32,7 +35,7 @@ export default defineConfig({
port: 5173, port: 5173,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3001', target: `http://localhost:${VITE_SERVICE_PORT}`,
changeOrigin: true changeOrigin: true
} }
} }
@ -45,4 +48,5 @@ export default defineConfig({
} }
} }
} }
}
}) })