up
Some checks failed
CI / Test (ubuntu-latest) (push) Has been cancelled
CI / Test (windows-latest) (push) Has been cancelled
CI / Lint (ubuntu-latest) (push) Has been cancelled
CI / Lint (windows-latest) (push) Has been cancelled
CI / Check (ubuntu-latest) (push) Has been cancelled
CI / Check (windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Website on push / Deploy Push Playground Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Docs Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Antd Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Element Ftp (push) Has been cancelled
Deploy Website on push / Deploy Push Naive Ftp (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
CI / CI OK (push) Has been cancelled
Deploy Website on push / Rerun on failure (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Issue Close Require / close-issues (push) Has been cancelled
Close stale issues / stale (push) Has been cancelled

This commit is contained in:
杨志
2026-03-19 09:01:05 +08:00
parent 6dd6854e6b
commit d20caa9c8d
33 changed files with 1157 additions and 2996 deletions

View File

@@ -1,157 +0,0 @@
<div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
**日本語** | [English](./README.md) | [中文](./README.zh-CN.md)
## 紹介
Vue Vben Adminは、最新の`vue3``vite``TypeScript`などの主流技術を使用して開発された、無料でオープンソースの中・後端テンプレートです。すぐに使える中・後端のフロントエンドソリューションとして、学習の参考にもなります。
## アップグレード通知
これは最新バージョン `5.0` であり、以前のバージョンとは互換性がありません。新しいプロジェクトを開始する場合は、最新バージョンを使用することをお勧めします。古いバージョンを表示したい場合は、[v2ブランチ](https://github.com/vbenjs/vue-vben-admin/tree/v2)を使用してください。
## 特徴
- **最新技術スタック**Vue 3やViteなどの最先端フロントエンド技術で開発
- **TypeScript**アプリケーション規模のJavaScriptのための言語
- **テーマ**:複数のテーマカラーが利用可能で、カスタマイズオプションも豊富
- **国際化**:完全な内蔵国際化サポート
- **権限管理**:動的ルートベースの権限生成ソリューションを内蔵
## プレビュー
- [Vben Admin](https://vben.pro/) - フルバージョンの中国語サイト
テストアカウントvben/123456
<div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</div>
### Gitpodを使用
GitpodGitHub用の無料オンライン開発環境でプロジェクトを開き、すぐにコーディングを開始します。
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## ドキュメント
[ドキュメント](https://doc.vben.pro/)
## インストールと使用
1. プロジェクトコードを取得
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
2. 依存関係のインストール
```bash
cd vue-vben-admin
npm i -g corepack
pnpm install
```
3. 実行
```bash
pnpm dev
```
4. ビルド
```bash
pnpm build
```
## 変更ログ
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 貢献方法
ご参加をお待ちしております![Issueを提出](https://github.com/anncwb/vue-vben-admin/issues/new/choose)するか、Pull Requestを送信してください。
**Pull Request プロセス:**
1. コードをフォーク
2. 自分のブランチを作成:`git checkout -b feat/xxxx`
3. 変更をコミット:`git commit -am 'feat(function): add xxxxx'`
4. ブランチをプッシュ:`git push origin feat/xxxx`
5. `pull request`を送信
## Git貢献提出規則
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 規則 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 新機能の追加
- `fix` 問題/バグの修正
- `style` コードスタイルに関連し、実行結果に影響しない
- `perf` 最適化/パフォーマンス向上
- `refactor` リファクタリング
- `revert` 変更の取り消し
- `test` テスト関連
- `docs` ドキュメント/注釈
- `chore` 依存関係の更新/スキャフォールディング設定の変更など
- `ci` 継続的インテグレーション
- `types` 型定義ファイルの変更
## ブラウザサポート
ローカル開発には `Chrome 80+` ブラウザを推奨します
モダンブラウザをサポートし、IEはサポートしません
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: |
| 最新2バージョン | 最新2バージョン | 最新2バージョン | 最新2バージョン |
## メンテナー
[@Vben](https://github.com/anncwb)
## スター歴史
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## 寄付
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## 貢献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## ライセンス
[MIT © Vben-2020](./LICENSE)

157
README.md
View File

@@ -1,157 +0,0 @@
<div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml)
**English** | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md)
## Introduction
Vue Vben Admin is a free and open source middle and back-end template. Using the latest `vue3`, `vite`, `TypeScript` and other mainstream technology development, the out-of-the-box middle and back-end front-end solutions can also be used for learning reference.
## Upgrade Notice
This is the latest version, 5.0, and it is not compatible with previous versions. If you are starting a new project, it is recommended to use the latest version. If you wish to view the old version, please use the [v2 branch](https://github.com/vbenjs/vue-vben-admin/tree/v2).
## Features
- **Latest Technology Stack**: Developed with cutting-edge front-end technologies like Vue 3 and Vite
- **TypeScript**: A language for application-scale JavaScript
- **Themes**: Multiple theme colors available with customizable options
- **Internationalization**: Comprehensive built-in internationalization support
- **Permissions**: Built-in solution for dynamic route-based permission generation
## Preview
- [Vben Admin](https://vben.pro/) - Full version Chinese site
Test Account: vben/123456
<div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</div>
### Use Gitpod
Open the project in Gitpod (free online dev environment for GitHub) and start coding immediately.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## Documentation
[Document](https://doc.vben.pro/)
## Install and Use
1. Get the project code
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
2. Install dependencies
```bash
cd vue-vben-admin
npm i -g corepack
pnpm install
```
3. Run
```bash
pnpm dev
```
4. Build
```bash
pnpm build
```
## Change Log
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## How to Contribute
You are very welcome to join! [Raise an issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) or submit a Pull Request.
**Pull Request Process:**
1. Fork the code
2. Create your branch: `git checkout -b feat/xxxx`
3. Submit your changes: `git commit -am 'feat(function): add xxxxx'`
4. Push your branch: `git push origin feat/xxxx`
5. Submit `pull request`
## Git Contribution Submission Specification
Reference [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) specification ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` Add new features
- `fix` Fix the problem/BUG
- `style` The code style is related and does not affect the running result
- `perf` Optimization/performance improvement
- `refactor` Refactor
- `revert` Undo edit
- `test` Test related
- `docs` Documentation/notes
- `chore` Dependency update/scaffolding configuration modification etc.
- `ci` Continuous integration
- `types` Type definition file changes
## Browser Support
The `Chrome 80+` browser is recommended for local development
Support modern browsers, not IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Maintainer
[@Vben](https://github.com/anncwb)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## Donate
If you think this project is helpful to you, you can help the author buy a cup of coffee to show your support!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aee;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## Contributors
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## License
[MIT © Vben-2020](./LICENSE)

View File

@@ -1,157 +0,0 @@
<div align="center">
<a href="https://github.com/anncwb/vue-vben-admin">
<img alt="VbenAdmin Logo" width="215" src="https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp">
</a>
<br>
<br>
[![license](https://img.shields.io/github/license/anncwb/vue-vben-admin.svg)](LICENSE)
<h1>Vue Vben Admin</h1>
</div>
[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=vbenjs_vue-vben-admin&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) ![codeql](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml/badge.svg) ![build](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml/badge.svg) ![ci](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml/badge.svg) ![deploy](https://github.com/vbenjs/vue-vben-admin/actions/workflows/deploy.yml/badge.svg)
**中文** | [English](./README.md) | [日本語](./README.ja-JP.md)
## 简介
Vue Vben Admin 是 Vue Vben Admin 的升级版本。作为一个免费开源的中后台模板,它采用了最新的 Vue 3、Vite、TypeScript 等主流技术开发,开箱即用,可用于中后台前端开发,也适合学习参考。
## 升级提示
该版本为最新版本 `5.0`,与其他版本不兼容,如果你是新项目,建议使用最新版本。如果你想查看旧版本,请使用 [v2 分支](https://github.com/vbenjs/vue-vben-admin/tree/v2)
## 特性
- **最新技术栈**:使用 Vue3/vite 等前端前沿技术开发
- **TypeScript**:应用程序级 JavaScript 的语言
- **主题**:提供多套主题色彩,可配置自定义主题
- **国际化**:内置完善的国际化方案
- **权限**:内置完善的动态路由权限生成方案
## 预览
- [Vben Admin](https://vben.pro/) - 完整版中文站点
测试账号vben/123456
<div align="center">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview1.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview2.png">
<img alt="VbenAdmin Logo" width="100%" src="https://anncwb.github.io/anncwb/images/preview3.png">
</div>
### 使用 Gitpod
在 Gitpod适用于 GitHub 的免费在线开发环境)中打开项目,并立即开始编码。
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/vbenjs/vue-vben-admin)
## 文档
[文档地址](https://doc.vben.pro/)
## 安装使用
1. 获取项目代码
```bash
git clone https://github.com/vbenjs/vue-vben-admin.git
```
2. 安装依赖
```bash
cd vue-vben-admin
npm i -g corepack
pnpm install
```
3. 运行
```bash
pnpm dev
```
4. 打包
```bash
pnpm build
```
## 更新日志
[CHANGELOG](https://github.com/vbenjs/vue-vben-admin/releases)
## 如何贡献
非常欢迎你的加入![提一个 Issue](https://github.com/anncwb/vue-vben-admin/issues/new/choose) 或者提交一个 Pull Request。
**Pull Request 流程:**
1. Fork 代码
2. 创建自己的分支:`git checkout -b feature/xxxx`
3. 提交你的修改:`git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支:`git push origin feature/xxxx`
5. 提交 `pull request`
## Git 贡献提交规范
参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `ci` 持续集成
- `types` 类型定义文件更改
## 浏览器支持
本地开发推荐使用 `Chrome 80+` 浏览器
支持现代浏览器,不支持 IE
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)</br>Safari |
| :-: | :-: | :-: | :-: |
| last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## 维护者
[@Vben](https://github.com/anncwb)
## Star 历史
[![Star History Chart](https://api.star-history.com/svg?repos=vbenjs/vue-vben-admin&type=Date)](https://star-history.com/#vbenjs/vue-vben-admin&Date)
## 捐赠
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
![donate](https://unpkg.com/@vbenjs/static-source@0.1.7/source/sponsor.png)
<a style="display: block;width: 100px;height: 50px;line-height: 50px; color: #fff;text-align: center; background: #408aed;border-radius: 4px;" href="https://www.paypal.com/paypalme/cvvben">Paypal Me</a>
## 贡献者
<a href="https://openomy.app/github/vbenjs/vue-vben-admin" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.app/svg?repo=vbenjs/vue-vben-admin&chart=bubble&latestMonth=3" target="_blank" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
<a href="https://github.com/vbenjs/vue-vben-admin/graphs/contributors">
<img alt="Contributors" src="https://contrib.rocks/image?repo=vbenjs/vue-vben-admin" />
</a>
## Discord
- [Github Discussions](https://github.com/anncwb/vue-vben-admin/discussions)
## 许可证
[MIT © Vben-2020](./LICENSE)

View File

@@ -1,6 +1,6 @@
# 生产环境配置
VITE_BASE=/super/
VITE_GLOB_API_URL=http://xz.dhdjy.com
VITE_GLOB_API_URL=https://xz.dhdjy.com
VITE_ROUTER_HISTORY=history
VITE_COMPRESS=gzip
VITE_PWA=false

View File

@@ -45,6 +45,8 @@
"dayjs": "catalog:",
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
"vue-router": "catalog:",
"xlsx": "^0.18.5",
"xlsx-js-style": "^1.2.0"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,78 +0,0 @@
import { requestClient } from '#/api/request';
export namespace BookingApi {
/** 预订列表查询参数 */
export interface ListParams {
page?: number;
limit?: number;
classroom_id?: number;
booking_date?: string;
status?: number;
student_id?: number;
type?: string;
}
/** 预订信息 */
export interface BookingInfo {
id?: number;
booking_no?: string | null;
classroom_id?: number;
seat_number?: string;
user_id?: number | null;
user_name?: string | null;
booking_date?: string;
start_time?: string;
end_time?: string;
purpose?: string | null;
status?: number;
remark?: string | null;
create_time?: string;
update_time?: string;
seat_row_index?: number;
seat_col_index?: number;
student_id?: number;
classroom_name?: string;
time_range?: string;
[key: string]: any;
}
/** 同意座位变更申请参数 */
export interface ApproveParams {
id: number;
}
/** 拒绝座位变更申请参数 */
export interface RejectParams {
id: number;
reason: string;
}
}
/**
* 获取预订列表
* GET /api/admin/booking/list
*/
export async function getBookingListApi(params?: BookingApi.ListParams) {
// 使用 responseReturn: 'body' 获取完整响应体
return requestClient.get<any>('/api/admin/booking/list', {
params,
responseReturn: 'body',
});
}
/**
* 同意座位变更申请
* POST /api/admin/booking/approve
*/
export async function approveBookingApi(data: BookingApi.ApproveParams) {
return requestClient.post('/api/admin/booking/approve', data);
}
/**
* 拒绝座位变更申请
* POST /api/admin/booking/reject
*/
export async function rejectBookingApi(data: BookingApi.RejectParams) {
return requestClient.post('/api/admin/booking/reject', data);
}

View File

@@ -103,7 +103,9 @@ export namespace ClassroomApi {
/** 取消选座参数 */
export interface CancelBookingParams {
id: number;
id?: number; // 预订ID优先使用
seat_number?: string; // 座位号当没有预订ID时使用
classroomId?: number; // 教室ID使用座位号时必填
}
/** 批量取消选座参数 */
@@ -123,6 +125,8 @@ export namespace ClassroomApi {
student_name: string;
student_mobile: string;
select_time: string;
booking_id?: number; // 预订ID用于取消选座
student_id?: number; // 学生ID用于分配座位
}
}
@@ -211,7 +215,10 @@ export async function randomAssignSeatsApi(data: ClassroomApi.RandomAssignSeatsP
* GET /api/admin/classroom/getUnassignedStudents
*/
export async function getUnassignedStudentsApi(params: ClassroomApi.UnassignedStudentsParams) {
return requestClient.get<any>('/api/admin/classroom/getUnassignedStudents', { params });
return requestClient.get<any>('/api/admin/classroom/getUnassignedStudents', {
params,
responseReturn: 'body',
});
}
/**

View File

@@ -1,5 +1,4 @@
export * from './classroom';
export * from './booking';
export * from './student';
export * from './class';
export * from './teacher';

View File

@@ -16,10 +16,6 @@
"list": "教室列表",
"detail": "教室详情"
},
"booking": {
"title": "预订管理",
"list": "预订列表"
},
"student": {
"title": "学生管理",
"list": "学生列表",

View File

@@ -1,19 +0,0 @@
import type { RouteRecordRaw } from 'vue-router';
import { $t } from '#/locales';
const routes: RouteRecordRaw[] = [
{
name: 'BookingList',
path: '/booking/list',
component: () => import('#/views/booking/list.vue'),
meta: {
icon: 'lucide:calendar-check',
order: 2,
title: $t('page.booking.title'),
},
},
];
export default routes;

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { Recordable } from '@vben/types';
import { computed, ref } from 'vue';
@@ -54,7 +53,7 @@ const formSchema = computed((): VbenFormSchema[] => {
* Asynchronously handle the login process
* @param values 登录表单数据
*/
async function handleLogin(values: Recordable<any>) {
async function handleLogin() {
// 处理验证码登录
}
</script>

View File

@@ -1,6 +1,5 @@
<script lang="ts" setup>
import type { VbenFormSchema } from '@vben/common-ui';
import type { BasicOption } from '@vben/types';
import { computed, markRaw } from 'vue';

View File

@@ -1,291 +0,0 @@
<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue';
import { Page } from '@vben/common-ui';
import { Button, Card, Table, Space, message, Modal, Input, DatePicker, Select, Tag } from 'ant-design-vue';
import {
getBookingListApi,
approveBookingApi,
rejectBookingApi,
type BookingApi
} from '#/api';
defineOptions({ name: 'BookingList' });
const loading = ref(false);
const tableData = ref<BookingApi.BookingInfo[]>([]);
const total = ref(0);
const pagination = reactive({
current: 1,
pageSize: 10,
});
const searchForm = reactive({
classroom_id: undefined as number | undefined,
booking_date: '',
status: undefined as number | undefined,
student_id: undefined as number | undefined,
});
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '教室名称',
dataIndex: 'classroom_name',
key: 'classroom_name',
width: 180,
},
{
title: '座位号',
dataIndex: 'seat_number',
key: 'seat_number',
width: 100,
},
{
title: '学生ID',
dataIndex: 'student_id',
key: 'student_id',
width: 100,
},
{
title: '预订日期',
dataIndex: 'booking_date',
key: 'booking_date',
width: 120,
},
{
title: '时间范围',
dataIndex: 'time_range',
key: 'time_range',
width: 180,
customRender: ({ text }: { text: string }) => {
if (!text || text === '00:00:00 - 00:00:00') {
return '-';
}
return text;
},
},
{
title: '用途',
dataIndex: 'purpose',
key: 'purpose',
width: 150,
customRender: ({ text }: { text: string | null }) => {
return text || '-';
},
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
width: 180,
},
{
title: '操作',
key: 'action',
width: 150,
fixed: 'right' as const,
},
];
const fetchData = async () => {
loading.value = true;
try {
const params: BookingApi.ListParams = {
page: pagination.current,
limit: pagination.pageSize,
...searchForm,
};
const res = await getBookingListApi(params);
// 根据实际返回格式code 为 0 表示成功
if (res && (res.code === 0 || res.code === 200)) {
tableData.value = Array.isArray(res.data) ? res.data : [];
total.value = res.count || 0;
} else {
tableData.value = [];
total.value = 0;
}
} catch (error) {
console.error('获取预订列表失败:', error);
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
const handleSearch = () => {
pagination.current = 1;
fetchData();
};
const handleReset = () => {
searchForm.classroom_id = undefined;
searchForm.booking_date = '';
searchForm.status = undefined;
searchForm.student_id = undefined;
handleSearch();
};
const getStatusText = (status?: number) => {
const statusMap: Record<number, string> = {
0: '待审核',
1: '已通过',
2: '已拒绝',
};
return statusMap[status ?? -1] || '未知';
};
const getStatusColor = (status?: number) => {
const colorMap: Record<number, string> = {
0: 'orange',
1: 'green',
2: 'red',
};
return colorMap[status ?? -1] || 'default';
};
const handleApprove = (record: BookingApi.BookingInfo) => {
Modal.confirm({
title: '确认同意',
content: '确定要同意这个座位变更申请吗?',
onOk: async () => {
try {
await approveBookingApi({ id: record.id! });
message.success('操作成功');
fetchData();
} catch (error) {
console.error('操作失败:', error);
}
},
});
};
const handleReject = (record: BookingApi.BookingInfo) => {
Modal.confirm({
title: '拒绝申请',
content: '请输入拒绝原因',
onOk: async () => {
try {
await rejectBookingApi({ id: record.id!, reason: '管理员拒绝' });
message.success('操作成功');
fetchData();
} catch (error) {
console.error('操作失败:', error);
}
},
});
};
const handleTableChange = (pag: any) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
fetchData();
};
onMounted(() => {
fetchData();
});
</script>
<template>
<Page title="预订列表">
<Card>
<div class="mb-4">
<Space wrap>
<Input
v-model:value="searchForm.classroom_id"
placeholder="教室ID"
style="width: 150px"
type="number"
allow-clear
@press-enter="handleSearch"
/>
<Input
v-model:value="searchForm.student_id"
placeholder="学生ID"
style="width: 150px"
type="number"
allow-clear
@press-enter="handleSearch"
/>
<DatePicker
v-model:value="searchForm.booking_date"
placeholder="预订日期"
style="width: 180px"
format="YYYY-MM-DD"
allow-clear
/>
<Select
v-model:value="searchForm.status"
placeholder="状态"
style="width: 120px"
allow-clear
>
<Select.Option :value="0">待审核</Select.Option>
<Select.Option :value="1">已通过</Select.Option>
<Select.Option :value="2">已拒绝</Select.Option>
</Select>
<Button type="primary" @click="handleSearch">搜索</Button>
<Button @click="handleReset">重置</Button>
</Space>
</div>
<Table
:columns="columns"
:data-source="tableData"
:loading="loading"
:scroll="{ x: 1400 }"
:pagination="{
current: pagination.current,
pageSize: pagination.pageSize,
total: total,
showSizeChanger: true,
showTotal: (total) => `共 ${total} 条`,
}"
row-key="id"
@change="handleTableChange"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<Tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</Tag>
</template>
<template v-else-if="column.key === 'action'">
<Space>
<Button
v-if="record.status === 0"
type="link"
size="small"
@click="handleApprove(record)"
>
同意
</Button>
<Button
v-if="record.status === 0"
type="link"
danger
size="small"
@click="handleReject(record)"
>
拒绝
</Button>
<span v-else class="text-gray-400">-</span>
</Space>
</template>
</template>
</Table>
</Card>
</Page>
</template>

View File

@@ -53,7 +53,6 @@ const columns = [
{
title: '教室',
key: 'classroom_name',
width: 150,
},
{
title: '学生人数',

View File

@@ -2,7 +2,8 @@
import { ref, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { Card, Form, Input, Select, InputNumber, Button, message, Tabs, Radio } from 'ant-design-vue';
import { Card, Form, Input, Select, InputNumber, Button, message, Tabs, Radio, DatePicker } from 'ant-design-vue';
import dayjs, { type Dayjs } from 'dayjs';
import {
getClassroomDetailApi,
saveClassroomApi,
@@ -24,7 +25,10 @@ const schoolList = ref<SchoolApi.SchoolInfo[]>([]);
const teacherList = ref<TeacherApi.TeacherInfo[]>([]);
const schoolListLoading = ref(false);
const teacherListLoading = ref(false);
const formData = ref<ClassroomApi.SaveParams>({
const formData = ref<ClassroomApi.SaveParams & {
fiexd_start_time: Dayjs | null;
fiexd_end_time: Dayjs | null;
}>({
name: '',
building: '',
floor: '',
@@ -35,6 +39,8 @@ const formData = ref<ClassroomApi.SaveParams>({
teacher_id: undefined,
description: '',
status: 1,
fiexd_start_time: null,
fiexd_end_time: null,
});
const isEdit = computed(() => !!route.params.id);
@@ -60,6 +66,13 @@ const handleSubmit = async () => {
delete data.floor;
delete data.room;
}
// 处理选座时间字段,将 dayjs 对象转换为字符串
if (formData.value.fiexd_start_time) {
(data as any).fiexd_start_time = formData.value.fiexd_start_time.format('YYYY-MM-DD HH:mm:ss');
}
if (formData.value.fiexd_end_time) {
(data as any).fiexd_end_time = formData.value.fiexd_end_time.format('YYYY-MM-DD HH:mm:ss');
}
await saveClassroomApi(data);
message.success(isEdit.value ? '更新成功' : '创建成功');
router.back();
@@ -93,6 +106,8 @@ const fetchDetail = async () => {
teacher_id: data.teacher_id,
description: data.description || '',
status: data.status !== undefined ? data.status : 1,
fiexd_start_time: (data as any).fiexd_start_time ? dayjs((data as any).fiexd_start_time) : null,
fiexd_end_time: (data as any).fiexd_end_time ? dayjs((data as any).fiexd_end_time) : null,
};
} else {
message.error(res?.message || res?.msg || '获取教室详情失败');
@@ -141,6 +156,17 @@ const fetchTeacherList = async () => {
}
};
// 验证选座结束时间
const validateEndTime = (_rule: any, value: Dayjs | null) => {
return new Promise<void>((resolve, reject) => {
if (value && formData.value.fiexd_start_time && value.isBefore(formData.value.fiexd_start_time)) {
reject('选座结束时间必须晚于开始时间');
} else {
resolve();
}
});
};
onMounted(() => {
fetchSchoolList();
fetchTeacherList();
@@ -253,6 +279,35 @@ onMounted(() => {
<Form.Item label="描述" name="description">
<Input.TextArea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
</Form.Item>
<Form.Item
label="选座开始时间"
name="fiexd_start_time"
:rules="[{ required: true, message: '请选择选座开始时间' }]"
>
<DatePicker
v-model:value="formData.fiexd_start_time"
placeholder="请选择选座开始时间"
style="width: 100%"
show-time
format="YYYY-MM-DD HH:mm:ss"
/>
</Form.Item>
<Form.Item
label="选座结束时间"
name="fiexd_end_time"
:rules="[
{ required: true, message: '请选择选座结束时间' },
{ validator: validateEndTime },
]"
>
<DatePicker
v-model:value="formData.fiexd_end_time"
placeholder="请选择选座结束时间"
style="width: 100%"
show-time
format="YYYY-MM-DD HH:mm:ss"
/>
</Form.Item>
<Form.Item label="状态" name="status">
<Radio.Group v-model:value="formData.status">
<Radio :value="1">启用</Radio>

View File

@@ -9,9 +9,16 @@ import {
batchDeleteClassroomApi,
getClassroomSeatListApi,
getClassroomDetailApi,
getClassroomSchoolListApi,
getClassroomStatusApi,
cancelBookingApi,
cancelBookingAutoApi,
assignSeatApi,
getUnassignedStudentsApi,
type ClassroomApi
} from '#/api';
import SeatLayoutEditor from '#/components/classroom/SeatLayoutEditor.vue';
import * as XLSX from 'xlsx-js-style';
defineOptions({ name: 'ClassroomList' });
@@ -41,10 +48,13 @@ const seatListPagination = reactive({
current: 1,
pageSize: 10,
});
const selectedSeatKeys = ref<string[]>([]);
const layoutVisible = ref(false);
const layoutLoading = ref(false);
const currentClassroomId = ref<number | null>(null);
const currentLayout = ref<ClassroomApi.ClassroomLayout | null>(null);
const schoolList = ref<Array<{ id: number; name: string }>>([]);
const schoolListLoading = ref(false);
const columns = [
{
@@ -59,27 +69,32 @@ const columns = [
key: 'name',
},
{
title: '楼栋',
dataIndex: 'building',
key: 'building',
title: '校区',
dataIndex: 'school_name',
key: 'school_name',
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
return record.location?.building || record.building || '-';
return (record as any).school_name || '-';
},
},
{
title: '楼层',
dataIndex: 'floor',
key: 'floor',
title: '地址',
dataIndex: 'address',
key: 'address',
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
return record.location?.floor || record.floor || '-';
},
},
{
title: '房间号',
dataIndex: 'room',
key: 'room',
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
return record.location?.room || '-';
const building = record.location?.building || record.building || '';
const floor = record.location?.floor || record.floor || '';
const room = record.location?.room || '';
const addressParts: string[] = [];
if (building && building.trim() !== '') {
addressParts.push(building);
}
if (floor && floor.trim() !== '') {
addressParts.push(`${floor}`);
}
if (room && room.trim() !== '') {
addressParts.push(`${room}`);
}
return addressParts.length > 0 ? addressParts.join('') : '-';
},
},
{
@@ -213,10 +228,23 @@ const handleViewStatus = async (record: ClassroomApi.ClassroomInfo) => {
}
seatListVisible.value = true;
currentClassroomId.value = record.id;
currentClassroomName.value = record.name || '';
seatListLoading.value = true;
seatListData.value = [];
// 同时加载布局数据,以便导出布局
currentLayout.value = null;
try {
const layoutRes = await getClassroomDetailApi({ id: record.id });
if (layoutRes && (layoutRes.code === 0 || layoutRes.code === 200)) {
const data = layoutRes.data || layoutRes;
currentLayout.value = data.layout || null;
}
} catch (error) {
console.warn('获取教室布局失败:', error);
}
try {
const res = await getClassroomSeatListApi({ id: record.id });
if (res && (res.code === 0 || res.code === 200)) {
@@ -288,6 +316,273 @@ const handleLayoutSaved = (layout: ClassroomApi.ClassroomLayout) => {
// handleCloseLayout();
};
// 导出座位布局为Excel
const handleExportLayout = async () => {
if (!currentLayout.value || !currentClassroomId.value) {
message.warning('没有布局数据可导出');
return;
}
try {
// 获取座位状态数据
let seatStatusMap = new Map<string, { name: string; date: string }>();
try {
const statusRes = await getClassroomSeatListApi({ id: currentClassroomId.value });
if (statusRes && (statusRes.code === 0 || statusRes.code === 200)) {
const seatList = Array.isArray(statusRes.data) ? statusRes.data : [];
seatList.forEach((seat: ClassroomApi.SeatInfo) => {
if (seat.is_selected === 1 && seat.seat_number) {
seatStatusMap.set(seat.seat_number, {
name: seat.student_name || '',
date: seat.select_time || '',
});
}
});
}
} catch (error) {
console.warn('获取座位状态失败,将只导出布局信息:', error);
}
const layout = currentLayout.value;
const cols = layout.cols || 0;
const rows = layout.rows || 0;
const cells = layout.cells || [];
if (cols === 0 || rows === 0) {
message.warning('布局数据为空,无法导出');
return;
}
// 创建单元格映射
const cellMap = new Map<string, ClassroomApi.ClassroomLayoutCell>();
cells.forEach((cell) => {
const key = `${cell.col},${cell.row}`;
cellMap.set(key, cell);
});
// 先创建数据数组
const data: any[][] = [];
// 第一行:列标题(空单元格 + A, B, C...
const headerRow: any[] = [''];
for (let col = 0; col < cols; col++) {
// 处理超过26列的情况AA, AB, AC...
let colChar = '';
if (col < 26) {
colChar = String.fromCharCode(65 + col); // A-Z
} else {
const first = Math.floor(col / 26) - 1;
const second = col % 26;
colChar = String.fromCharCode(65 + first) + String.fromCharCode(65 + second);
}
headerRow.push(colChar);
}
data.push(headerRow);
// 填充数据行
for (let row = 0; row < rows; row++) {
const dataRow: any[] = [row + 1]; // 行号
for (let col = 0; col < cols; col++) {
const key = `${col},${row}`;
const cell = cellMap.get(key);
if (!cell || cell.merged) {
// 被合并的单元格或空白单元格
dataRow.push('');
} else {
let cellValue = '';
if (cell.type === 'seat') {
const seatNumber = cell.number || '';
const status = cell.status === 0 ? '不可选' : '';
const seatStatus = seatStatusMap.get(seatNumber);
if (seatStatus) {
// 已预订:编号、姓名、日期
cellValue = `${seatNumber} ${seatStatus.name} ${seatStatus.date}`;
} else if (status === '不可选') {
// 不可选
cellValue = `${seatNumber} 不可选`;
} else {
// 空闲
cellValue = `${seatNumber} 空闲`;
}
} else if (cell.type === 'aisle') {
cellValue = '过道';
} else if (cell.type === 'projector') {
cellValue = '投影';
} else if (cell.type === 'pillar') {
cellValue = '柱子';
} else if (cell.type === 'door') {
cellValue = '门';
} else {
// empty
cellValue = '';
}
dataRow.push(cellValue);
}
}
data.push(dataRow);
}
// 使用 aoa_to_sheet 创建工作表
const ws = XLSX.utils.aoa_to_sheet(data);
// 设置样式
const range = XLSX.utils.decode_range(ws['!ref'] || 'A1');
// 定义边框样式(所有单元格统一使用)
const borderStyle = {
top: { style: 'thin', color: { rgb: '000000' } },
bottom: { style: 'thin', color: { rgb: '000000' } },
left: { style: 'thin', color: { rgb: '000000' } },
right: { style: 'thin', color: { rgb: '000000' } },
};
// 设置列标题样式
for (let c = 0; c <= range.e.c; c++) {
const cellRef = XLSX.utils.encode_cell({ r: 0, c });
if (!ws[cellRef]) ws[cellRef] = { v: '', t: 's' };
if (c === 0) {
// 行号列标题
ws[cellRef].s = {
fill: { fgColor: { rgb: 'F6FFED' } },
font: { bold: true },
alignment: { horizontal: 'center', vertical: 'center' },
border: borderStyle,
};
} else {
// 列标题
ws[cellRef].s = {
fill: { fgColor: { rgb: 'E6F7FF' } },
font: { bold: true, color: { rgb: '1890FF' } },
alignment: { horizontal: 'center', vertical: 'center' },
border: borderStyle,
};
}
}
// 设置行号列样式
for (let r = 1; r <= range.e.r; r++) {
const cellRef = XLSX.utils.encode_cell({ r, c: 0 });
if (!ws[cellRef]) ws[cellRef] = { v: r, t: 'n' };
ws[cellRef].s = {
fill: { fgColor: { rgb: 'F6FFED' } },
font: { bold: true, color: { rgb: '52C41A' } },
alignment: { horizontal: 'center', vertical: 'center' },
border: borderStyle,
};
}
// 设置数据单元格样式
for (let r = 1; r <= range.e.r; r++) {
for (let c = 1; c <= range.e.c; c++) {
const cellRef = XLSX.utils.encode_cell({ r, c });
if (!ws[cellRef]) continue;
const key = `${c - 1},${r - 1}`;
const cell = cellMap.get(key);
if (!cell || cell.merged) {
ws[cellRef].s = {
fill: { fgColor: { rgb: 'FFFFFF' } },
alignment: { horizontal: 'center', vertical: 'center' },
border: borderStyle,
};
} else {
let cellStyle: any = {
alignment: { horizontal: 'center', vertical: 'center', wrapText: true },
border: borderStyle,
};
if (cell.type === 'seat') {
const seatNumber = cell.number || '';
const status = cell.status === 0 ? '不可选' : '';
const seatStatus = seatStatusMap.get(seatNumber);
if (seatStatus) {
// 已预订(蓝色背景)
cellStyle.fill = { fgColor: { rgb: '4472C4' } };
cellStyle.font = { color: { rgb: 'FFFFFF' }, bold: true };
} else if (status === '不可选') {
// 不可选(红色背景)
cellStyle.fill = { fgColor: { rgb: 'FF0000' } };
cellStyle.font = { color: { rgb: 'FFFFFF' }, bold: true };
} else {
// 空闲(绿色背景)
cellStyle.fill = { fgColor: { rgb: '70AD47' } };
cellStyle.font = { color: { rgb: 'FFFFFF' }, bold: true };
}
} else if (cell.type === 'aisle') {
cellStyle.fill = { fgColor: { rgb: 'FFC000' } };
cellStyle.font = { color: { rgb: 'FFFFFF' }, bold: true };
} else if (cell.type === 'projector') {
cellStyle.fill = { fgColor: { rgb: '00B0F0' } };
cellStyle.font = { color: { rgb: 'FFFFFF' }, bold: true };
} else if (cell.type === 'pillar') {
cellStyle.fill = { fgColor: { rgb: '808080' } };
cellStyle.font = { color: { rgb: 'FFFFFF' }, bold: true };
} else if (cell.type === 'door') {
cellStyle.fill = { fgColor: { rgb: '1890FF' } };
cellStyle.font = { color: { rgb: 'FFFFFF' }, bold: true };
} else {
// empty
cellStyle.fill = { fgColor: { rgb: 'FFFFFF' } };
}
ws[cellRef].s = cellStyle;
}
}
}
// 设置列宽统一为10
ws['!cols'] = [{ wch: 10 }]; // 行号列
for (let i = 0; i < cols; i++) {
ws['!cols'].push({ wch: 10 });
}
// 设置行高(加倍高度,便于阅读)
ws['!rows'] = [{ hpt: 40 }]; // 标题行
for (let i = 0; i < rows; i++) {
ws['!rows'].push({ hpt: 60 });
}
// 设置合并单元格(如果有)
const merges: any[] = [];
cells.forEach((cell) => {
if (!cell.merged) {
const colspan = cell.colspan || 1;
const rowspan = cell.rowspan || 1;
if (colspan > 1 || rowspan > 1) {
// Excel合并单元格结束位置 = 起始位置 + 跨度 - 1
// 注意Excel的行列从0开始但我们的数据从1开始因为有标题行
merges.push({
s: { r: cell.row + 1, c: cell.col + 1 },
e: { r: cell.row + rowspan, c: cell.col + colspan },
});
}
}
});
if (merges.length > 0) {
ws['!merges'] = merges;
}
// 创建工作簿并添加工作表
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '座位布局');
// 导出文件
const fileName = `${currentClassroomName.value || '教室'}_座位布局_${new Date().toISOString().slice(0, 10)}.xlsx`;
XLSX.writeFile(wb, fileName);
message.success('导出成功');
} catch (error: any) {
console.error('导出失败:', error);
message.error(error?.message || '导出失败');
}
};
// 处理座位列表分页变化
const handleSeatListTableChange = (pag: any) => {
if (pag) {
@@ -296,6 +591,214 @@ const handleSeatListTableChange = (pag: any) => {
}
};
// 取消选座
const handleCancelBooking = async (record: ClassroomApi.SeatInfo) => {
if (!currentClassroomId.value) {
message.error('教室ID不存在');
return;
}
// 如果没有booking_id尝试通过座位号取消
let bookingId = record.booking_id;
// 如果没有booking_id尝试通过座位状态API获取
if (!bookingId && record.is_selected === 1) {
try {
const statusRes = await getClassroomStatusApi({ id: currentClassroomId.value });
if (statusRes && (statusRes.code === 0 || statusRes.code === 200)) {
const statusData = statusRes.data || statusRes;
// 查找对应座位的预订ID
if (Array.isArray(statusData)) {
const seatStatus = statusData.find((item: any) => item.seat_number === record.seat_number);
if (seatStatus && seatStatus.booking_id) {
bookingId = seatStatus.booking_id;
}
} else if (statusData.seats && Array.isArray(statusData.seats)) {
const seatStatus = statusData.seats.find((item: any) => item.seat_number === record.seat_number);
if (seatStatus && seatStatus.booking_id) {
bookingId = seatStatus.booking_id;
}
}
}
} catch (error) {
console.warn('获取座位状态失败:', error);
}
}
Modal.confirm({
title: '确认取消选座',
content: `确定要取消座位 ${record.seat_number} 的选座吗?`,
onOk: async () => {
try {
// 优先使用booking_id如果没有则使用seat_number
if (bookingId) {
await cancelBookingApi({ id: bookingId });
} else {
// 尝试使用座位号取消
await cancelBookingApi({
seat_number: record.seat_number,
classroomId: currentClassroomId.value
});
}
message.success('取消选座成功');
// 重新加载座位列表
if (currentClassroomId.value) {
const res = await getClassroomSeatListApi({ id: currentClassroomId.value });
if (res && (res.code === 0 || res.code === 200)) {
seatListData.value = Array.isArray(res.data) ? res.data : [];
selectedSeatKeys.value = [];
}
}
} catch (error: any) {
console.error('取消选座失败:', error);
message.error(error?.response?.data?.message || error?.message || '取消选座失败');
}
},
});
};
// 批量取消选座
const handleBatchCancelBooking = () => {
if (selectedSeatKeys.value.length === 0) {
message.warning('请选择要取消的座位');
return;
}
// 获取选中的已选座位
const selectedSeats = seatListData.value.filter(
(seat) => selectedSeatKeys.value.includes(seat.seat_number) && seat.is_selected === 1 && seat.booking_id
);
if (selectedSeats.length === 0) {
message.warning('请选择已选座的座位');
return;
}
const bookingIds = selectedSeats.map((seat) => seat.booking_id!).filter((id) => id);
if (bookingIds.length === 0) {
message.warning('选中的座位中没有有效的预订ID');
return;
}
Modal.confirm({
title: '确认批量取消选座',
content: `确定要取消 ${bookingIds.length} 个座位的选座吗?`,
onOk: async () => {
try {
await cancelBookingAutoApi({ ids: bookingIds });
message.success('批量取消选座成功');
// 重新加载座位列表
if (currentClassroomId.value) {
const res = await getClassroomSeatListApi({ id: currentClassroomId.value });
if (res && (res.code === 0 || res.code === 200)) {
seatListData.value = Array.isArray(res.data) ? res.data : [];
selectedSeatKeys.value = [];
}
}
} catch (error: any) {
console.error('批量取消选座失败:', error);
message.error(error?.response?.data?.message || error?.message || '批量取消选座失败');
}
},
});
};
// 分配座位相关
const assignSeatVisible = ref(false);
const assignSeatRecord = ref<ClassroomApi.SeatInfo | null>(null);
const unassignedStudents = ref<Array<{ id: number; name: string; phone?: string; student_id?: string | null }>>([]);
const unassignedStudentsLoading = ref(false);
const selectedStudentId = ref<number | undefined>(undefined);
// 打开分配座位弹窗
const handleAssignSeat = async (record: ClassroomApi.SeatInfo) => {
if (!currentClassroomId.value) {
message.error('教室ID不存在');
return;
}
assignSeatRecord.value = record;
assignSeatVisible.value = true;
selectedStudentId.value = undefined;
unassignedStudents.value = [];
unassignedStudentsLoading.value = true;
try {
const res = await getUnassignedStudentsApi({ classroomId: currentClassroomId.value });
if (res && (res.code === 0 || res.code === 200)) {
const data = res.data || res;
unassignedStudents.value = Array.isArray(data) ? data : [];
if (unassignedStudents.value.length === 0) {
message.warning('没有未分配座位的学生');
}
} else {
message.error(res?.message || res?.msg || '获取学生列表失败');
unassignedStudents.value = [];
}
} catch (error: any) {
console.error('获取学生列表失败:', error);
message.error(error?.response?.data?.message || error?.message || '获取学生列表失败');
unassignedStudents.value = [];
} finally {
unassignedStudentsLoading.value = false;
}
};
// 确认分配座位
const handleConfirmAssignSeat = async () => {
if (!assignSeatRecord.value || !selectedStudentId.value || !currentClassroomId.value) {
message.warning('请选择学生');
return;
}
if (!currentLayout.value) {
message.error('布局数据不存在,无法分配座位');
return;
}
// 从布局数据中找到座位的row和col
const seatNumber = assignSeatRecord.value.seat_number;
const cells = currentLayout.value.cells || [];
const seatCell = cells.find((cell) => cell.type === 'seat' && cell.number === seatNumber);
if (!seatCell) {
message.error('找不到对应的座位信息');
return;
}
try {
await assignSeatApi({
classroomId: currentClassroomId.value,
studentId: selectedStudentId.value,
row: seatCell.row,
col: seatCell.col,
number: seatNumber,
});
message.success('分配座位成功');
assignSeatVisible.value = false;
assignSeatRecord.value = null;
selectedStudentId.value = undefined;
// 重新加载座位列表
const res = await getClassroomSeatListApi({ id: currentClassroomId.value });
if (res && (res.code === 0 || res.code === 200)) {
seatListData.value = Array.isArray(res.data) ? res.data : [];
}
} catch (error: any) {
console.error('分配座位失败:', error);
message.error(error?.response?.data?.message || error?.message || '分配座位失败');
}
};
// 关闭分配座位弹窗
const handleCloseAssignSeat = () => {
assignSeatVisible.value = false;
assignSeatRecord.value = null;
selectedStudentId.value = undefined;
unassignedStudents.value = [];
};
// 导出座位列表
const handleExportSeatList = () => {
if (seatListData.value.length === 0) {
@@ -345,7 +848,28 @@ const handleExportSeatList = () => {
message.success('导出成功');
};
// 获取校区列表
const fetchSchoolList = async () => {
schoolListLoading.value = true;
try {
const res = await getClassroomSchoolListApi();
if (Array.isArray(res)) {
schoolList.value = res;
} else if (res && (res.code === 0 || res.code === 200)) {
schoolList.value = Array.isArray(res.data) ? res.data : [];
} else {
schoolList.value = [];
}
} catch (error) {
console.error('获取校区列表失败:', error);
schoolList.value = [];
} finally {
schoolListLoading.value = false;
}
};
onMounted(() => {
fetchSchoolList();
fetchData();
});
</script>
@@ -367,6 +891,27 @@ onMounted(() => {
style="width: 200px"
@press-enter="handleSearch"
/>
<Select
v-model:value="searchForm.school_id"
placeholder="校区"
style="width: 150px"
allow-clear
:loading="schoolListLoading"
show-search
:filter-option="(input, option) => {
const label = option?.label || '';
return label.toLowerCase().includes(input.toLowerCase());
}"
>
<Select.Option
v-for="item in schoolList"
:key="item.id"
:value="item.id"
:label="item.name || ''"
>
{{ item.name || `校区 ${item.id}` }}
</Select.Option>
</Select>
<Select
v-model:value="searchForm.type"
placeholder="教室类型"
@@ -426,9 +971,14 @@ onMounted(() => {
@cancel="handleCloseSeatList"
>
<div style="margin-bottom: 16px; text-align: right;">
<Space>
<Button type="primary" @click="handleExportLayout">
导出布局
</Button>
<Button type="primary" @click="handleExportSeatList">
导出
</Button>
</Space>
</div>
<Table
:columns="[
@@ -437,6 +987,7 @@ onMounted(() => {
{ title: '学员姓名', dataIndex: 'student_name', key: 'student_name' },
{ title: '学员手机号', dataIndex: 'student_mobile', key: 'student_mobile' },
{ title: '选座时间', dataIndex: 'select_time', key: 'select_time' },
{ title: '操作', key: 'action', width: 120, fixed: 'right' },
]"
:data-source="seatListData"
:loading="seatListLoading"
@@ -467,10 +1018,76 @@ onMounted(() => {
<template v-else-if="column.key === 'select_time'">
{{ record.select_time || '-' }}
</template>
<template v-else-if="column.key === 'action'">
<Space>
<Button
v-if="record.is_selected === 1"
type="link"
danger
size="small"
@click="handleCancelBooking(record)"
>
取消
</Button>
<Button
v-else
type="link"
size="small"
@click="handleAssignSeat(record)"
>
分配
</Button>
</Space>
</template>
</template>
</Table>
</Modal>
<!-- 分配座位弹窗 -->
<Modal
v-model:open="assignSeatVisible"
title="分配座位"
:footer="null"
@cancel="handleCloseAssignSeat"
>
<div style="margin-bottom: 16px;">
<div style="margin-bottom: 8px;">
<strong>座位号:</strong>{{ assignSeatRecord?.seat_number }}
</div>
<div style="margin-bottom: 16px;">
<strong>选择学生:</strong>
</div>
<Select
v-model:value="selectedStudentId"
placeholder="请选择学生"
style="width: 100%"
:loading="unassignedStudentsLoading"
show-search
:filter-option="(input, option) => {
const label = option?.label || '';
return label.toLowerCase().includes(input.toLowerCase());
}"
>
<Select.Option
v-for="student in unassignedStudents"
:key="student.id"
:value="student.id"
:label="`${student.name} (${student.phone || ''})`"
>
{{ student.name }} ({{ student.phone || '' }})
</Select.Option>
</Select>
</div>
<div style="text-align: right; margin-top: 16px;">
<Space>
<Button @click="handleCloseAssignSeat">取消</Button>
<Button type="primary" :disabled="!selectedStudentId" @click="handleConfirmAssignSeat">
确认分配
</Button>
</Space>
</div>
</Modal>
<!-- 座位布局弹窗 -->
<Modal
v-model:open="layoutVisible"

View File

@@ -2,13 +2,10 @@
import { ref, onMounted, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { Card, Form, Input, Button, message, Tabs, Table, Space, Modal, Radio } from 'ant-design-vue';
import { Card, Form, Input, Button, message, Tabs } from 'ant-design-vue';
import {
getSchoolDetailApi,
saveSchoolApi,
getSchoolAccountListApi,
saveSchoolAccountApi,
deleteSchoolAccountApi,
type SchoolApi
} from '#/api';
@@ -17,9 +14,7 @@ defineOptions({ name: 'SchoolDetail' });
const route = useRoute();
const router = useRouter();
const formRef = ref();
const accountFormRef = ref();
const loading = ref(false);
const accountLoading = ref(false);
const activeTab = ref('basic');
const formData = ref<Partial<SchoolApi.SaveParams>>({
name: '',
@@ -29,18 +24,7 @@ const formData = ref<Partial<SchoolApi.SaveParams>>({
description: '',
});
// 账号管理相关
const accountList = ref<SchoolApi.AccountInfo[]>([]);
const accountFormData = ref<Partial<SchoolApi.SaveAccountParams>>({
username: '',
password: '',
status: 1,
});
const accountModalVisible = ref(false);
const isAccountEdit = ref(false);
const isEdit = computed(() => !!route.params.id);
const schoolId = computed(() => isEdit.value ? Number(route.params.id) : 0);
const handleSubmit = async () => {
try {
@@ -92,114 +76,9 @@ const fetchDetail = async () => {
}
};
// 账号管理相关方法
const accountColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80,
},
{
title: '用户名',
dataIndex: 'username',
key: 'username',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100,
},
{
title: '操作',
key: 'action',
width: 200,
},
];
const fetchAccountList = async () => {
if (!isEdit.value) return;
accountLoading.value = true;
try {
const res = await getSchoolAccountListApi({ school_id: schoolId.value });
// 支持 code 为 0 或 200 的成功响应
if (res && (res.code === 0 || res.code === 200)) {
// 根据实际返回格式,数据在 res.data 数组中
const data = Array.isArray(res.data) ? res.data : [];
accountList.value = data;
} else {
accountList.value = [];
}
} catch (error) {
console.error('获取账号列表失败:', error);
accountList.value = [];
} finally {
accountLoading.value = false;
}
};
const handleAddAccount = () => {
isAccountEdit.value = false;
accountFormData.value = {
username: '',
password: '',
status: 1,
};
accountModalVisible.value = true;
};
const handleEditAccount = (record: SchoolApi.AccountInfo) => {
isAccountEdit.value = true;
accountFormData.value = {
id: record.id,
username: record.username || '',
password: '', // 编辑时不显示密码
status: record.status !== undefined ? record.status : 1,
};
accountModalVisible.value = true;
};
const handleDeleteAccount = (record: SchoolApi.AccountInfo) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除账号"${record.username}"吗?`,
onOk: async () => {
try {
await deleteSchoolAccountApi({ id: record.id! });
message.success('删除成功');
fetchAccountList();
} catch (error) {
console.error('删除失败:', error);
}
},
});
};
const handleAccountSubmit = async () => {
try {
await accountFormRef.value.validate();
accountLoading.value = true;
const data = {
...accountFormData.value,
school_id: schoolId.value,
};
await saveSchoolAccountApi(data);
message.success(isAccountEdit.value ? '更新成功' : '创建成功');
accountModalVisible.value = false;
fetchAccountList();
} catch (error: any) {
console.error('保存账号失败:', error);
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存账号失败');
} finally {
accountLoading.value = false;
}
};
onMounted(() => {
if (isEdit.value) {
fetchDetail();
fetchAccountList();
}
});
</script>
@@ -240,78 +119,9 @@ onMounted(() => {
</Form.Item>
</Form>
</Tabs.TabPane>
<Tabs.TabPane key="account" tab="账号管理" :disabled="!isEdit">
<div class="mb-4">
<Button type="primary" @click="handleAddAccount">添加账号</Button>
</div>
<Table
:columns="accountColumns"
:data-source="accountList"
:loading="accountLoading"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
{{ record.status === 1 ? '启用' : '禁用' }}
</template>
<template v-else-if="column.key === 'action'">
<Space>
<Button type="link" size="small" @click="handleEditAccount(record)">编辑</Button>
<Button type="link" danger size="small" @click="handleDeleteAccount(record)">删除</Button>
</Space>
</template>
</template>
</Table>
</Tabs.TabPane>
</Tabs>
</Card>
<!-- 账号编辑弹窗 -->
<Modal
v-model:open="accountModalVisible"
:title="isAccountEdit ? '编辑账号' : '新增账号'"
:confirm-loading="accountLoading"
@ok="handleAccountSubmit"
@cancel="accountModalVisible = false"
>
<Form
ref="accountFormRef"
:model="accountFormData"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<Form.Item
label="用户名"
name="username"
:rules="[{ required: true, message: '请输入用户名' }]"
>
<Input
v-model:value="accountFormData.username"
placeholder="请输入用户名"
:disabled="isAccountEdit"
/>
</Form.Item>
<Form.Item
label="密码"
name="password"
:rules="[{ required: !isAccountEdit, message: '请输入密码' }]"
>
<Input.Password
v-model:value="accountFormData.password"
placeholder="请输入密码"
/>
<div v-if="isAccountEdit" style="margin-top: 4px; font-size: 12px; color: #999;">
留空则不修改密码
</div>
</Form.Item>
<Form.Item label="状态" name="status">
<Radio.Group v-model:value="accountFormData.status">
<Radio :value="1">启用</Radio>
<Radio :value="0">禁用</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
</Page>
</template>

View File

@@ -2,10 +2,13 @@
import { ref, reactive, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import { Button, Card, Table, Space, message, Modal, Input } from 'ant-design-vue';
import { Button, Card, Table, Space, message, Modal, Input, Form, Radio } from 'ant-design-vue';
import {
getSchoolListApi,
deleteSchoolAccountApi,
deleteSchoolApi,
getSchoolAccountListApi,
getSchoolListApi,
saveSchoolAccountApi,
type SchoolApi
} from '#/api';
@@ -58,7 +61,7 @@ const columns = [
{
title: '操作',
key: 'action',
width: 200,
width: 280,
},
];
@@ -117,6 +120,113 @@ const handleDelete = (record: SchoolApi.SchoolInfo) => {
});
};
// 账号管理
const accountLoading = ref(false);
const accountModalVisible = ref(false);
const accountFormRef = ref();
const accountList = ref<SchoolApi.AccountInfo[]>([]);
const accountColumns = [
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '状态', dataIndex: 'status', key: 'status', width: 100 },
{ title: '操作', key: 'action', width: 200 },
];
const currentSchool = ref<SchoolApi.SchoolInfo | null>(null);
const isAccountEdit = ref(false);
const accountFormData = ref<Partial<SchoolApi.SaveAccountParams>>({
username: '',
password: '',
status: 1,
});
const fetchAccountList = async (schoolId: number) => {
accountLoading.value = true;
try {
const res = await getSchoolAccountListApi({ school_id: schoolId });
if (res && (res.code === 0 || res.code === 200)) {
accountList.value = Array.isArray(res.data) ? res.data : [];
} else {
accountList.value = [];
}
} catch (error) {
console.error('获取账号列表失败:', error);
accountList.value = [];
} finally {
accountLoading.value = false;
}
};
const openAccountModal = (record: SchoolApi.SchoolInfo) => {
currentSchool.value = record;
isAccountEdit.value = false;
accountFormData.value = {
username: '',
password: '',
status: 1,
};
fetchAccountList(record.id!);
accountModalVisible.value = true;
};
const handleAddAccount = () => {
isAccountEdit.value = false;
accountFormData.value = {
username: '',
password: '',
status: 1,
};
};
const handleEditAccount = (record: SchoolApi.AccountInfo) => {
isAccountEdit.value = true;
accountFormData.value = {
id: record.id,
username: record.username || '',
password: '',
status: record.status !== undefined ? record.status : 1,
};
};
const handleDeleteAccount = (record: SchoolApi.AccountInfo) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除账号"${record.username}"吗?`,
onOk: async () => {
if (!record.id) return;
try {
await deleteSchoolAccountApi({ id: record.id });
message.success('删除成功');
if (currentSchool.value?.id) {
fetchAccountList(currentSchool.value.id);
}
} catch (error) {
console.error('删除失败:', error);
}
},
});
};
const handleAccountSubmit = async () => {
if (!currentSchool.value?.id) return;
try {
await accountFormRef.value.validate();
accountLoading.value = true;
const data = {
...accountFormData.value,
school_id: currentSchool.value.id,
};
await saveSchoolAccountApi(data);
message.success(isAccountEdit.value ? '更新成功' : '创建成功');
fetchAccountList(currentSchool.value.id);
accountModalVisible.value = false;
} catch (error: any) {
console.error('保存账号失败:', error);
message.error(error?.response?.data?.message || error?.response?.data?.msg || '保存账号失败');
} finally {
accountLoading.value = false;
}
};
const handleTableChange = (pag: any) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
@@ -171,6 +281,7 @@ onMounted(() => {
</template>
<template v-else-if="column.key === 'action'">
<Space>
<Button type="link" size="small" @click="openAccountModal(record)">账号管理</Button>
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
</Space>
@@ -178,6 +289,74 @@ onMounted(() => {
</template>
</Table>
</Card>
<Modal
v-model:open="accountModalVisible"
:title="`${currentSchool?.name || '账号管理'}`"
:confirm-loading="accountLoading"
width="720px"
@ok="handleAccountSubmit"
@cancel="accountModalVisible = false"
>
<div class="mb-3">
<Button type="primary" size="small" @click="handleAddAccount">新增账号</Button>
</div>
<Table
:columns="accountColumns"
:data-source="accountList"
:loading="accountLoading"
size="small"
row-key="id"
:pagination="false"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
{{ record.status === 1 ? '启用' : '禁用' }}
</template>
<template v-else-if="column.key === 'action'">
<Space>
<Button type="link" size="small" @click="handleEditAccount(record)">编辑</Button>
<Button type="link" danger size="small" @click="handleDeleteAccount(record)">删除</Button>
</Space>
</template>
</template>
</Table>
<Form
ref="accountFormRef"
:model="accountFormData"
class="mt-4"
layout="vertical"
>
<Form.Item
label="用户名"
name="username"
:rules="[{ required: true, message: '请输入用户名' }]"
>
<Input
v-model:value="accountFormData.username"
placeholder="请输入用户名"
:disabled="isAccountEdit"
/>
</Form.Item>
<Form.Item
label="密码"
name="password"
:rules="[{ required: !isAccountEdit, message: '请输入密码' }]"
>
<Input.Password v-model:value="accountFormData.password" placeholder="请输入密码" />
<div v-if="isAccountEdit" style="margin-top: 4px; font-size: 12px; color: #999;">
留空则不修改密码
</div>
</Form.Item>
<Form.Item label="状态" name="status">
<Radio.Group v-model:value="accountFormData.status">
<Radio :value="1">启用</Radio>
<Radio :value="0">禁用</Radio>
</Radio.Group>
</Form.Item>
</Form>
</Modal>
</Page>
</template>

View File

@@ -19,7 +19,6 @@ const loading = ref(false);
const classList = ref<ClassApi.ClassInfo[]>([]);
const classListLoading = ref(false);
const formData = ref<Partial<StudentApi.SaveParams>>({
student_id: '',
name: '',
gender: undefined,
class_id: undefined,
@@ -124,14 +123,14 @@ onMounted(() => {
:wrapper-col="{ span: 20 }"
>
<Form.Item
v-if="isEdit"
label="学员ID"
name="student_id"
:rules="[{ required: true, message: '请输入学员ID' }]"
>
<Input
v-model:value="formData.student_id as string"
:placeholder="isEdit ? '学员ID' : '请输入学员ID'"
:disabled="isEdit"
placeholder="学员ID"
disabled
/>
</Form.Item>
<Form.Item

View File

@@ -179,9 +179,11 @@ const uploadedFile = ref<string>('');
const selectedClassId = ref<number | undefined>(undefined);
const classList = ref<ClassApi.ClassInfo[]>([]);
const fileList = ref<any[]>([]);
const classListLoading = ref(false);
// 获取班级列表
const fetchClassList = async () => {
classListLoading.value = true;
try {
const res = await getClassListApi({ page: 1, limit: 1000 });
if (res && (res.code === 0 || res.code === 200)) {
@@ -196,7 +198,10 @@ const fetchClassList = async () => {
classList.value = [];
}
} catch (error) {
console.error('获取班级列表失败:', error);
classList.value = [];
} finally {
classListLoading.value = false;
}
};
@@ -309,6 +314,7 @@ const handleCancelImport = () => {
};
onMounted(() => {
fetchClassList();
fetchData();
});
</script>
@@ -339,6 +345,27 @@ onMounted(() => {
allow-clear
@press-enter="handleSearch"
/>
<Select
v-model:value="searchForm.class_id"
placeholder="班级"
style="width: 150px"
allow-clear
:loading="classListLoading"
show-search
:filter-option="(input, option) => {
const label = option?.label || '';
return label.toLowerCase().includes(input.toLowerCase());
}"
>
<Select.Option
v-for="item in classList"
:key="item.id"
:value="item.id"
:label="item.name || ''"
>
{{ item.name || `班级 ${item.id}` }}
</Select.Option>
</Select>
<Select
v-model:value="searchForm.bind_status"
placeholder="绑定状态"

View File

@@ -7,7 +7,7 @@ export default defineConfig(async () => {
server: {
proxy: {
'/api': {
target: 'http://xz.dhdjy.com',
target: 'https://xz.dhdjy.com',
changeOrigin: true,
secure: true, // https 接口设为 true
ws: true,

View File

@@ -0,0 +1,74 @@
# 座位布局导出功能说明
## 功能概述
在座位布局弹窗中新增了"导出"按钮可以将座位布局导出为Excel格式文件包含完整的布局信息和座位状态。
## 功能特点
1. **完整的布局信息**:导出包含所有座位、过道、柱子、投影等布局元素
2. **座位状态显示**
- **蓝色单元格**:已预订座位(显示编号、学员姓名、预订日期)
- **绿色单元格**:空闲座位(显示编号和"空闲"标识)
- **红色单元格**:不可选座位(显示编号和"不可选"标识)
- **橙色单元格**:过道
- **蓝色单元格**:投影区域
- **灰色单元格**:柱子
- **浅蓝色单元格**:门
3. **样式美化**Excel文件包含颜色填充、字体样式、对齐方式等
4. **合并单元格支持**:正确导出合并的单元格
## 使用方法
1. 在教室列表页面,点击某个教室的"座位布局"按钮
2. 在座位布局弹窗的右上角,点击"导 出"按钮
3. 系统会自动生成Excel文件并下载文件名格式`教室名称_座位布局_日期.xlsx`
## 技术实现
### 依赖库
- `xlsx-js-style`用于生成带样式的Excel文件
- `xlsx`基础Excel处理库
### 安装方式
如果安装失败可以尝试使用不同的npm源
```bash
# 使用淘宝镜像
pnpm add xlsx-js-style --registry https://registry.npmmirror.com
# 或使用官方源
pnpm add xlsx-js-style --registry https://registry.npmjs.org
```
### 代码位置
- 导出功能实现:`apps/web-antd/src/views/classroom/list.vue`
- 导出按钮位置:座位布局弹窗顶部右侧
## 导出格式说明
### Excel结构
- **第一行**列标题A, B, C, D...
- **第一列**行号1, 2, 3, 4...
- **数据区域**:座位布局数据,包含颜色和样式
### 单元格内容格式
- **已预订座位**`编号 学员姓名 预订日期`(蓝色背景)
- **空闲座位**`编号 空闲`(绿色背景)
- **不可选座位**`编号 不可选`(红色背景)
- **过道**`过道`(橙色背景)
- **投影**`投影`(蓝色背景)
- **柱子**`柱子`(灰色背景)
- **门**`门`(浅蓝色背景)
## 注意事项
1. 导出功能需要获取座位状态数据,如果获取失败,将只导出布局信息(不包含预订状态)
2. 合并单元格会正确导出,保持与界面一致的布局
3. Excel文件支持在Excel、WPS等软件中打开和编辑

View File

@@ -12,7 +12,7 @@ export const VBEN_DOC_URL = 'https://doc.vben.pro';
* @zh_CN Vben Logo
*/
export const VBEN_LOGO_URL =
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp';
'https://xz.dhdjy.com/uploads/banner/20251208/4327f4acabebc22064f652d18976cf33.png';
/**
* @zh_CN Vben Admin 首页地址

View File

@@ -64,7 +64,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
"logo": {
"enable": true,
"fit": "contain",
"source": "https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp",
"source": "https://xz.dhdjy.com/uploads/banner/20251208/4327f4acabebc22064f652d18976cf33.png",
},
"navigation": {
"accordion": true,

View File

@@ -65,7 +65,7 @@ const defaultPreferences: Preferences = {
logo: {
enable: true,
fit: 'contain',
source: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
source: 'https://xz.dhdjy.com/uploads/banner/20251208/4327f4acabebc22064f652d18976cf33.png',
},
navigation: {
accordion: true,

View File

@@ -14,7 +14,6 @@ import { useVbenForm } from '@vben-core/form-ui';
import { VbenButton, VbenCheckbox } from '@vben-core/shadcn-ui';
import Title from './auth-title.vue';
import ThirdPartyLogin from './third-party-login.vue';
interface Props extends AuthenticationProps {
formSchema?: VbenFormSchema[];
@@ -123,14 +122,6 @@ defineExpose({
{{ $t('authentication.rememberMe') }}
</VbenCheckbox>
</div>
<span
v-if="showForgetPassword"
class="vben-link text-sm font-normal"
@click="handleGo(forgetPasswordPath)"
>
{{ $t('authentication.forgetPassword') }}
</span>
</div>
<VbenButton
:class="{
@@ -143,44 +134,5 @@ defineExpose({
>
{{ submitButtonText || $t('common.login') }}
</VbenButton>
<div
v-if="showCodeLogin || showQrcodeLogin"
class="mb-2 mt-4 flex items-center justify-between"
>
<VbenButton
v-if="showCodeLogin"
class="w-1/2"
variant="outline"
@click="handleGo(codeLoginPath)"
>
{{ $t('authentication.mobileLogin') }}
</VbenButton>
<VbenButton
v-if="showQrcodeLogin"
class="ml-4 w-1/2"
variant="outline"
@click="handleGo(qrCodeLoginPath)"
>
{{ $t('authentication.qrcodeLogin') }}
</VbenButton>
</div>
<!-- 第三方登录 -->
<slot name="third-party-login">
<ThirdPartyLogin v-if="showThirdPartyLogin" />
</slot>
<slot name="to-register">
<div v-if="showRegister" class="mt-3 text-center text-sm">
{{ $t('authentication.accountTip') }}
<span
class="vben-link text-sm font-normal"
@click="handleGo(registerPath)"
>
{{ $t('authentication.createAccount') }}
</span>
</div>
</slot>
</div>
</template>

View File

@@ -31,18 +31,5 @@ withDefaults(defineProps<Props>(), {
>
{{ icp }}
</a>
<!-- Copyright Text -->
Copyright © {{ date }}
<!-- Company Link -->
<a
v-if="companyName"
:href="companySiteLink || 'javascript:void(0)'"
class="hover:text-primary-hover mx-1"
target="_blank"
>
{{ companyName }}
</a>
</div>
</template>

View File

@@ -1,7 +1,7 @@
{
"welcomeBack": "欢迎回来",
"pageTitle": "开箱即用的大型中后台管理系统",
"pageDesc": "工程化、高性能、跨组件库的前端模版",
"pageTitle": "东汇达教育座位预定系统",
"pageDesc": "内部选座专用系统",
"loginSuccess": "登录成功",
"loginSuccessDesc": "欢迎回来",
"loginSubtitle": "请输入您的帐户信息以开始管理您的项目",

View File

@@ -8,7 +8,7 @@ import { requestClient } from '../request';
*/
async function downloadFile1() {
return requestClient.download<Blob>(
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
'https://xz.dhdjy.com/uploads/banner/20251208/4327f4acabebc22064f652d18976cf33.png',
);
}
@@ -18,7 +18,7 @@ async function downloadFile1() {
*/
async function downloadFile2() {
return requestClient.download<RequestResponse<Blob>>(
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
'https://xz.dhdjy.com/uploads/banner/20251208/4327f4acabebc22064f652d18976cf33.png',
{
responseReturn: 'raw',
},

View File

@@ -53,7 +53,7 @@ function getResponse() {
@click="
downloadFileFromImageUrl({
source:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
'https://xz.dhdjy.com/uploads/banner/20251208/4327f4acabebc22064f652d18976cf33.png',
fileName: 'vben-logo.png',
})
"

View File

@@ -400,7 +400,7 @@ function handleSetFormValue() {
name: 'example.png',
status: 'done',
uid: '-1',
url: 'https://unpkg.com/@vbenjs/static-source@0.1.7/source/logo-v1.webp',
url: 'https://xz.dhdjy.com/uploads/banner/20251208/4327f4acabebc22064f652d18976cf33.png',
},
],
mentions: '@afc163',

1969
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff