first commit

This commit is contained in:
zyh
2025-08-24 15:47:51 +08:00
commit 69bf5500cd
13 changed files with 2262 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# Dependencies
node_modules/
# Temp
dist/
.vite/
.DS_Store
*.log

12
README.md Normal file
View File

@@ -0,0 +1,12 @@
# Vue 3 + Vite + Element Plus + Axios
最小可用的 Vue3 脚手架,集成:
- 组件库Element Plus
- 网络请求Axios含基础拦截器
快速使用:
- 安装依赖:`npm install`
- 开发:`npm run dev`
- 构建:`npm run build`;预览:`npm run preview`
详细文档请见:`docs/开发指南.md`

323
docs/开发指南.md Normal file
View File

@@ -0,0 +1,323 @@
# 项目开发指南Vue 3 + Vite + Element Plus + Axios
> 本文档面向日常开发与维护覆盖环境准备、目录结构、编码规范、网络请求、UI 使用、构建部署与常见问题等。
## 一、项目概览
- 框架Vue 3Composition API
- 构建Vite 5
- 组件库Element Plus
- 网络Axios已内置实例与拦截器
- 运行脚本:`npm run dev | build | preview`
## 二、环境要求
- Node.js建议 18+(可用 `node -v` 检查)
- 包管理器npm内置于 Node或 pnpm/yarn任选其一
- 操作系统Windows / macOS / Linux
## 三、目录结构
当前主要目录:
```
├─ index.html # 应用入口 HTML
├─ vite.config.js # Vite 配置
├─ package.json # 脚本与依赖
├─ src/
│ ├─ main.js # 应用入口,注册 Element Plus
│ ├─ App.vue # 示例页面(按钮 + 请求演示)
│ └─ plugins/
│ └─ http.js # Axios 实例与拦截器
└─ docs/
└─ 开发指南.md # 本文档
```
推荐扩展(按需新增):
```
src/
├─ api/ # 接口定义与请求函数(封装到 http 实例上)
├─ assets/ # 静态资源(图片、字体等)
├─ components/ # 通用基础组件
├─ views/ # 页面级组件
├─ router/ # 路由(如使用 Vue Router
├─ store/ # 状态管理(如使用 Pinia
├─ styles/ # 全局样式、变量、主题定制
└─ utils/ # 工具方法(格式化、校验、下载等)
```
## 四、快速开始
- 安装依赖:`npm install`
- 启动开发:`npm run dev`(默认 http://localhost:5173
- 构建产物:`npm run build`(输出至 `dist/`
- 本地预览:`npm run preview`
## 五、配置说明
### 1. 环境变量
建议使用 Vite 的环境文件管理不同环境:
- `.env.development`(开发)
- `.env.production`(生产)
示例内容:
```
# .env.development
VITE_API_BASE=https://dev-api.example.com
# .env.production
VITE_API_BASE=https://api.example.com
```
在代码中使用:
```js
const baseURL = import.meta.env.VITE_API_BASE
```
### 2. Vite 配置(`vite.config.js`
常用配置要点:
- 别名:为 `src` 设置 `@` 方便导入。
- 代理:开发阶段转发 API避免 CORS。
示例(按需修改后替换到 `vite.config.js`
```js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
},
server: {
proxy: {
'/api': {
target: 'https://dev-api.example.com',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, ''),
},
},
},
})
```
### 3. Axios 配置(`src/plugins/http.js`
当前实现:
- `baseURL: '/'`(建议改为 `import.meta.env.VITE_API_BASE`
- 超时 15s
- 预留请求拦截器(注入 Token与响应拦截器统一错误处理
建议修改:
```js
const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE || '/',
timeout: 15000,
})
```
统一错误处理示例(可在响应拦截器中加入):
```js
import { ElMessage } from 'element-plus'
http.interceptors.response.use(
(res) => res,
(error) => {
const msg = error?.response?.data?.message || error.message || '请求失败'
ElMessage.error(msg)
return Promise.reject(error)
}
)
```
## 六、代码与组件规范
- 组件目录:一个组件一个目录(可含 `index.vue` / `index.ts`)。
- 命名规范:
- 组件名:`PascalCase`(如 `UserCard.vue`)。
- 变量/函数:`camelCase`
- 文件:`kebab-case`(工具类/样式)。
- 组件结构:`<template>` / `<script setup>` / `<style scoped>` 顺序。
- 导入顺序Vue/第三方 → 别名 `@` → 相对路径。
- 提交信息语义化feat/fix/docs/chore/refactor/style/test/build
## 七、UI 与交互Element Plus
- 全量引入已配置于 `src/main.js`
```js
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
```
- 主题定制:
- 推荐在 `src/styles/variables.scss` 定义主题变量,按需覆盖 Element Plus 变量。
- 也可使用暗色主题与自定义主题(参考官方文档)。
- 常用交互:
- 消息:`ElMessage.success('保存成功')`
- 弹窗:`ElMessageBox.confirm('确定操作吗?', '提示')`
- 加载:`ElLoading.service({ text: '加载中...' })`
## 八、网络请求规范
- 统一通过 `src/plugins/http.js` 导出的 `http` 发起请求:
```js
import http from '@/plugins/http'
export function fetchUser(id) {
return http.get(`/users/${id}`)
}
export function createUser(payload) {
return http.post('/users', payload)
}
```
- 结果解构与错误处理:
```js
try {
const { data } = await fetchUser(1)
// 根据后端约定判断 code / success
} catch (e) {
// 统一拦截器会提示,这里可按需记录或兜底
}
```
- 可选:取消请求(例如在切换路由或重复点击时):
```js
const controller = new AbortController()
http.get('/path', { signal: controller.signal })
// 需要时取消
controller.abort()
```
## 九、常见开发食谱
- 新增页面:
-`src/views/` 新建页面组件,如 `UserList.vue`
- 如使用路由,在 `router` 中注册路由并在入口挂载。
- 调用接口 + 加载态示例(参考 `App.vue`
```vue
<script setup>
import { ref } from 'vue'
import http from '@/plugins/http'
const loading = ref(false)
const data = ref(null)
async function load() {
loading.value = true
try {
const res = await http.get('/example')
data.value = res.data
} finally {
loading.value = false
}
}
</script>
```
- 表单校验Element Plus
- 使用 `el-form` + `rules` + `el-form-item`,提交前 `formRef.validate()`
- 消息提示:
- 成功:`ElMessage.success('操作成功')`
- 失败:`ElMessage.error('操作失败')`
## 十、路由与状态(可选集成)
- 安装:
- Vue Router`npm i vue-router`
- Pinia`npm i pinia`
- 快速示例Router
```js
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{ path: '/', component: () => import('@/views/Home.vue') },
]
export default createRouter({ history: createWebHistory(), routes })
```
```js
// main.js
import router from '@/router'
app.use(router)
```
- 快速示例Pinia
```js
// src/store/index.js
import { createPinia, defineStore } from 'pinia'
export const pinia = createPinia()
export const useCounter = defineStore('counter', {
state: () => ({ count: 0 }),
})
```
```js
// main.js
import { pinia } from '@/store'
app.use(pinia)
```
## 十一、构建与部署
- 构建命令:`npm run build`,产物在 `dist/`
- 预览构建:`npm run preview`
- 静态资源路径:如需部署到子路径,设置 `base``vite.config.js`)或 `--base`
- Nginx 示例:
```
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass https://api.example.com/;
}
```
## 十二、质量保障(可选)
- 代码格式化:建议接入 Prettier + ESLint。
- 提交规范:建议使用 Commitlint + Husky 规范提交信息。
- 单元测试:可引入 Vitest 进行组件与函数测试。
## 十三、常见问题FAQ
- 无法请求跨域?
- 开发环境配置 Vite 代理(见上文 server.proxy
- 生产环境由网关或 Nginx 统一转发。
- 接口报错但页面无提示?
- 确认是否已在响应拦截器中调用 `ElMessage.error`
- Element Plus 主题不生效?
- 确保样式覆盖顺序正确,且未被 `scoped` 限制。
---
如需我帮你接入 Router/Pinia、主题定制或代码规范工具链请告诉我具体偏好与要求。

21
docs/需求文档.md Normal file
View File

@@ -0,0 +1,21 @@
明白✅
1. 二界面 URL 模板
我已把配置改成:**`域名/{codeNo}`**(文档已更新为 `https://你的域名/{codeNo}`,按运行环境替换域名即可)。
2. 关于 `http://36.138.184.60:12345/编号/二维码.png`
这个是“**脚本端**”暴露出来的**静态图片**地址——**正常情况下会直接返回 PNG 图片**。如果你在页面里看不到图,常见原因有:
* **混合内容被拦截**:你的前端如果是 **HTTPS**,而这张图是 **HTTP**,浏览器会拦截(安全策略)。
解决:给 `36.138.184.60:12345`**HTTPS**,或让后端提供一个 **HTTPS 代理端点**(例如:`GET /api/link/{token}/qr.png`)去转发这张图片,前端只请求自己的 HTTPS 域名即可。(我已在文档里加了这条注意事项)
* **二维码尚未生成 / 已过期**:脚本端还没把 `二维码.png` 写到对应 `编号/` 目录,或已被轮转清理 → 会 404 或显示旧图。
解决:确保先触发“生成二维码”动作,并用返回的 `createdAt/expireAt` 做本地倒计时;必要时加 `?t=时间戳` 防缓存。
* **编号不匹配**:前端用的 `编号` 与实际分配的机器编号不同。
解决:以后端返回的 `machineId/编号` 为准。
* **端口/防火墙**12345 端口未对外放通或临时不可达。
解决:主机/网关放行该端口并做健康检查。
* **跨域非问题**`<img>` 加载图片不受 CORS 限制,但若被混合内容拦截,同样会显示不出。
如果你愿意最省心的做法:我们在后端新增一个**图片代理接口**HTTPS前端统一拿这个接口的 URL当脚本端换 IP/端口或做 HTTPS这边都不用改前端。
需要的话我可以顺手把这个代理端点的 Spring Boot 代码骨架也给你(带缓存/超时/错误降级)。

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue3 App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1655
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "vue3-app",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.7.7",
"element-plus": "^2.8.8",
"vue": "^3.4.38"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.1.4",
"vite": "^5.4.6"
}
}

11
src/App.vue Normal file
View File

@@ -0,0 +1,11 @@
<template>
<Login />
</template>
<script setup>
import Login from './views/Login.vue'
</script>
<style scoped>
/* 页面样式由 Login 组件内维护 */
</style>

9
src/api/auth.js Normal file
View File

@@ -0,0 +1,9 @@
import http from '../plugins/http'
// 登录 API 封装
export function login(payload) {
// 约定 payload: { username: string, password: string }
// 根据实际接口调整路径与字段
return http.post('/auth/login', payload)
}

10
src/main.js Normal file
View File

@@ -0,0 +1,10 @@
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')

29
src/plugins/http.js Normal file
View File

@@ -0,0 +1,29 @@
import axios from 'axios'
const http = axios.create({
baseURL: '/',
timeout: 15000,
})
// 请求拦截器
http.interceptors.request.use(
(config) => {
// 例如:在此添加认证 token
// const token = localStorage.getItem('token')
// if (token) config.headers.Authorization = `Bearer ${token}`
return config
},
(error) => Promise.reject(error)
)
// 响应拦截器
http.interceptors.response.use(
(response) => response,
(error) => {
// 统一错误处理(可以按需要自定义)
return Promise.reject(error)
}
)
export default http

141
src/views/Login.vue Normal file
View File

@@ -0,0 +1,141 @@
<template>
<div class="login-page">
<el-card class="login-card" shadow="hover">
<template #header>
<div class="card-header">
<img class="logo" alt="logo" src="https://vuejs.org/images/logo.png" />
<div class="title">平台登录</div>
</div>
</template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="80px" @keyup.enter.native="onSubmit">
<el-form-item label="用户名" prop="username">
<el-input v-model.trim="form.username" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model.trim="form.password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<div class="actions">
<el-checkbox v-model="remember">记住我</el-checkbox>
<el-link type="primary" :underline="false" @click="onForget">忘记密码</el-link>
</div>
<el-form-item>
<el-button type="primary" :loading="loading" class="submit" @click="onSubmit"> </el-button>
</el-form-item>
</el-form>
<el-alert
v-if="notice"
:title="notice"
type="info"
show-icon
class="notice"
/>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { login } from '../api/auth'
const formRef = ref()
const loading = ref(false)
const remember = ref(false)
const notice = ref('')
const form = ref({
username: '',
password: '',
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少 6 位', trigger: 'blur' },
],
}
onMounted(() => {
const saved = localStorage.getItem('login-remember')
const savedUser = localStorage.getItem('login-username')
remember.value = saved === '1'
if (remember.value && savedUser) {
form.value.username = savedUser
}
notice.value = import.meta?.env?.VITE_API_BASE
? `当前 API: ${import.meta.env.VITE_API_BASE}`
: '未配置 VITE_API_BASE默认使用 /'
})
function onForget() {
ElMessage.info('请联系管理员重置密码')
}
function persistRemember() {
localStorage.setItem('login-remember', remember.value ? '1' : '0')
if (remember.value) {
localStorage.setItem('login-username', form.value.username || '')
} else {
localStorage.removeItem('login-username')
}
}
async function onSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
loading.value = true
try {
const payload = { username: form.value.username, password: form.value.password }
const res = await login(payload)
// 依据后端返回结构处理,这里仅做示例提示
ElMessage.success('登录成功')
persistRemember()
console.debug('login response:', res.data)
// TODO: 登录成功后的跳转(如接入 Router
} catch (e) {
const msg = e?.response?.data?.message || e.message || '登录失败'
ElMessage.error(msg)
} finally {
loading.value = false
}
})
}
</script>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #f2f6fc 0%, #ffffff 100%);
}
.login-card {
width: 420px;
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
}
.logo { width: 32px; height: 32px; }
.title { font-size: 18px; font-weight: 600; }
.actions {
display: flex;
justify-content: space-between;
align-items: center;
margin: -4px 0 8px;
}
.submit { width: 100%; }
.notice { margin-top: 8px; }
</style>

8
vite.config.js Normal file
View File

@@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
})