Compare commits
4 Commits
0e5e6481ef
...
98fc8b8eea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98fc8b8eea | ||
|
|
f52c8fde10 | ||
|
|
22241fb5b6 | ||
|
|
e406110af2 |
394
.drone.yml
394
.drone.yml
@@ -1,138 +1,256 @@
|
|||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: deploy-test
|
name: deploy-test
|
||||||
|
|
||||||
# 测试环境:test 分支触发,部署到生产服务器的开发环境
|
# 测试环境:test 分支触发,部署到 kpl
|
||||||
trigger:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
- test
|
- test
|
||||||
event:
|
event:
|
||||||
- push
|
- push
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: sync-code
|
- name: sync-code
|
||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
settings:
|
settings:
|
||||||
host: 120.79.247.16
|
host: 120.79.247.16
|
||||||
username: root
|
username: root
|
||||||
password:
|
password:
|
||||||
from_secret: prod_ssh_password
|
from_secret: prod_ssh_password
|
||||||
port: 22
|
port: 22
|
||||||
command_timeout: 5m
|
command_timeout: 5m
|
||||||
script:
|
script:
|
||||||
- echo "=== 同步代码到测试环境 ==="
|
- echo "=== [测试环境] 同步代码 ==="
|
||||||
- cd /root/aiedu
|
- cd /root/aiedu
|
||||||
- git fetch origin
|
- git fetch origin
|
||||||
- git checkout test 2>/dev/null || git checkout -b test origin/test
|
- git checkout test 2>/dev/null || git checkout -b test origin/test
|
||||||
- git reset --hard origin/test
|
- git reset --hard origin/test
|
||||||
- echo "代码同步完成"
|
- echo "代码同步完成"
|
||||||
|
|
||||||
- name: rebuild-dev
|
- name: build-frontend-test
|
||||||
image: appleboy/drone-ssh
|
image: appleboy/drone-ssh
|
||||||
settings:
|
settings:
|
||||||
host: 120.79.247.16
|
host: 120.79.247.16
|
||||||
username: root
|
username: root
|
||||||
password:
|
password:
|
||||||
from_secret: prod_ssh_password
|
from_secret: prod_ssh_password
|
||||||
port: 22
|
port: 22
|
||||||
command_timeout: 10m
|
command_timeout: 10m
|
||||||
script:
|
script:
|
||||||
- echo "=== 重建测试环境 ==="
|
- echo "=== [测试环境] 编译前端到 dist-test ==="
|
||||||
- cd /root/aiedu
|
- cd /root/aiedu/kaopeilian-frontend
|
||||||
- docker-compose -f docker-compose.dev.yml up -d --build
|
- npm install --silent
|
||||||
- sleep 5
|
- npm run build
|
||||||
- docker ps | grep kpl-
|
- rm -rf /root/aiedu/dist-test/*
|
||||||
- echo "=== 测试环境部署完成: https://kpl.ireborn.com.cn ==="
|
- cp -r dist/* /root/aiedu/dist-test/
|
||||||
|
- echo "前端编译完成 -> dist-test"
|
||||||
---
|
|
||||||
kind: pipeline
|
- name: restart-backend
|
||||||
type: docker
|
image: appleboy/drone-ssh
|
||||||
name: deploy-prod
|
settings:
|
||||||
|
host: 120.79.247.16
|
||||||
# 生产环境:main 分支触发,批量更新所有租户
|
username: root
|
||||||
trigger:
|
password:
|
||||||
branch:
|
from_secret: prod_ssh_password
|
||||||
- main
|
port: 22
|
||||||
event:
|
command_timeout: 5m
|
||||||
- push
|
script:
|
||||||
|
- echo "=== [测试环境] 重启后端 ==="
|
||||||
steps:
|
- docker restart kpl-backend-dev
|
||||||
- name: sync-code-to-server
|
- sleep 5
|
||||||
image: appleboy/drone-ssh
|
- docker ps | grep kpl-
|
||||||
settings:
|
- echo "=== 测试环境部署完成: https://kpl.ireborn.com.cn ==="
|
||||||
host: 120.79.247.16
|
|
||||||
username: root
|
---
|
||||||
password:
|
kind: pipeline
|
||||||
from_secret: prod_ssh_password
|
type: docker
|
||||||
port: 22
|
name: deploy-staging
|
||||||
command_timeout: 10m
|
|
||||||
script:
|
# 预生产环境:staging 分支触发,部署到 aiedu
|
||||||
- echo "=== 同步代码到生产服务器 ==="
|
trigger:
|
||||||
- cd /root/aiedu
|
branch:
|
||||||
- git fetch origin
|
- staging
|
||||||
- git reset --hard origin/main
|
event:
|
||||||
- echo "代码同步完成"
|
- push
|
||||||
|
|
||||||
- name: build-frontend
|
steps:
|
||||||
image: appleboy/drone-ssh
|
- name: sync-code
|
||||||
settings:
|
image: appleboy/drone-ssh
|
||||||
host: 120.79.247.16
|
settings:
|
||||||
username: root
|
host: 120.79.247.16
|
||||||
password:
|
username: root
|
||||||
from_secret: prod_ssh_password
|
password:
|
||||||
port: 22
|
from_secret: prod_ssh_password
|
||||||
command_timeout: 10m
|
port: 22
|
||||||
script:
|
command_timeout: 5m
|
||||||
- echo "=== 编译前端(所有租户共享)==="
|
script:
|
||||||
- cd /root/aiedu/kaopeilian-frontend
|
- echo "=== [预生产] 同步代码 ==="
|
||||||
- npm install --silent
|
- cd /root/aiedu
|
||||||
- npm run build
|
- git fetch origin
|
||||||
- echo "前端编译完成,所有租户已更新"
|
- git checkout staging 2>/dev/null || git checkout -b staging origin/staging
|
||||||
|
- git reset --hard origin/staging
|
||||||
- name: rebuild-backends
|
- echo "代码同步完成"
|
||||||
image: appleboy/drone-ssh
|
|
||||||
settings:
|
- name: build-frontend-staging
|
||||||
host: 120.79.247.16
|
image: appleboy/drone-ssh
|
||||||
username: root
|
settings:
|
||||||
password:
|
host: 120.79.247.16
|
||||||
from_secret: prod_ssh_password
|
username: root
|
||||||
port: 22
|
password:
|
||||||
command_timeout: 15m
|
from_secret: prod_ssh_password
|
||||||
script:
|
port: 22
|
||||||
- echo "=== 重建所有后端服务 ==="
|
command_timeout: 10m
|
||||||
- cd /root/aiedu
|
script:
|
||||||
- docker-compose -f docker-compose.prod-multi.yml up -d --build --no-deps hua-backend yy-backend hl-backend xy-backend fw-backend ex-backend cxw-backend
|
- echo "=== [预生产] 编译前端到 dist-staging ==="
|
||||||
- sleep 10
|
- cd /root/aiedu/kaopeilian-frontend
|
||||||
- docker ps | grep backend
|
- npm install --silent
|
||||||
- echo "=== 生产环境批量更新完成 ==="
|
- npm run build
|
||||||
- echo "租户列表: hua, yy, hl, xy, fw, ex, cxw"
|
- rm -rf /root/aiedu/dist-staging/*
|
||||||
|
- cp -r dist/* /root/aiedu/dist-staging/
|
||||||
volumes:
|
- echo "前端编译完成 -> dist-staging"
|
||||||
- name: docker-sock
|
|
||||||
host:
|
- name: restart-backend
|
||||||
path: /var/run/docker.sock
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
---
|
host: 120.79.247.16
|
||||||
kind: pipeline
|
username: root
|
||||||
type: docker
|
password:
|
||||||
name: code-check
|
from_secret: prod_ssh_password
|
||||||
|
port: 22
|
||||||
trigger:
|
command_timeout: 5m
|
||||||
event:
|
script:
|
||||||
- push
|
- echo "=== [预生产] 重启后端 ==="
|
||||||
- pull_request
|
- docker restart kaopeilian-backend
|
||||||
|
- sleep 5
|
||||||
steps:
|
- docker ps | grep kaopeilian-
|
||||||
- name: python-lint
|
- echo "=== 预生产部署完成: https://aiedu.ireborn.com.cn ==="
|
||||||
image: python:3.9-slim
|
|
||||||
commands:
|
---
|
||||||
- cd backend
|
kind: pipeline
|
||||||
- pip install flake8 -q
|
type: docker
|
||||||
- flake8 app --count --select=E9,F63,F7,F82 --show-source --statistics || true
|
name: deploy-prod
|
||||||
- echo "Python lint completed"
|
|
||||||
|
# 生产环境:main 分支触发,部署到所有租户
|
||||||
- name: frontend-check
|
#
|
||||||
image: node:18-alpine
|
# 使用方法:
|
||||||
commands:
|
# git commit -m "feat: xxx [all]" - 部署所有租户
|
||||||
- cd frontend
|
# git commit -m "feat: xxx [hua]" - 仅部署 hua
|
||||||
- echo "Frontend check completed"
|
# git commit -m "feat: xxx [cxw,yy,hl]" - 部署指定多个租户
|
||||||
|
# git commit -m "feat: xxx" - 默认部署所有租户
|
||||||
|
#
|
||||||
|
trigger:
|
||||||
|
branch:
|
||||||
|
- main
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: sync-code-to-server
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
|
host: 120.79.247.16
|
||||||
|
username: root
|
||||||
|
password:
|
||||||
|
from_secret: prod_ssh_password
|
||||||
|
port: 22
|
||||||
|
command_timeout: 10m
|
||||||
|
script:
|
||||||
|
- echo "=== [生产] 同步代码 ==="
|
||||||
|
- cd /root/aiedu
|
||||||
|
- git fetch origin
|
||||||
|
- git reset --hard origin/main
|
||||||
|
- echo "代码同步完成"
|
||||||
|
|
||||||
|
- name: build-frontend-prod
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
|
host: 120.79.247.16
|
||||||
|
username: root
|
||||||
|
password:
|
||||||
|
from_secret: prod_ssh_password
|
||||||
|
port: 22
|
||||||
|
command_timeout: 10m
|
||||||
|
script:
|
||||||
|
- echo "=== [生产] 编译前端到 dist-prod ==="
|
||||||
|
- cd /root/aiedu/kaopeilian-frontend
|
||||||
|
- npm install --silent
|
||||||
|
- npm run build
|
||||||
|
- rm -rf /root/aiedu/dist-prod/*
|
||||||
|
- cp -r dist/* /root/aiedu/dist-prod/
|
||||||
|
- echo "前端编译完成 -> dist-prod,所有生产租户已更新"
|
||||||
|
|
||||||
|
- name: deploy-tenants
|
||||||
|
image: appleboy/drone-ssh
|
||||||
|
settings:
|
||||||
|
host: 120.79.247.16
|
||||||
|
username: root
|
||||||
|
password:
|
||||||
|
from_secret: prod_ssh_password
|
||||||
|
port: 22
|
||||||
|
command_timeout: 15m
|
||||||
|
script:
|
||||||
|
- echo "=== [生产] 部署租户后端 ==="
|
||||||
|
- cd /root/aiedu
|
||||||
|
- |
|
||||||
|
# 获取 commit message
|
||||||
|
COMMIT_MSG="${DRONE_COMMIT_MESSAGE}"
|
||||||
|
echo "Commit: $COMMIT_MSG"
|
||||||
|
|
||||||
|
# 所有可用租户
|
||||||
|
ALL_TENANTS="hua yy hl xy fw ex cxw"
|
||||||
|
|
||||||
|
# 解析要部署的租户
|
||||||
|
if echo "$COMMIT_MSG" | grep -q '\[all\]'; then
|
||||||
|
TENANTS="$ALL_TENANTS"
|
||||||
|
echo "部署所有租户: $TENANTS"
|
||||||
|
elif echo "$COMMIT_MSG" | grep -oP '\[\K[a-z,]+(?=\])' > /tmp/tenants.txt 2>/dev/null; then
|
||||||
|
TENANTS=$(cat /tmp/tenants.txt | tr ',' ' ')
|
||||||
|
echo "部署指定租户: $TENANTS"
|
||||||
|
else
|
||||||
|
TENANTS="$ALL_TENANTS"
|
||||||
|
echo "默认部署所有租户: $TENANTS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 构建 docker compose 命令
|
||||||
|
BACKEND_SERVICES=""
|
||||||
|
for t in $TENANTS; do
|
||||||
|
BACKEND_SERVICES="$BACKEND_SERVICES ${t}-backend"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "重建后端服务: $BACKEND_SERVICES"
|
||||||
|
docker compose -f docker-compose.prod-multi.yml up -d --build --no-deps $BACKEND_SERVICES
|
||||||
|
|
||||||
|
sleep 10
|
||||||
|
docker ps | grep backend
|
||||||
|
echo "=== [生产] 部署完成 ==="
|
||||||
|
echo "已更新租户: $TENANTS"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: docker-sock
|
||||||
|
host:
|
||||||
|
path: /var/run/docker.sock
|
||||||
|
|
||||||
|
---
|
||||||
|
kind: pipeline
|
||||||
|
type: docker
|
||||||
|
name: code-check
|
||||||
|
|
||||||
|
trigger:
|
||||||
|
event:
|
||||||
|
- push
|
||||||
|
- pull_request
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: python-lint
|
||||||
|
image: python:3.9-slim
|
||||||
|
commands:
|
||||||
|
- cd backend
|
||||||
|
- pip install flake8 -q
|
||||||
|
- flake8 app --count --select=E9,F63,F7,F82 --show-source --statistics || true
|
||||||
|
- echo "Python lint completed"
|
||||||
|
|
||||||
|
- name: frontend-check
|
||||||
|
image: node:18-alpine
|
||||||
|
commands:
|
||||||
|
- cd frontend
|
||||||
|
- echo "Frontend check completed"
|
||||||
|
|||||||
217
docs/测试环境配置.md
Normal file
217
docs/测试环境配置.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# 考培练系统 - 环境配置与部署指南
|
||||||
|
|
||||||
|
> 最后更新:2026-01-28
|
||||||
|
|
||||||
|
## 一、环境总览
|
||||||
|
|
||||||
|
| 环境 | 分支 | 域名 | dist 目录 | 用途 |
|
||||||
|
|------|------|------|-----------|------|
|
||||||
|
| **测试环境** | `test` | https://kpl.ireborn.com.cn | dist-test | 功能开发测试 |
|
||||||
|
| **预生产** | `staging` | https://aiedu.ireborn.com.cn | dist-staging | 集成测试/预发布 |
|
||||||
|
| **生产环境** | `main` | 各租户域名 | dist-prod | 正式生产 |
|
||||||
|
| **超级管理后台** | - | https://admin.kpl.ireborn.com.cn | - | SaaS管理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、租户列表
|
||||||
|
|
||||||
|
| 租户代码 | 名称 | 域名 | 前端端口 | 后端端口 |
|
||||||
|
|----------|------|------|----------|----------|
|
||||||
|
| hua | 华尔倍丽 | https://hua.ireborn.com.cn | 3010 | 8010 |
|
||||||
|
| yy | 杨扬宠物 | https://yy.ireborn.com.cn | 3011 | 8011 |
|
||||||
|
| hl | 武汉禾丽 | https://hl.ireborn.com.cn | 3012 | 8012 |
|
||||||
|
| xy | 芯颜定制 | https://xy.ireborn.com.cn | 3013 | 8013 |
|
||||||
|
| fw | 飞沃 | https://fw.ireborn.com.cn | 3014 | 8014 |
|
||||||
|
| ex | 恩喜成都总院 | https://ex.ireborn.com.cn | 3015 | 8015 |
|
||||||
|
| cxw | 崔曦文 | https://cxw.kpl.ireborn.com.cn | 3016 | 8016 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、CI/CD 部署方式
|
||||||
|
|
||||||
|
### 3.1 开发1环境 (kpl-dev)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 推送到 test 分支自动部署
|
||||||
|
git push cicd test
|
||||||
|
```
|
||||||
|
|
||||||
|
- **触发条件**:`test` 分支 push
|
||||||
|
- **部署目标**:kpl-dev 容器组
|
||||||
|
- **访问地址**:https://kpl.ireborn.com.cn
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 开发2环境 (主站)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 推送到 dev2 分支自动部署
|
||||||
|
git push cicd dev2
|
||||||
|
```
|
||||||
|
|
||||||
|
- **触发条件**:`dev2` 分支 push
|
||||||
|
- **部署目标**:kaopeilian 主站容器
|
||||||
|
- **访问地址**:https://aiedu.ireborn.com.cn
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 生产环境 (租户)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 推送到 main 分支,通过 commit message 控制部署范围
|
||||||
|
git push cicd main
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 部署所有租户
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "feat: 新功能上线 [all]"
|
||||||
|
git push cicd main
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 部署单个租户
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "fix: 修复问题 [hua]"
|
||||||
|
git push cicd main
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 部署多个租户
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -m "feat: 功能更新 [cxw,yy,hl]"
|
||||||
|
git push cicd main
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 默认行为
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 不带标签默认部署所有租户
|
||||||
|
git commit -m "feat: 常规更新"
|
||||||
|
git push cicd main
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、手动部署命令
|
||||||
|
|
||||||
|
### 4.1 SSH 登录服务器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh root@120.79.247.16
|
||||||
|
# 密码: Rxm88808
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 重启单个租户后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/aiedu
|
||||||
|
docker restart cxw-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 重建单个租户后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/aiedu
|
||||||
|
docker-compose -f docker-compose.prod-multi.yml up -d --build --no-deps cxw-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 查看日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f cxw-backend --tail 100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 重新编译前端(所有租户共享)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /root/aiedu/kaopeilian-frontend
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、数据库连接
|
||||||
|
|
||||||
|
### 5.1 生产共享 MySQL (prod-mysql)
|
||||||
|
|
||||||
|
- **端口**:3309
|
||||||
|
- **用户**:root
|
||||||
|
- **密码**:ProdMySQL2025!@#
|
||||||
|
- **数据库**:kaopeilian_hua, kaopeilian_yy, kaopeilian_hl, kaopeilian_xy, kaopeilian_fw, kaopeilian_ex, kaopeilian_cxw
|
||||||
|
|
||||||
|
### 5.2 开发测试 MySQL (kpl-mysql-dev)
|
||||||
|
|
||||||
|
- **端口**:3308
|
||||||
|
- **用户**:root
|
||||||
|
- **密码**:nj861021
|
||||||
|
- **数据库**:kaopeilian
|
||||||
|
|
||||||
|
### 5.3 主站 MySQL (kaopeilian-mysql)
|
||||||
|
|
||||||
|
- **端口**:3307
|
||||||
|
- **用户**:root
|
||||||
|
- **密码**:nj861021
|
||||||
|
- **数据库**:kaopeilian
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、容器管理
|
||||||
|
|
||||||
|
### 当前运行容器统计
|
||||||
|
|
||||||
|
| 类型 | 数量 |
|
||||||
|
|------|------|
|
||||||
|
| 前端容器 | 11 |
|
||||||
|
| 后端容器 | 11 |
|
||||||
|
| Redis | 10 |
|
||||||
|
| MySQL | 4 |
|
||||||
|
| Nginx | 1 |
|
||||||
|
| **总计** | **37** |
|
||||||
|
|
||||||
|
### 查看所有容器
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker ps --format 'table {{.Names}}\t{{.Status}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、测试账户
|
||||||
|
|
||||||
|
| 角色 | 用户名 | 密码 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 系统管理员 | admin | Admin123! |
|
||||||
|
| 培训经理 | manager | Admin123! |
|
||||||
|
| 测试学员 | testuser | Admin123! |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、注意事项
|
||||||
|
|
||||||
|
1. **前端共享**:所有生产租户共享同一套前端代码,编译一次全部更新
|
||||||
|
2. **后端独立**:每个租户有独立的后端容器和数据库
|
||||||
|
3. **域名解析**:
|
||||||
|
- `*.ireborn.com.cn` 解析到 120.79.242.43(SCRM服务器)
|
||||||
|
- `*.kpl.ireborn.com.cn` 解析到 120.79.247.16(考培练服务器)
|
||||||
|
4. **SSL证书**:使用 Let's Encrypt,自动续期
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、Git 仓库配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看远程仓库
|
||||||
|
git remote -v
|
||||||
|
|
||||||
|
# origin: GitHub 源代码仓库
|
||||||
|
# cicd: Gitea CI/CD 触发仓库
|
||||||
|
|
||||||
|
# 常规开发
|
||||||
|
git push origin main
|
||||||
|
|
||||||
|
# 触发部署
|
||||||
|
git push cicd test # 部署开发1
|
||||||
|
git push cicd dev2 # 部署开发2
|
||||||
|
git push cicd main # 部署生产
|
||||||
|
```
|
||||||
@@ -338,40 +338,76 @@
|
|||||||
<div class="course-section">
|
<div class="course-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h4>可用课程列表</h4>
|
<h4>可用课程列表</h4>
|
||||||
<el-input
|
<div class="header-actions">
|
||||||
v-model="courseSearchText"
|
<el-input
|
||||||
placeholder="搜索课程名称"
|
v-model="courseSearchText"
|
||||||
style="width: 200px"
|
placeholder="搜索课程名称"
|
||||||
clearable
|
style="width: 200px"
|
||||||
>
|
clearable
|
||||||
<template #prefix>
|
>
|
||||||
<el-icon><Search /></el-icon>
|
<template #prefix>
|
||||||
</template>
|
<el-icon><Search /></el-icon>
|
||||||
</el-input>
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<el-table :data="filteredAllCourses" style="width: 100%" max-height="300">
|
|
||||||
<el-table-column prop="name" label="课程名称" min-width="200" />
|
<!-- 批量操作栏 -->
|
||||||
<el-table-column prop="category" label="分类" width="120">
|
<div class="batch-actions" v-if="selectedCourses.length > 0">
|
||||||
|
<span class="selected-count">已选择 {{ selectedCourses.length }} 门课程</span>
|
||||||
|
<el-button type="danger" size="small" @click="batchAssignCourse('required')">
|
||||||
|
<el-icon><Star /></el-icon>
|
||||||
|
批量设为必修
|
||||||
|
</el-button>
|
||||||
|
<el-button type="warning" size="small" @click="batchAssignCourse('optional')">
|
||||||
|
<el-icon><CollectionTag /></el-icon>
|
||||||
|
批量设为选修
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" @click="clearSelection">
|
||||||
|
取消选择
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
ref="courseTableRef"
|
||||||
|
:data="filteredAllCourses"
|
||||||
|
style="width: 100%"
|
||||||
|
max-height="300"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
class="course-table"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55">
|
||||||
|
<template #header>
|
||||||
|
<el-checkbox
|
||||||
|
v-model="isAllSelected"
|
||||||
|
:indeterminate="isIndeterminate"
|
||||||
|
@change="handleSelectAll"
|
||||||
|
class="custom-checkbox"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="name" label="课程名称" min-width="180" />
|
||||||
|
<el-table-column prop="category" label="分类" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag size="small">{{ scope.row.category }}</el-tag>
|
<el-tag size="small">{{ scope.row.category }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="duration" label="学时" width="80" />
|
<el-table-column prop="duration" label="学时" width="70" />
|
||||||
<el-table-column prop="difficulty" label="难度" width="100">
|
<el-table-column prop="difficulty" label="难度" width="80">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="getDifficultyTagType(scope.row.difficulty)" size="small">
|
<el-tag :type="getDifficultyTagType(scope.row.difficulty)" size="small">
|
||||||
{{ getDifficultyText(scope.row.difficulty) }}
|
{{ getDifficultyText(scope.row.difficulty) }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="当前状态" width="120">
|
<el-table-column label="当前状态" width="100">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag v-if="isCourseRequired(scope.row.id)" type="danger" size="small">必修</el-tag>
|
<el-tag v-if="isCourseRequired(scope.row.id)" type="danger" size="small">必修</el-tag>
|
||||||
<el-tag v-else-if="isCourseOptional(scope.row.id)" type="warning" size="small">选修</el-tag>
|
<el-tag v-else-if="isCourseOptional(scope.row.id)" type="warning" size="small">选修</el-tag>
|
||||||
<span v-else class="text-muted">未分配</span>
|
<span v-else class="text-muted">未分配</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="200">
|
<el-table-column label="操作" width="180">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
v-if="!isCourseRequired(scope.row.id)"
|
v-if="!isCourseRequired(scope.row.id)"
|
||||||
@@ -452,6 +488,12 @@ const requiredCourses = ref<any[]>([])
|
|||||||
const optionalCourses = ref<any[]>([])
|
const optionalCourses = ref<any[]>([])
|
||||||
const allCourses = ref<any[]>([])
|
const allCourses = ref<any[]>([])
|
||||||
|
|
||||||
|
// 全选相关
|
||||||
|
const courseTableRef = ref<any>(null)
|
||||||
|
const selectedCourses = ref<any[]>([])
|
||||||
|
const isAllSelected = ref(false)
|
||||||
|
const isIndeterminate = ref(false)
|
||||||
|
|
||||||
// 岗位表单
|
// 岗位表单
|
||||||
const positionForm = reactive({
|
const positionForm = reactive({
|
||||||
id: '',
|
id: '',
|
||||||
@@ -941,6 +983,67 @@ const filteredAllCourses = computed(() => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理表格选择变化
|
||||||
|
*/
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedCourses.value = selection
|
||||||
|
const total = filteredAllCourses.value.length
|
||||||
|
isAllSelected.value = selection.length === total && total > 0
|
||||||
|
isIndeterminate.value = selection.length > 0 && selection.length < total
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全选/取消全选
|
||||||
|
*/
|
||||||
|
const handleSelectAll = (val: boolean) => {
|
||||||
|
if (courseTableRef.value) {
|
||||||
|
if (val) {
|
||||||
|
filteredAllCourses.value.forEach(row => {
|
||||||
|
courseTableRef.value.toggleRowSelection(row, true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
courseTableRef.value.clearSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除选择
|
||||||
|
*/
|
||||||
|
const clearSelection = () => {
|
||||||
|
if (courseTableRef.value) {
|
||||||
|
courseTableRef.value.clearSelection()
|
||||||
|
}
|
||||||
|
selectedCourses.value = []
|
||||||
|
isAllSelected.value = false
|
||||||
|
isIndeterminate.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量分配课程
|
||||||
|
*/
|
||||||
|
const batchAssignCourse = async (type: 'required' | 'optional') => {
|
||||||
|
if (!currentPosition.value || selectedCourses.value.length === 0) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const courseIds = selectedCourses.value.map(c => c.id)
|
||||||
|
const response = await request.post(`/api/v1/admin/positions/${currentPosition.value.id}/courses`, {
|
||||||
|
course_ids: courseIds,
|
||||||
|
course_type: type
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response && response.code === 200) {
|
||||||
|
await loadPositionCourses(currentPosition.value.id)
|
||||||
|
clearSelection()
|
||||||
|
ElMessage.success(`已将 ${courseIds.length} 门课程设为${type === 'required' ? '必修' : '选修'}`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量分配课程失败:', error)
|
||||||
|
ElMessage.error('批量分配课程失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查课程是否为必修
|
* 检查课程是否为必修
|
||||||
*/
|
*/
|
||||||
@@ -1326,11 +1429,120 @@ onMounted(() => {
|
|||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量操作栏样式
|
||||||
|
.batch-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: linear-gradient(135deg, #e8f4fd 0%, #f0f7ff 100%);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d4e8fc;
|
||||||
|
|
||||||
|
.selected-count {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #409eff;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
&.el-button--danger {
|
||||||
|
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #ff5252 0%, #d63031 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.el-button--warning {
|
||||||
|
background: linear-gradient(135deg, #ffa726 0%, #ff9800 100%);
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-muted {
|
.text-muted {
|
||||||
color: #909399;
|
color: #909399;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 课程表格样式优化
|
||||||
|
.course-table {
|
||||||
|
// Checkbox 样式优化
|
||||||
|
:deep(.el-checkbox) {
|
||||||
|
.el-checkbox__inner {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #dcdfe6;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
left: 5px;
|
||||||
|
top: 1px;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .el-checkbox__inner {
|
||||||
|
border-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-checked .el-checkbox__inner {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-indeterminate .el-checkbox__inner {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-color: #667eea;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
height: 3px;
|
||||||
|
top: 6px;
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格行选中效果
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
&.current-row,
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selection-row {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表头样式
|
||||||
|
:deep(.el-table__header-wrapper) {
|
||||||
|
th {
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user