原型设计系统
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
99
DESIGN_PROMPTS.md
Normal file
99
DESIGN_PROMPTS.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# 产品设计系统 - 提示词 (Prompt) 约束指南
|
||||
|
||||
本指南旨在规范如何使用 AI 辅助生成符合 `Funtime Design System` 标准的代码和内容。本系统基于 **Vue 3 + TypeScript + Element Plus + Pinia** 构建。
|
||||
|
||||
## 1. 技术栈约束 (Tech Stack Constraints)
|
||||
|
||||
在生成代码时,请始终遵循以下技术栈版本和规范:
|
||||
|
||||
- **框架**: Vue 3 (Composition API, `<script setup lang="ts">`)
|
||||
- **语言**: TypeScript (严格模式)
|
||||
- **UI 组件库**: Element Plus
|
||||
- **状态管理**: Pinia
|
||||
- **构建工具**: Vite
|
||||
- **CSS 预处理**: CSS / SCSS (scoped)
|
||||
- **图标库**: `@element-plus/icons-vue`
|
||||
|
||||
## 2. 代码生成提示词模板 (Code Generation Prompts)
|
||||
|
||||
### 2.1 新增组件 (New Component)
|
||||
|
||||
当你需要 AI 生成一个新的 UI 组件时,请使用以下结构:
|
||||
|
||||
> **Role**: Senior Vue 3 Engineer
|
||||
> **Task**: Create a new component named `[ComponentName]`
|
||||
> **Requirements**:
|
||||
> 1. Use `<script setup lang="ts">`.
|
||||
> 2. Use **Element Plus** components for UI.
|
||||
> 3. Implement props interface definition using TypeScript.
|
||||
> 4. Use **Pinia** store if global state is needed (refer to `useDesignStore`).
|
||||
> 5. Ensure responsive design and proper styling (scoped CSS).
|
||||
> 6. Do NOT use `Options API`.
|
||||
> **Context**: This component is part of a Product Design System.
|
||||
|
||||
### 2.2 模拟数据 (Mock Data)
|
||||
|
||||
当需要填充数据时:
|
||||
|
||||
> **Task**: Generate mock data for `[FeatureName]`
|
||||
> **Format**: JSON or TypeScript array
|
||||
> **Constraint**:
|
||||
> - Use `vite-plugin-mock` structure if creating an API mock.
|
||||
> - Data should look realistic for a product design context (e.g., project names, status, user counts).
|
||||
> - Refer to `mock/index.ts` for existing patterns.
|
||||
|
||||
### 2.3 状态管理 (State Management)
|
||||
|
||||
当修改 Pinia Store 时:
|
||||
|
||||
> **Task**: Update `src/store/design.ts`
|
||||
> **Action**: Add state/actions for `[Feature]`
|
||||
> **Constraint**:
|
||||
> - Use Setup Store syntax (`defineStore('id', () => { ... })`).
|
||||
> - Keep state reactive using `ref` or `reactive`.
|
||||
> - Export a composable hook `use[StoreName]`.
|
||||
|
||||
## 3. 设计规范 (Design Guidelines)
|
||||
|
||||
- **布局**: 使用 `src/layout/index.vue` 作为主布局,包含侧边栏导航和顶部 Header。
|
||||
- **颜色**:
|
||||
- Primary: `#409eff` (Element Plus Default)
|
||||
- Background: `#f5f7fa`
|
||||
- **字体**: Helvetica Neue, Arial, sans-serif
|
||||
|
||||
## 4. 交互规范 (Interaction Rules)
|
||||
|
||||
- **反馈**: 所有的增删改操作必须有用户反馈 (使用 `ElMessage.success` 或 `ElMessage.error`)。
|
||||
- **加载状态**: 异步操作期间应展示 Loading 状态。
|
||||
- **表单验证**: 所有输入表单必须包含基本的非空验证。
|
||||
|
||||
## 5. 示例 (Example)
|
||||
|
||||
**请求**: "创建一个用户列表页面,包含搜索功能。"
|
||||
|
||||
**AI 应回复的代码结构**:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="user-list">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="header-actions">
|
||||
<el-input v-model="searchQuery" placeholder="Search users..." />
|
||||
<el-button type="primary">Add User</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-table :data="filteredUsers">
|
||||
<!-- Columns -->
|
||||
</el-table>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
// Imports...
|
||||
|
||||
// Logic...
|
||||
</script>
|
||||
```
|
||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Funtime Design System</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
20
mock/index.ts
Normal file
20
mock/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/api/stats',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: [
|
||||
{ label: 'Total Users', value: '1,234' },
|
||||
{ label: 'Active Projects', value: '56' },
|
||||
{ label: 'Design Assets', value: '892' },
|
||||
{ label: 'Pending Reviews', value: '12' }
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
] as MockMethod[]
|
||||
37
mock/modules.ts
Normal file
37
mock/modules.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/api/modules',
|
||||
method: 'get',
|
||||
response: () => {
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
title: 'Personnel Evaluation',
|
||||
description: 'Evaluate employee performance and manage reviews.',
|
||||
icon: 'User',
|
||||
path: '/personnel'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: 'Training',
|
||||
description: 'Manage training programs and track employee progress.',
|
||||
icon: 'School',
|
||||
path: '/training'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'Data Dashboard',
|
||||
description: 'View key performance indicators and analytics.',
|
||||
icon: 'DataLine',
|
||||
path: '/data-dashboard'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
] as MockMethod[]
|
||||
163
mock/training.ts
Normal file
163
mock/training.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { MockMethod } from 'vite-plugin-mock'
|
||||
|
||||
const courses = [
|
||||
{
|
||||
id: 1,
|
||||
title: '餐饮职业服务管理实操从入门到精通',
|
||||
chapters: 18,
|
||||
learners: 3,
|
||||
status: 0, // 0: 未学习, 1: 进行中, 2: 已学习
|
||||
cover: 'https://dummyimage.com/300x400/409eff/ffffff&text=Service+Management',
|
||||
category: 'personal_growth'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '企业前厅培训服务流程',
|
||||
chapters: 18,
|
||||
learners: 3,
|
||||
status: 1,
|
||||
cover: 'https://dummyimage.com/300x400/79bbff/ffffff&text=Front+Desk+Training',
|
||||
category: 'personal_growth'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '一本书搞懂餐厅经营管理',
|
||||
chapters: 18,
|
||||
learners: 3,
|
||||
status: 1,
|
||||
cover: 'https://dummyimage.com/300x400/337ecc/ffffff&text=Restaurant+Management',
|
||||
category: 'personal_growth'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '从零开始学习做餐饮管理之经营篇实操要点说明',
|
||||
chapters: 18,
|
||||
learners: 3,
|
||||
status: 2,
|
||||
cover: 'https://dummyimage.com/300x400/529b2e/ffffff&text=Zero+to+Hero',
|
||||
category: 'personal_growth'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '餐饮门店营销实战',
|
||||
chapters: 12,
|
||||
learners: 15,
|
||||
status: 0,
|
||||
cover: 'https://dummyimage.com/300x400/e6a23c/ffffff&text=Marketing',
|
||||
category: 'skill'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '食品安全与卫生管理',
|
||||
chapters: 8,
|
||||
learners: 42,
|
||||
status: 2,
|
||||
cover: 'https://dummyimage.com/300x400/f56c6c/ffffff&text=Safety',
|
||||
category: 'rules'
|
||||
}
|
||||
]
|
||||
|
||||
// Mock Detail Data
|
||||
const courseDetails = {
|
||||
1: {
|
||||
...courses[0],
|
||||
updateTime: '2025-10-20 23:24',
|
||||
totalTime: '4时 28分 38秒',
|
||||
views: 18390,
|
||||
completedChapters: 3,
|
||||
totalChapters: 20, // Override simple list count for detail realism
|
||||
outline: [
|
||||
{ id: 101, title: '第1章—基础入门课程UI/UX基础入门...', type: 'video', duration: '3分23秒', status: 2 },
|
||||
{ id: 102, title: '第2章—转战B端UI设计入门课程', type: 'audio', duration: '3分23秒', status: 2 },
|
||||
{ id: 103, title: '第3章—多风格插画设计赋能', type: 'doc', duration: '3分23秒', status: 2 },
|
||||
{ id: 104, title: '第4章—基础入门课程UI/UX基础入门...', type: 'video', duration: '暂未学习', status: 0 },
|
||||
{ id: 105, title: '第5章—进阶交互设计实战', type: 'video', duration: '15分00秒', status: 0 },
|
||||
{ id: 106, title: '第6章—设计系统搭建指南', type: 'doc', duration: '10页', status: 0 },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// Mock Chapter Detail Data
|
||||
const chapterDetails = {
|
||||
101: {
|
||||
id: 101,
|
||||
title: '第1章—基础入门课程UI/UX基础入门...',
|
||||
videoUrl: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
|
||||
content: '本章节主要介绍了UI/UX设计的基础概念,包括色彩理论、排版原则以及用户体验的核心要素。通过实际案例分析,帮助学员建立正确的设计思维。',
|
||||
comments: [
|
||||
{ id: 1, user: '范平', role: '设计师', content: '我体会到了不仅要在专业技能上有所提升用稻盛讲到“要用正确的人做我体会到了不仅要在专业技能上有所提升...', time: '1小时前', likes: 99 },
|
||||
{ id: 2, user: '赵婵涛', role: 'UI设计师', content: '灵感是业余者的专属,我们专业人士只要在早上打卡上班即可。', time: '1小时前', likes: 99 },
|
||||
{ id: 3, user: '巩香晓', role: '平面设计师', content: '永远不要跟别人比幸运,我从来没想过我比别人幸运,我也许比他们更有毅力,在最困难的时候,他们熬不住了,我可以多熬一秒钟。', time: '1小时前', likes: 99 }
|
||||
]
|
||||
},
|
||||
102: {
|
||||
id: 102,
|
||||
title: '第2章—转战B端UI设计入门课程',
|
||||
videoUrl: 'https://media.w3.org/2010/05/sintel/trailer.mp4',
|
||||
content: 'B端设计注重效率和逻辑。本章深入探讨B端产品的特点,如何构建清晰的信息架构,以及复杂表单和数据可视化的设计技巧。',
|
||||
comments: []
|
||||
}
|
||||
}
|
||||
|
||||
export default [
|
||||
{
|
||||
url: '/api/training/courses',
|
||||
method: 'get',
|
||||
response: ({ query }: { query: any }) => {
|
||||
const { category, type } = query
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: {
|
||||
list: courses,
|
||||
stats: {
|
||||
learned: 5,
|
||||
total: 15
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/api/training/course/detail',
|
||||
method: 'get',
|
||||
response: ({ query }: { query: any }) => {
|
||||
const { id } = query
|
||||
const numericId = Number(id)
|
||||
const detail = courseDetails[numericId as keyof typeof courseDetails] || {
|
||||
...courses.find(c => c.id === numericId) || courses[0],
|
||||
updateTime: '2025-10-20 23:24',
|
||||
totalTime: '2时 15分 00秒',
|
||||
views: 1205,
|
||||
completedChapters: 0,
|
||||
totalChapters: 18,
|
||||
outline: [
|
||||
{ id: 201, title: '第1章—课程介绍', type: 'video', duration: '5分00秒', status: 0 },
|
||||
{ id: 202, title: '第2章—基础知识', type: 'video', duration: '10分00秒', status: 0 }
|
||||
]
|
||||
}
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: detail
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
url: '/api/training/chapter/detail',
|
||||
method: 'get',
|
||||
response: ({ query }: { query: any }) => {
|
||||
const { id } = query
|
||||
const numericId = Number(id)
|
||||
// Default to chapter 101 if not found for demo purposes
|
||||
const detail = chapterDetails[numericId as keyof typeof chapterDetails] || chapterDetails[101]
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
message: 'ok',
|
||||
data: detail
|
||||
}
|
||||
}
|
||||
}
|
||||
] as MockMethod[]
|
||||
3477
package-lock.json
generated
Normal file
3477
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "funtime-design-system",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"axios": "^1.6.0",
|
||||
"element-plus": "^2.5.0",
|
||||
"pinia": "^2.1.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mockjs": "^1.0.10",
|
||||
"@types/node": "^20.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"sass": "^1.70.0",
|
||||
"typescript": "^5.2.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-plugin-mock": "^3.0.0",
|
||||
"vue-tsc": "^1.8.0"
|
||||
}
|
||||
}
|
||||
19
src/App.vue
Normal file
19
src/App.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#app {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
6
src/env.d.ts
vendored
Normal file
6
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
52
src/layout/index.vue
Normal file
52
src/layout/index.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside width="200px">
|
||||
<el-menu
|
||||
class="el-menu-vertical-demo"
|
||||
router
|
||||
:default-active="$route.path"
|
||||
>
|
||||
<el-menu-item index="/dashboard">
|
||||
<el-icon><Menu /></el-icon>
|
||||
<span>工作台</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header>
|
||||
<div class="header-content">
|
||||
<h2>Funtime Product Design</h2>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// Icons are registered globally in main.ts, but can also be imported if needed.
|
||||
// Using global registration for simplicity as done in main.ts
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
height: 100vh;
|
||||
}
|
||||
.el-aside {
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
}
|
||||
.el-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.header-content h2 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
}
|
||||
</style>
|
||||
20
src/main.ts
Normal file
20
src/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createApp } from 'vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import { createPinia } from 'pinia'
|
||||
import router from './router'
|
||||
import App from './App.vue'
|
||||
import './style.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
55
src/router/index.ts
Normal file
55
src/router/index.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
|
||||
import Layout from '../layout/index.vue'
|
||||
|
||||
const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/',
|
||||
component: Layout,
|
||||
redirect: '/dashboard',
|
||||
children: [
|
||||
{
|
||||
path: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/dashboard/index.vue'),
|
||||
meta: { title: '工作台' }
|
||||
},
|
||||
{
|
||||
path: 'personnel',
|
||||
name: 'Personnel',
|
||||
component: () => import('../views/personnel/index.vue'),
|
||||
meta: { title: 'Personnel Evaluation' }
|
||||
},
|
||||
{
|
||||
path: 'training',
|
||||
name: 'Training',
|
||||
component: () => import('../views/training/index.vue'),
|
||||
meta: { title: 'Training' }
|
||||
},
|
||||
{
|
||||
path: 'training/detail/:id',
|
||||
name: 'TrainingDetail',
|
||||
component: () => import('../views/training/detail.vue'),
|
||||
meta: { title: 'Course Detail' }
|
||||
},
|
||||
{
|
||||
path: 'training/study/:courseId/:chapterId',
|
||||
name: 'TrainingStudy',
|
||||
component: () => import('../views/training/study.vue'),
|
||||
meta: { title: 'Chapter Learning' }
|
||||
},
|
||||
{
|
||||
path: 'data-dashboard',
|
||||
name: 'DataDashboard',
|
||||
component: () => import('../views/data-dashboard/index.vue'),
|
||||
meta: { title: '数据工作台' }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
8
src/style.css
Normal file
8
src/style.css
Normal file
@@ -0,0 +1,8 @@
|
||||
:root {
|
||||
--el-color-primary: #409eff;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
120
src/views/dashboard/index.vue
Normal file
120
src/views/dashboard/index.vue
Normal file
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<h1 class="welcome-title">Welcome to Funtime Design System</h1>
|
||||
<p class="subtitle">Select a module to get started</p>
|
||||
|
||||
<el-row :gutter="24">
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="8" v-for="module in modules" :key="module.id">
|
||||
<el-card class="module-card" shadow="hover" @click="navigateTo(module.path)">
|
||||
<div class="module-icon">
|
||||
<component :is="module.icon" v-if="module.icon" />
|
||||
</div>
|
||||
<h3 class="module-title">{{ module.title }}</h3>
|
||||
<p class="module-desc">{{ module.description }}</p>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
|
||||
interface ModuleItem {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
icon: string
|
||||
path: string
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const modules = ref<ModuleItem[]>([])
|
||||
|
||||
const navigateTo = (path: string) => {
|
||||
router.push(path)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/modules')
|
||||
if (res.data.code === 0) {
|
||||
modules.value = res.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch modules:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
padding: 40px 20px;
|
||||
background-color: #f5f7fa;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.welcome-title {
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
font-size: 16px;
|
||||
color: #909399;
|
||||
}
|
||||
.module-card {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
height: 100%;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.module-card:hover {
|
||||
transform: translateY(-8px);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
.icon-wrapper {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background-color: #ecf5ff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 36px;
|
||||
color: #409eff;
|
||||
margin-bottom: 24px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.module-card:hover .icon-wrapper {
|
||||
background-color: #409eff;
|
||||
color: #ffffff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.module-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
.module-desc {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
78
src/views/data-dashboard/index.vue
Normal file
78
src/views/data-dashboard/index.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>数据工作台</h1>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" v-for="item in stats" :key="item.label">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card-value">{{ item.value }}</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="12">
|
||||
<el-card header="Recent Activity">
|
||||
<el-timeline>
|
||||
<el-timeline-item timestamp="2024/04/12" placement="top">
|
||||
<el-card>
|
||||
<h4>Update Design System</h4>
|
||||
<p>Tom committed 2024/04/12 20:46</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item timestamp="2024/04/03" placement="top">
|
||||
<el-card>
|
||||
<h4>New Component Added</h4>
|
||||
<p>Tom committed 2024/04/03 20:46</p>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card header="Quick Actions">
|
||||
<el-button type="primary">Create New Project</el-button>
|
||||
<el-button>Import Assets</el-button>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
|
||||
interface StatItem {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const stats = ref<StatItem[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/stats')
|
||||
if (res.data.code === 0) {
|
||||
stats.value = res.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats:', error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #409eff;
|
||||
}
|
||||
.card-header {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
15
src/views/personnel/index.vue
Normal file
15
src/views/personnel/index.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<h1>Personnel Evaluation</h1>
|
||||
<p>Welcome to the Personnel Evaluation module.</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
354
src/views/training/detail.vue
Normal file
354
src/views/training/detail.vue
Normal file
@@ -0,0 +1,354 @@
|
||||
<template>
|
||||
<div class="course-detail-container" v-loading="loading">
|
||||
<div class="back-nav">
|
||||
<el-page-header @back="goBack" title="课程详情" />
|
||||
</div>
|
||||
|
||||
<!-- Top Section: Course Info -->
|
||||
<div class="course-header-card">
|
||||
<div class="header-content">
|
||||
<div class="course-cover-wrapper">
|
||||
<img :src="course.cover" class="course-cover" v-if="course.cover" />
|
||||
</div>
|
||||
<div class="course-info-wrapper">
|
||||
<h1 class="course-title">{{ course.title }}</h1>
|
||||
<p class="update-time">更新时间:{{ course.updateTime }}</p>
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ course.totalTime }}</div>
|
||||
<div class="stat-label">累计学习</div>
|
||||
</div>
|
||||
<el-divider direction="vertical" class="stat-divider" />
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ course.views }}</div>
|
||||
<div class="stat-label">浏览次数</div>
|
||||
</div>
|
||||
<el-divider direction="vertical" class="stat-divider" />
|
||||
<div class="stat-item">
|
||||
<div class="stat-value highlight">
|
||||
{{ course.completedChapters }}<span class="total">/{{ course.totalChapters }}</span>
|
||||
</div>
|
||||
<div class="stat-label">已学/总章节</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<el-button type="primary" size="large" class="start-btn" @click="startLearning">
|
||||
{{ course.completedChapters > 0 ? '继续学习' : '开始学习' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content: Tabs -->
|
||||
<div class="course-content-card">
|
||||
<el-tabs v-model="activeTab" class="course-tabs">
|
||||
<el-tab-pane label="大纲" name="outline">
|
||||
<div class="outline-list">
|
||||
<div
|
||||
v-for="chapter in course.outline"
|
||||
:key="chapter.id"
|
||||
class="chapter-item"
|
||||
:class="{ 'is-completed': chapter.status === 2 }"
|
||||
@click="goToStudy(chapter.id)"
|
||||
>
|
||||
<div class="chapter-icon-wrapper">
|
||||
<el-icon class="chapter-icon" v-if="chapter.type === 'video'"><VideoPlay /></el-icon>
|
||||
<el-icon class="chapter-icon" v-else-if="chapter.type === 'audio'"><Headset /></el-icon>
|
||||
<el-icon class="chapter-icon" v-else><Document /></el-icon>
|
||||
</div>
|
||||
<div class="chapter-info">
|
||||
<div class="chapter-title">{{ chapter.title }}</div>
|
||||
<div class="chapter-meta">
|
||||
<span v-if="chapter.status === 0">暂未学习</span>
|
||||
<span v-else>学习时长: {{ chapter.duration }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chapter-status">
|
||||
<el-tag v-if="chapter.status === 2" type="success" effect="plain" round>已学完</el-tag>
|
||||
<el-tag v-else-if="chapter.status === 1" type="warning" effect="plain" round>进行中</el-tag>
|
||||
<el-button v-else link type="primary">开始</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="评论" name="reviews">
|
||||
<el-empty description="暂无评论" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { VideoPlay, Document, Headset } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('outline')
|
||||
|
||||
const course = ref<any>({
|
||||
title: '',
|
||||
cover: '',
|
||||
updateTime: '',
|
||||
totalTime: '',
|
||||
views: 0,
|
||||
completedChapters: 0,
|
||||
totalChapters: 0,
|
||||
outline: []
|
||||
})
|
||||
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
const goToStudy = (chapterId: number) => {
|
||||
router.push(`/training/study/${route.params.id}/${chapterId}`)
|
||||
}
|
||||
|
||||
const startLearning = () => {
|
||||
if (course.value.outline && course.value.outline.length > 0) {
|
||||
// Find first uncompleted or just first chapter
|
||||
const nextChapter = course.value.outline.find((c: any) => c.status !== 2) || course.value.outline[0]
|
||||
goToStudy(nextChapter.id)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await axios.get('/api/training/course/detail', {
|
||||
params: { id: route.params.id }
|
||||
})
|
||||
if (res.data.code === 0) {
|
||||
course.value = res.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch course detail:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.course-detail-container {
|
||||
padding: 20px 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.back-nav {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Header Card */
|
||||
.course-header-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 24px;
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
|
||||
.course-cover-wrapper {
|
||||
flex-shrink: 0;
|
||||
width: 240px;
|
||||
height: 320px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
||||
|
||||
.course-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.course-info-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.course-title {
|
||||
font-size: 28px;
|
||||
color: #303133;
|
||||
margin: 0 0 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.update-time {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.stat-divider {
|
||||
height: 40px;
|
||||
margin: 0 40px;
|
||||
border-color: #ebeef5;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.highlight {
|
||||
color: #409eff;
|
||||
.total {
|
||||
color: #303133;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-row {
|
||||
margin-top: auto;
|
||||
|
||||
.start-btn {
|
||||
width: 180px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Content Card */
|
||||
.course-content-card {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px 30px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.course-tabs {
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 18px;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.outline-list {
|
||||
margin-top: 10px;
|
||||
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #f9fafe;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
margin: 0 -10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.chapter-icon-wrapper {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
background-color: #ecf5ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.chapter-icon {
|
||||
font-size: 24px;
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-info {
|
||||
flex: 1;
|
||||
|
||||
.chapter-title {
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.chapter-meta {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-status {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.course-header-card .header-content {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.course-cover-wrapper {
|
||||
width: 180px;
|
||||
height: 240px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.course-info-wrapper {
|
||||
width: 100%;
|
||||
|
||||
.stats-row {
|
||||
justify-content: center;
|
||||
.stat-divider { margin: 0 15px; }
|
||||
}
|
||||
|
||||
.action-row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
464
src/views/training/index.vue
Normal file
464
src/views/training/index.vue
Normal file
@@ -0,0 +1,464 @@
|
||||
<template>
|
||||
<div class="training-container">
|
||||
<!-- Left Sidebar: Categories -->
|
||||
<div class="training-sidebar">
|
||||
<h2 class="sidebar-title">课程学习</h2>
|
||||
<el-menu
|
||||
default-active="all"
|
||||
class="category-menu"
|
||||
@select="handleCategorySelect"
|
||||
>
|
||||
<el-menu-item index="all">
|
||||
<span>全部</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="personal_growth">
|
||||
<span>个人成长</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="rules">
|
||||
<span>规章制度</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="skills">
|
||||
<span>技能项</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="team">
|
||||
<span>团队提升与管理</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
|
||||
<!-- Right Main Content -->
|
||||
<div class="training-main">
|
||||
<!-- Top Bar: Search and Tabs -->
|
||||
<div class="top-bar">
|
||||
<div class="search-wrapper">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="请输入课程名称"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
class="course-search"
|
||||
/>
|
||||
</div>
|
||||
<div class="tabs-wrapper">
|
||||
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
|
||||
<el-tab-pane label="通用课程" name="general" />
|
||||
<el-tab-pane label="本岗课程" name="position" />
|
||||
<el-tab-pane label="它岗课程" name="other" />
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stats Bar -->
|
||||
<div class="stats-bar">
|
||||
<div class="stats-left">
|
||||
<el-icon class="bell-icon"><Bell /></el-icon>
|
||||
<span class="stats-text">
|
||||
已学课程 <span class="highlight">{{ stats.learned }}</span> | 全部 {{ stats.total }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Course List (Grid) -->
|
||||
<div class="course-list" v-loading="loading">
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="course in filteredCourses"
|
||||
:key="course.id"
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
:xl="6"
|
||||
>
|
||||
<el-card class="course-card" :body-style="{ padding: '0px' }" shadow="hover" @click="goToDetail(course.id)">
|
||||
<div class="course-cover">
|
||||
<img :src="course.cover" class="image" />
|
||||
<div class="chapter-badge">共{{ course.chapters }}章节</div>
|
||||
<div class="status-tag" :class="getStatusClass(course.status)">
|
||||
{{ getStatusText(course.status) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="course-info">
|
||||
<h3 class="course-title" :title="course.title">{{ course.title }}</h3>
|
||||
<div class="course-meta">
|
||||
<span class="learners">学习总人数 {{ course.learners }}</span>
|
||||
<el-button type="primary" link v-if="course.status === 1">继续学习</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Action Bar (Desktop optimized as floating or fixed bottom) -->
|
||||
<div class="continue-learning-bar" v-if="lastActiveCourse">
|
||||
<div class="cl-info">
|
||||
<img :src="lastActiveCourse.cover" class="cl-cover" />
|
||||
<div class="cl-text">
|
||||
<div class="cl-title">{{ lastActiveCourse.title }}</div>
|
||||
<div class="cl-progress">已学 70%</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-button type="primary" round>继续学习</el-button>
|
||||
<el-icon class="close-bar"><Close /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { Search, Bell, Close } from '@element-plus/icons-vue'
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
// Interfaces
|
||||
interface Course {
|
||||
id: number
|
||||
title: string
|
||||
chapters: number
|
||||
learners: number
|
||||
status: number // 0: Not Started, 1: In Progress, 2: Completed
|
||||
cover: string
|
||||
category: string
|
||||
}
|
||||
|
||||
interface Stats {
|
||||
learned: number
|
||||
total: number
|
||||
}
|
||||
|
||||
// State
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const activeTab = ref('general')
|
||||
const activeCategory = ref('all')
|
||||
const courses = ref<Course[]>([])
|
||||
const stats = ref<Stats>({ learned: 0, total: 0 })
|
||||
|
||||
// Derived State
|
||||
const filteredCourses = computed(() => {
|
||||
return courses.value.filter(course => {
|
||||
// Filter by search
|
||||
const matchSearch = course.title.toLowerCase().includes(searchQuery.value.toLowerCase())
|
||||
// Filter by category (mock logic)
|
||||
const matchCategory = activeCategory.value === 'all' || course.category === activeCategory.value
|
||||
// Filter by tab (mock logic - assuming tabs might filter by type, but here we just show all for demo unless we add type to mock)
|
||||
// For now, we assume all courses belong to 'general' or match current tab logic if we had that data
|
||||
|
||||
return matchSearch && matchCategory
|
||||
})
|
||||
})
|
||||
|
||||
const lastActiveCourse = computed(() => {
|
||||
return courses.value.find(c => c.status === 1) || courses.value[0]
|
||||
})
|
||||
|
||||
// Methods
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await axios.get('/api/training/courses', {
|
||||
params: {
|
||||
category: activeCategory.value,
|
||||
type: activeTab.value
|
||||
}
|
||||
})
|
||||
if (res.data.code === 0) {
|
||||
courses.value = res.data.data.list
|
||||
stats.value = res.data.data.stats
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch courses:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleCategorySelect = (index: string) => {
|
||||
activeCategory.value = index
|
||||
// fetchData() // Uncomment if backend supports filtering
|
||||
}
|
||||
|
||||
const handleTabClick = () => {
|
||||
// fetchData() // Uncomment if backend supports filtering
|
||||
}
|
||||
|
||||
const getStatusText = (status: number) => {
|
||||
switch (status) {
|
||||
case 0: return '未学习'
|
||||
case 1: return '进行中'
|
||||
case 2: return '已学习'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusClass = (status: number) => {
|
||||
switch (status) {
|
||||
case 0: return 'status-not-started'
|
||||
case 1: return 'status-in-progress'
|
||||
case 2: return 'status-completed'
|
||||
default: return ''
|
||||
}
|
||||
}
|
||||
|
||||
const goToDetail = (id: number) => {
|
||||
router.push(`/training/detail/${id}`)
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.training-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 80px); /* Adjust based on global layout header height */
|
||||
background-color: #f5f7fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.training-sidebar {
|
||||
width: 240px;
|
||||
background-color: #fff;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 0;
|
||||
|
||||
.sidebar-title {
|
||||
padding: 0 20px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.category-menu {
|
||||
border-right: none;
|
||||
|
||||
:deep(.el-menu-item) {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 15px;
|
||||
|
||||
&.is-active {
|
||||
background-color: #ecf5ff;
|
||||
border-right: 3px solid #409eff;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
.training-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px 30px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-wrapper {
|
||||
max-width: 400px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
height: 1px;
|
||||
background-color: #e4e7ed;
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
|
||||
.stats-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #606266;
|
||||
|
||||
.bell-icon {
|
||||
font-size: 20px;
|
||||
color: #409eff;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.stats-text {
|
||||
font-size: 14px;
|
||||
.highlight {
|
||||
color: #303133;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-list {
|
||||
padding-bottom: 80px; /* Space for bottom bar */
|
||||
}
|
||||
|
||||
.course-card {
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
transition: transform 0.3s;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.course-cover {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.chapter-badge {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
background: rgba(0,0,0,0.6);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
||||
&.status-not-started {
|
||||
background-color: #fdf6ec;
|
||||
color: #e6a23c;
|
||||
}
|
||||
&.status-in-progress {
|
||||
background-color: #ecf5ff;
|
||||
color: #409eff;
|
||||
}
|
||||
&.status-completed {
|
||||
background-color: #f0f9eb;
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.course-info {
|
||||
padding: 14px;
|
||||
|
||||
.course-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
line-height: 1.4;
|
||||
height: 44px; /* 2 lines */
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.course-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.continue-learning-bar {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
width: 360px;
|
||||
background-color: rgba(50, 50, 50, 0.9);
|
||||
backdrop-filter: blur(10px);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
||||
z-index: 100;
|
||||
|
||||
.cl-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-right: 15px;
|
||||
|
||||
.cl-cover {
|
||||
width: 40px;
|
||||
height: 50px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.cl-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.cl-title {
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.cl-progress {
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-bar {
|
||||
margin-left: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
color: #909399;
|
||||
&:hover { color: #fff; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
484
src/views/training/study.vue
Normal file
484
src/views/training/study.vue
Normal file
@@ -0,0 +1,484 @@
|
||||
<template>
|
||||
<div class="study-container" v-loading="loading">
|
||||
<!-- Breadcrumb -->
|
||||
<div class="breadcrumb-nav">
|
||||
<el-page-header @back="goBack" title="返回">
|
||||
<template #breadcrumb>
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item :to="{ path: '/training' }">培训课程</el-breadcrumb-item>
|
||||
<el-breadcrumb-item :to="{ path: `/training/detail/${route.params.courseId}` }">{{ course.title || '课程详情' }}</el-breadcrumb-item>
|
||||
<el-breadcrumb-item>章节学习</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</template>
|
||||
<template #content>
|
||||
<span class="page-header-content"> 章节学习 </span>
|
||||
</template>
|
||||
</el-page-header>
|
||||
</div>
|
||||
|
||||
<div class="study-content">
|
||||
<el-row :gutter="24">
|
||||
<!-- Left: Main Learning Area -->
|
||||
<el-col :xs="24" :sm="24" :md="16" :lg="17" :xl="18">
|
||||
<div class="video-section">
|
||||
<div class="video-player-wrapper">
|
||||
<video
|
||||
v-if="currentChapter.videoUrl"
|
||||
:src="currentChapter.videoUrl"
|
||||
controls
|
||||
class="video-player"
|
||||
poster="https://dummyimage.com/800x450/000/fff&text=Video+Player"
|
||||
></video>
|
||||
<div v-else class="video-placeholder">
|
||||
<el-empty description="暂无视频资源" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="chapter-header">
|
||||
<h1 class="chapter-title">{{ currentChapter.title }}</h1>
|
||||
<div class="chapter-actions">
|
||||
<el-button type="primary" link icon="Share">分享</el-button>
|
||||
<el-button type="primary" link icon="Star">收藏</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-tabs-wrapper">
|
||||
<el-tabs v-model="activeTab" class="content-tabs">
|
||||
<el-tab-pane label="章节介绍" name="intro">
|
||||
<div class="chapter-intro">
|
||||
<p>{{ currentChapter.content || '暂无介绍' }}</p>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="评论" name="reviews">
|
||||
<div class="reviews-list">
|
||||
<div v-if="currentChapter.comments && currentChapter.comments.length > 0">
|
||||
<div v-for="comment in currentChapter.comments" :key="comment.id" class="review-item">
|
||||
<div class="review-avatar">
|
||||
<el-avatar :size="40" :src="comment.avatar || 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'" />
|
||||
</div>
|
||||
<div class="review-content">
|
||||
<div class="review-header">
|
||||
<span class="user-name">{{ comment.user }}</span>
|
||||
<span class="user-role">{{ comment.role }}</span>
|
||||
</div>
|
||||
<div class="review-text">{{ comment.content }}</div>
|
||||
<div class="review-footer">
|
||||
<span class="review-time">{{ comment.time }}</span>
|
||||
<div class="review-actions">
|
||||
<el-button link size="small" icon="Star">{{ comment.likes || 0 }}</el-button>
|
||||
<el-button link size="small" icon="ChatDotRound">{{ comment.replies || 0 }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-empty v-else description="暂无评论,快来抢沙发吧!" />
|
||||
|
||||
<!-- Comment Input -->
|
||||
<div class="comment-input-area">
|
||||
<el-input
|
||||
v-model="newComment"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="友善评论,文明发言"
|
||||
resize="none"
|
||||
/>
|
||||
<div class="comment-submit">
|
||||
<el-button type="primary" size="small" @click="submitComment">发表评论</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-col>
|
||||
|
||||
<!-- Right: Chapter Navigation -->
|
||||
<el-col :xs="24" :sm="24" :md="8" :lg="7" :xl="6">
|
||||
<div class="chapter-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3>课程目录</h3>
|
||||
<span class="progress-text">已完成 {{ course.completedChapters }}/{{ course.totalChapters }}</span>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<div
|
||||
v-for="chapter in course.outline"
|
||||
:key="chapter.id"
|
||||
class="sidebar-chapter-item"
|
||||
:class="{ 'is-active': chapter.id === Number(route.params.chapterId), 'is-completed': chapter.status === 2 }"
|
||||
@click="switchChapter(chapter.id)"
|
||||
>
|
||||
<div class="sidebar-chapter-icon">
|
||||
<el-icon v-if="chapter.status === 2"><CircleCheckFilled /></el-icon>
|
||||
<el-icon v-else-if="chapter.id === Number(route.params.chapterId)"><VideoPlay /></el-icon>
|
||||
<el-icon v-else><CircleCheck /></el-icon>
|
||||
</div>
|
||||
<div class="sidebar-chapter-info">
|
||||
<div class="sidebar-chapter-title">{{ chapter.title }}</div>
|
||||
<div class="sidebar-chapter-duration">{{ chapter.duration }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VideoPlay, CircleCheck, CircleCheckFilled, Share, Star, ChatDotRound } from '@element-plus/icons-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('reviews') // Default to reviews as per screenshot implication or user pref
|
||||
const newComment = ref('')
|
||||
|
||||
const course = ref<any>({
|
||||
title: '',
|
||||
outline: [],
|
||||
completedChapters: 0,
|
||||
totalChapters: 0
|
||||
})
|
||||
|
||||
const currentChapter = ref<any>({
|
||||
title: '',
|
||||
videoUrl: '',
|
||||
content: '',
|
||||
comments: []
|
||||
})
|
||||
|
||||
const fetchCourseDetail = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/training/course/detail', {
|
||||
params: { id: route.params.courseId }
|
||||
})
|
||||
if (res.data.code === 0) {
|
||||
course.value = res.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch course detail:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchChapterDetail = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await axios.get('/api/training/chapter/detail', {
|
||||
params: { id: route.params.chapterId }
|
||||
})
|
||||
if (res.data.code === 0) {
|
||||
currentChapter.value = res.data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch chapter detail:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const switchChapter = (chapterId: number) => {
|
||||
if (chapterId === Number(route.params.chapterId)) return
|
||||
router.push(`/training/study/${route.params.courseId}/${chapterId}`)
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.push(`/training/detail/${route.params.courseId}`)
|
||||
}
|
||||
|
||||
const submitComment = () => {
|
||||
if (!newComment.value.trim()) {
|
||||
ElMessage.warning('请输入评论内容')
|
||||
return
|
||||
}
|
||||
// Mock submission
|
||||
currentChapter.value.comments.unshift({
|
||||
id: Date.now(),
|
||||
user: '我',
|
||||
role: '学员',
|
||||
content: newComment.value,
|
||||
time: '刚刚',
|
||||
likes: 0
|
||||
})
|
||||
newComment.value = ''
|
||||
ElMessage.success('评论发表成功')
|
||||
}
|
||||
|
||||
watch(() => route.params.chapterId, () => {
|
||||
fetchChapterDetail()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchCourseDetail()
|
||||
fetchChapterDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.study-container {
|
||||
padding: 20px 40px;
|
||||
max-width: 1400px; /* Wider for study view */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb-nav {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header-content {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
/* Video Section */
|
||||
.video-section {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 20px;
|
||||
|
||||
.video-player-wrapper {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: #000;
|
||||
|
||||
.video-player {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-header {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
.chapter-title {
|
||||
font-size: 20px;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.chapter-actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Content Tabs */
|
||||
.content-tabs-wrapper {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
min-height: 300px;
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.chapter-intro {
|
||||
padding: 10px 0;
|
||||
line-height: 1.6;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* Reviews */
|
||||
.reviews-list {
|
||||
padding-top: 10px;
|
||||
|
||||
.review-item {
|
||||
display: flex;
|
||||
padding: 20px 0;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.review-avatar {
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.review-content {
|
||||
flex: 1;
|
||||
|
||||
.review-header {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
color: #303133;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
background: #f4f4f5;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.review-text {
|
||||
color: #606266;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.review-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.review-time {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.review-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-input-area {
|
||||
margin-top: 20px;
|
||||
background: #f9fafe;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
|
||||
.comment-submit {
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.chapter-sidebar {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
|
||||
position: sticky;
|
||||
top: 20px;
|
||||
|
||||
.sidebar-header {
|
||||
padding: 15px 20px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
max-height: calc(100vh - 200px);
|
||||
overflow-y: auto;
|
||||
|
||||
.sidebar-chapter-item {
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
transition: all 0.3s;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #ecf5ff;
|
||||
border-left-color: #409eff;
|
||||
|
||||
.sidebar-chapter-title {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.sidebar-chapter-icon {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-chapter-icon {
|
||||
margin-right: 12px;
|
||||
color: #c0c4cc;
|
||||
font-size: 18px;
|
||||
margin-top: 2px;
|
||||
|
||||
&.is-completed {
|
||||
color: #67c23a;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-chapter-info {
|
||||
flex: 1;
|
||||
|
||||
.sidebar-chapter-title {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.sidebar-chapter-duration {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.study-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.chapter-sidebar {
|
||||
margin-top: 20px;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": ["vite/client", "element-plus/global"]
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||
}
|
||||
14
vite.config.ts
Normal file
14
vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { viteMockServe } from 'vite-plugin-mock'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
viteMockServe({
|
||||
mockPath: 'mock',
|
||||
enable: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user