Initial commit: Audio Dashboard Management System
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
@@ -0,0 +1,148 @@
|
||||
# 耳机管理平台 - 前端项目
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **Vue 3** - 渐进式 JavaScript 框架
|
||||
- **Vue Router 4** - Vue.js 官方路由
|
||||
- **Element Plus** - 基于 Vue 3 的组件库
|
||||
- **Axios** - HTTP 客户端
|
||||
- **Vite** - 下一代前端构建工具
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── api/ # API 接口
|
||||
│ │ └── brand.js # 品牌管理接口
|
||||
│ ├── layout/ # 布局组件
|
||||
│ │ └── index.vue # 主布局
|
||||
│ ├── router/ # 路由配置
|
||||
│ │ └── index.js # 路由定义
|
||||
│ ├── utils/ # 工具函数
|
||||
│ │ └── request.js # Axios 封装
|
||||
│ ├── views/ # 页面组件
|
||||
│ │ └── brand/ # 品牌管理页面
|
||||
│ │ └── index.vue
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.js # 入口文件
|
||||
├── index.html # HTML 模板
|
||||
├── package.json # 依赖配置
|
||||
├── vite.config.js # Vite 配置
|
||||
├── install.bat # 安装依赖脚本
|
||||
└── start.bat # 启动脚本
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
**方式一:使用脚本(推荐)**
|
||||
```bash
|
||||
双击 install.bat
|
||||
```
|
||||
|
||||
**方式二:手动安装**
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 2. 启动开发服务器
|
||||
|
||||
**方式一:使用脚本(推荐)**
|
||||
```bash
|
||||
双击 start.bat
|
||||
```
|
||||
|
||||
**方式二:手动启动**
|
||||
```bash
|
||||
cd frontend
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
启动后访问:http://localhost:3000
|
||||
|
||||
### 3. 构建生产版本
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 品牌管理
|
||||
|
||||
- ✅ 品牌列表展示
|
||||
- ✅ 品牌名称模糊搜索
|
||||
- ✅ 新增品牌
|
||||
- ✅ 编辑品牌
|
||||
- ✅ 删除品牌
|
||||
- ✅ 分页功能
|
||||
|
||||
### 菜单结构
|
||||
|
||||
```
|
||||
耳机管理(一级菜单)
|
||||
└── 品牌管理(二级菜单)
|
||||
```
|
||||
|
||||
## API 配置
|
||||
|
||||
开发服务器已配置代理,将 `/api` 请求转发到后端服务:
|
||||
|
||||
- 前端请求:`/api/brands/`
|
||||
- 后端地址:`http://localhost:8002/api/brands/`
|
||||
|
||||
配置位于 `vite.config.js`
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **确保后端服务已启动**
|
||||
- 后端运行在 http://localhost:8002
|
||||
- 使用 `backend/restart.bat` 重启后端服务
|
||||
|
||||
2. **端口占用**
|
||||
- 前端使用 3000 端口
|
||||
- 后端使用 8002 端口
|
||||
- 如果端口被占用,请修改相应配置文件
|
||||
|
||||
3. **跨域问题**
|
||||
- 开发环境使用 Vite 代理解决跨域
|
||||
- 生产环境需要配置 CORS 或使用 Nginx 反向代理
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 依赖安装失败
|
||||
A: 检查网络连接,或使用淘宝镜像:
|
||||
```bash
|
||||
pnpm config set registry https://registry.npmmirror.com
|
||||
pnpm install
|
||||
```
|
||||
|
||||
如果还没有安装 pnpm,请先安装:
|
||||
```bash
|
||||
npm install -g pnpm
|
||||
```
|
||||
|
||||
### Q: 页面空白
|
||||
A: 打开浏览器控制台查看错误信息,确保后端服务正常运行
|
||||
|
||||
### Q: API 请求失败
|
||||
A: 检查后端服务是否启动,端口是否正确
|
||||
|
||||
## 开发规范
|
||||
|
||||
- 使用 Vue 3 Composition API (script setup)
|
||||
- 使用 Element Plus 组件库
|
||||
- 统一使用中文注释和提示
|
||||
- API 响应格式:`{ code: 1, msg: "success", data: {...} }`
|
||||
|
||||
## 后续扩展
|
||||
|
||||
可以添加的功能:
|
||||
- [ ] 型号管理页面
|
||||
- [ ] 用户登录/权限管理
|
||||
- [ ] 数据导出功能
|
||||
- [ ] 批量操作
|
||||
- [ ] 操作日志
|
||||
@@ -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>耳机管理平台</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ================================================
|
||||
echo 前端项目 - 安装依赖
|
||||
echo ================================================
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo 正在安装依赖...
|
||||
echo.
|
||||
|
||||
REM 使用官方镜像安装
|
||||
pnpm config set registry https://registry.npmjs.org
|
||||
pnpm install --registry=https://registry.npmjs.org
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo 依赖安装失败!请检查网络连接。
|
||||
echo 尝试使用淘宝镜像...
|
||||
echo.
|
||||
pnpm config set registry https://registry.npmmirror.com
|
||||
pnpm install
|
||||
)
|
||||
|
||||
if errorlevel 1 (
|
||||
echo.
|
||||
echo 依赖安装失败!请检查网络连接。
|
||||
pause
|
||||
) else (
|
||||
echo.
|
||||
echo 依赖安装完成!
|
||||
echo.
|
||||
echo 运行以下命令启动项目:
|
||||
echo pnpm dev
|
||||
echo.
|
||||
pause
|
||||
)
|
||||
Generated
+1338
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "audio-dashboard-frontend",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4",
|
||||
"axios": "^1.5.0",
|
||||
"element-plus": "^2.3.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.3.4",
|
||||
"vite": "^4.4.9"
|
||||
},
|
||||
"packageManager": "pnpm@8.0.0"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#app {
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取品牌列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.skip - 跳过记录数
|
||||
* @param {number} params.limit - 返回记录数
|
||||
* @param {string} params.name - 品牌名称(模糊查询)
|
||||
*/
|
||||
export function getBrands(params) {
|
||||
return request({
|
||||
url: '/brands/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个品牌
|
||||
* @param {number} id - 品牌 ID
|
||||
*/
|
||||
export function getBrand(id) {
|
||||
return request({
|
||||
url: `/brands/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建品牌
|
||||
* @param {Object} data - 品牌数据
|
||||
* @param {string} data.name - 品牌名称
|
||||
*/
|
||||
export function createBrand(data) {
|
||||
return request({
|
||||
url: '/brands/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新品牌
|
||||
* @param {Object} data - 品牌数据
|
||||
* @param {number} data.id - 品牌 ID
|
||||
* @param {string} data.name - 品牌名称
|
||||
*/
|
||||
export function updateBrand(data) {
|
||||
return request({
|
||||
url: `/brands/${data.id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除品牌
|
||||
* @param {number} id - 品牌 ID
|
||||
*/
|
||||
export function deleteBrand(id) {
|
||||
return request({
|
||||
url: `/brands/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取型号列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {number} params.skip - 跳过记录数
|
||||
* @param {number} params.limit - 返回记录数
|
||||
* @param {string} params.brand_name - 品牌名称(模糊查询)
|
||||
* @param {string} params.name - 型号名称(模糊查询)
|
||||
*/
|
||||
export function getModels(params) {
|
||||
return request({
|
||||
url: '/models/',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个型号
|
||||
* @param {number} id - 型号 ID
|
||||
*/
|
||||
export function getModel(id) {
|
||||
return request({
|
||||
url: `/models/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建型号
|
||||
* @param {Object} data - 型号数据
|
||||
*/
|
||||
export function createModel(data) {
|
||||
return request({
|
||||
url: '/models/',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新型号
|
||||
* @param {Object} data - 型号数据
|
||||
* @param {number} data.id - 型号 ID
|
||||
*/
|
||||
export function updateModel(data) {
|
||||
return request({
|
||||
url: `/models/${data.id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除型号
|
||||
* @param {number} id - 型号 ID
|
||||
*/
|
||||
export function deleteModel(id) {
|
||||
return request({
|
||||
url: `/models/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送到 Meilisearch
|
||||
* @param {Array<number>} modelIds - 型号 ID 列表
|
||||
*/
|
||||
export function pushToMeilisearch(modelIds) {
|
||||
return request({
|
||||
url: '/models/push-to-search',
|
||||
method: 'post',
|
||||
data: { model_ids: modelIds }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="app-wrapper">
|
||||
<el-container>
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside width="200px" class="sidebar">
|
||||
<div class="logo">
|
||||
<h2>耳机管理平台</h2>
|
||||
</div>
|
||||
<el-menu
|
||||
:default-active="activeMenu"
|
||||
background-color="#304156"
|
||||
text-color="#bfcbd9"
|
||||
active-text-color="#409EFF"
|
||||
router
|
||||
>
|
||||
<el-sub-menu index="1">
|
||||
<template #title>
|
||||
<el-icon><Headset /></el-icon>
|
||||
<span>耳机管理</span>
|
||||
</template>
|
||||
<el-menu-item index="/brand">
|
||||
<el-icon><Collection /></el-icon>
|
||||
<span>品牌管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="/model">
|
||||
<el-icon><List /></el-icon>
|
||||
<span>型号管理</span>
|
||||
</el-menu-item>
|
||||
</el-sub-menu>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<el-container>
|
||||
<el-header class="header">
|
||||
<div class="header-title">耳机品牌与型号管理系统</div>
|
||||
</el-header>
|
||||
<el-main class="main-content">
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { Headset, Collection, List } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const activeMenu = computed(() => route.path)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-wrapper {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.el-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #304156;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #2b3a4b;
|
||||
}
|
||||
|
||||
.logo h2 {
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0,21,41,.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
background-color: #f0f2f5;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(ElementPlus, { locale: zhCn })
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
||||
@@ -0,0 +1,31 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Layout from '@/layout/index.vue'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/brand',
|
||||
children: [
|
||||
{
|
||||
path: '/brand',
|
||||
name: 'Brand',
|
||||
component: () => import('@/views/brand/index.vue'),
|
||||
meta: { title: '品牌管理', icon: 'collection' }
|
||||
},
|
||||
{
|
||||
path: '/model',
|
||||
name: 'Model',
|
||||
component: () => import('@/views/model/index.vue'),
|
||||
meta: { title: '型号管理', icon: 'list' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
@@ -0,0 +1,41 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
// 创建 axios 实例
|
||||
const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
request.interceptors.request.use(
|
||||
config => {
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
console.error('请求错误:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
request.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 如果响应码为 0,表示错误
|
||||
if (res.code === 0) {
|
||||
ElMessage.error(res.msg || '请求失败')
|
||||
return Promise.reject(new Error(res.msg || '请求失败'))
|
||||
}
|
||||
|
||||
return res
|
||||
},
|
||||
error => {
|
||||
console.error('响应错误:', error)
|
||||
ElMessage.error(error.message || '网络错误')
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default request
|
||||
@@ -0,0 +1,313 @@
|
||||
<template>
|
||||
<div class="brand-container">
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="品牌名称">
|
||||
<el-input
|
||||
v-model="searchForm.name"
|
||||
placeholder="请输入品牌名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="table-card">
|
||||
<div class="card-header">
|
||||
<span class="title">品牌列表</span>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增品牌
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="name" label="品牌名称" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="500px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="品牌名称" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入品牌名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { getBrands, createBrand, updateBrand, deleteBrand } from '@/api/brand'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增品牌')
|
||||
const formRef = ref(null)
|
||||
|
||||
const searchForm = reactive({
|
||||
name: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const tableData = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
name: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入品牌名称', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getBrands({
|
||||
skip: (pagination.page - 1) * pagination.pageSize,
|
||||
limit: pagination.pageSize,
|
||||
name: searchForm.name || undefined
|
||||
})
|
||||
|
||||
if (res.code === 1 && res.data) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else if (res.code === 2) {
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.name = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新增品牌'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑品牌'
|
||||
formData.id = row.id
|
||||
formData.name = row.name
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除品牌 "${row.name}" 吗?`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
submitLoading.value = true
|
||||
const res = await deleteBrand(row.id)
|
||||
|
||||
if (res.code === 1) {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const api = formData.id ? updateBrand : createBrand
|
||||
const res = await api(formData)
|
||||
|
||||
if (res.code === 1) {
|
||||
ElMessage.success(formData.id ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 对话框关闭
|
||||
const handleDialogClose = () => {
|
||||
formData.id = null
|
||||
formData.name = ''
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.brand-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,486 @@
|
||||
<template>
|
||||
<div class="model-container">
|
||||
<el-card class="search-card">
|
||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||
<el-form-item label="品牌名称">
|
||||
<el-select
|
||||
v-model="searchForm.brand_name"
|
||||
placeholder="请选择品牌"
|
||||
filterable
|
||||
clearable
|
||||
style="width: 200px"
|
||||
>
|
||||
<el-option
|
||||
v-for="brand in brandOptions"
|
||||
:key="brand.id"
|
||||
:label="brand.name"
|
||||
:value="brand.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="型号名称">
|
||||
<el-input
|
||||
v-model="searchForm.name"
|
||||
placeholder="请输入型号名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="table-card">
|
||||
<div class="card-header">
|
||||
<span class="title">型号列表</span>
|
||||
<div class="header-buttons">
|
||||
<el-button
|
||||
type="warning"
|
||||
:disabled="selectedIds.length === 0"
|
||||
@click="handlePushToSearch"
|
||||
:loading="pushLoading"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>
|
||||
推送到搜索 ({{ selectedIds.length }})
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新型号
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
style="width: 100%"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="brand_name" label="品牌名称" width="150" />
|
||||
<el-table-column prop="name" label="型号名称" />
|
||||
<el-table-column prop="form" label="佩戴方式" width="120">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.form === 'in-ear'" type="success">入耳式</el-tag>
|
||||
<el-tag v-else-if="scope.row.form === 'over-ear'" type="primary">头戴式</el-tag>
|
||||
<el-tag v-else-if="scope.row.form === 'earbud'" type="info">耳塞式</el-tag>
|
||||
<span v-else>{{ scope.row.form || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rig" label="阻抗" width="120" />
|
||||
<el-table-column prop="eq_key" label="EQ 配置" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="create_at" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEdit(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDelete(scope.row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 新增/编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
@close="handleDialogClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="品牌名称" prop="brand_name">
|
||||
<el-select
|
||||
v-model="formData.brand_name"
|
||||
placeholder="请选择品牌"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option
|
||||
v-for="brand in brandOptions"
|
||||
:key="brand.id"
|
||||
:label="brand.name"
|
||||
:value="brand.name"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="型号名称" prop="name">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入型号名称"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="形式" prop="form">
|
||||
<el-select
|
||||
v-model="formData.form"
|
||||
placeholder="请选择佩戴方式"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="入耳式" value="in-ear" />
|
||||
<el-option label="头戴式" value="over-ear" />
|
||||
<el-option label="耳塞式" value="earbud" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="阻抗" prop="rig">
|
||||
<el-input
|
||||
v-model="formData.rig"
|
||||
placeholder="例如:32Ω"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="来源" prop="source">
|
||||
<el-input
|
||||
v-model="formData.source"
|
||||
placeholder="型号来源"
|
||||
maxlength="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="EQ 键" prop="eq_key">
|
||||
<el-input
|
||||
v-model="formData.eq_key"
|
||||
placeholder="EQ 配置键值"
|
||||
maxlength="255"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Upload } from '@element-plus/icons-vue'
|
||||
import { getModels, createModel, updateModel, deleteModel, pushToMeilisearch } from '@/api/model'
|
||||
import { getBrands } from '@/api/brand'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const pushLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新型号')
|
||||
const formRef = ref(null)
|
||||
const brandOptions = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
const searchForm = reactive({
|
||||
brand_name: '',
|
||||
name: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const tableData = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
brand_name: '',
|
||||
name: '',
|
||||
form: '',
|
||||
rig: '',
|
||||
source: '',
|
||||
eq_key: ''
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
brand_name: [
|
||||
{ required: true, message: '请选择品牌名称', trigger: 'change' }
|
||||
],
|
||||
name: [
|
||||
{ required: true, message: '请输入型号名称', trigger: 'blur' },
|
||||
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 加载品牌列表
|
||||
const loadBrands = async () => {
|
||||
try {
|
||||
const res = await getBrands({ skip: 0, limit: 1000 })
|
||||
if (res.code === 1 && res.data) {
|
||||
brandOptions.value = res.data.items || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载品牌列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getModels({
|
||||
skip: (pagination.page - 1) * pagination.pageSize,
|
||||
limit: pagination.pageSize,
|
||||
brand_name: searchForm.brand_name || undefined,
|
||||
name: searchForm.name || undefined
|
||||
})
|
||||
|
||||
if (res.code === 1 && res.data) {
|
||||
tableData.value = res.data.items || []
|
||||
pagination.total = res.data.total || 0
|
||||
} else if (res.code === 2) {
|
||||
tableData.value = []
|
||||
pagination.total = 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
ElMessage.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
searchForm.brand_name = ''
|
||||
searchForm.name = ''
|
||||
pagination.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 新增
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = '新型号'
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑型号'
|
||||
formData.id = row.id
|
||||
formData.brand_name = row.brand_name
|
||||
formData.name = row.name
|
||||
formData.form = row.form || ''
|
||||
formData.rig = row.rig || ''
|
||||
formData.source = row.source || ''
|
||||
formData.eq_key = row.eq_key || ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row) => {
|
||||
ElMessageBox.confirm(
|
||||
`确定要删除型号 "${row.brand_name} - ${row.name}" 吗?`,
|
||||
'警告',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
try {
|
||||
submitLoading.value = true
|
||||
const res = await deleteModel(row.id)
|
||||
|
||||
if (res.code === 1) {
|
||||
ElMessage.success('删除成功')
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const api = formData.id ? updateModel : createModel
|
||||
const res = await api(formData)
|
||||
|
||||
if (res.code === 1) {
|
||||
ElMessage.success(formData.id ? '更新成功' : '创建成功')
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 对话框关闭
|
||||
const handleDialogClose = () => {
|
||||
formData.id = null
|
||||
formData.brand_name = ''
|
||||
formData.name = ''
|
||||
formData.form = ''
|
||||
formData.rig = ''
|
||||
formData.source = ''
|
||||
formData.eq_key = ''
|
||||
if (formRef.value) {
|
||||
formRef.value.resetFields()
|
||||
}
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handlePageChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 处理选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(row => row.id)
|
||||
}
|
||||
|
||||
// 推送到 Meilisearch
|
||||
const handlePushToSearch = () => {
|
||||
if (selectedIds.value.length === 0) {
|
||||
ElMessage.warning('请选择要推送的型号')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessageBox.confirm(
|
||||
`确定要将选中的 ${selectedIds.value.length} 个型号推送到搜索引擎吗?`,
|
||||
'推送确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
).then(async () => {
|
||||
pushLoading.value = true
|
||||
try {
|
||||
const res = await pushToMeilisearch(selectedIds.value)
|
||||
|
||||
if (res.code === 1) {
|
||||
ElMessage.success(`成功推送 ${res.data.pushed_count} 条数据到搜索引擎`)
|
||||
console.log('推送的数据:', res.data.models)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '推送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('推送失败:', error)
|
||||
ElMessage.error('推送失败')
|
||||
} finally {
|
||||
pushLoading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadBrands()
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.model-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
echo ================================================
|
||||
echo 前端项目 - 启动开发服务器
|
||||
echo ================================================
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo 启动开发服务器...
|
||||
echo.
|
||||
echo 访问地址:http://localhost:3000
|
||||
echo API 代理:http://localhost:8002
|
||||
echo.
|
||||
|
||||
pnpm dev
|
||||
|
||||
pause
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8002',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user