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
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:
157
README.ja-JP.md
157
README.ja-JP.md
@@ -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)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**日本語** | [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を使用
|
||||
|
||||
Gitpod(GitHub用の無料オンライン開発環境)でプロジェクトを開き、すぐにコーディングを開始します。
|
||||
|
||||
[](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)
|
||||
|
||||
## 貢献方法
|
||||
|
||||
ご参加をお待ちしておりますするか、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)
|
||||
|
||||
## スター歴史
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## 寄付
|
||||
|
||||
このプロジェクトが役に立つと思われた場合、作者にコーヒーを一杯おごってサポートを示すことができます!
|
||||
|
||||

|
||||
|
||||
<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
157
README.md
@@ -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)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/codeql.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/build.yml) [](https://github.com/vbenjs/vue-vben-admin/actions/workflows/ci.yml) [](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.
|
||||
|
||||
[](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
|
||||
|
||||
[](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!
|
||||
|
||||

|
||||
|
||||
<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)
|
||||
157
README.zh-CN.md
157
README.zh-CN.md
@@ -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)
|
||||
|
||||
<h1>Vue Vben Admin</h1>
|
||||
</div>
|
||||
|
||||
[](https://sonarcloud.io/summary/new_code?id=vbenjs_vue-vben-admin)    
|
||||
|
||||
**中文** | [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 的免费在线开发环境)中打开项目,并立即开始编码。
|
||||
|
||||
[](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)
|
||||
|
||||
## 如何贡献
|
||||
|
||||
非常欢迎你的加入 或者提交一个 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 历史
|
||||
|
||||
[](https://star-history.com/#vbenjs/vue-vben-admin&Date)
|
||||
|
||||
## 捐赠
|
||||
|
||||
如果你觉得这个项目对你有帮助,你可以帮作者买一杯咖啡表示支持!
|
||||
|
||||

|
||||
|
||||
<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)
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export * from './classroom';
|
||||
export * from './booking';
|
||||
export * from './student';
|
||||
export * from './class';
|
||||
export * from './teacher';
|
||||
|
||||
@@ -16,10 +16,6 @@
|
||||
"list": "教室列表",
|
||||
"detail": "教室详情"
|
||||
},
|
||||
"booking": {
|
||||
"title": "预订管理",
|
||||
"list": "预订列表"
|
||||
},
|
||||
"student": {
|
||||
"title": "学生管理",
|
||||
"list": "学生列表",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -53,7 +53,6 @@ const columns = [
|
||||
{
|
||||
title: '教室',
|
||||
key: 'classroom_name',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '学生人数',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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="绑定状态"
|
||||
|
||||
@@ -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,
|
||||
|
||||
74
doc/座位布局导出功能说明.md
Normal file
74
doc/座位布局导出功能说明.md
Normal 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等软件中打开和编辑
|
||||
|
||||
@@ -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 首页地址
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"welcomeBack": "欢迎回来",
|
||||
"pageTitle": "开箱即用的大型中后台管理系统",
|
||||
"pageDesc": "工程化、高性能、跨组件库的前端模版",
|
||||
"pageTitle": "东汇达教育座位预定系统",
|
||||
"pageDesc": "内部选座专用系统",
|
||||
"loginSuccess": "登录成功",
|
||||
"loginSuccessDesc": "欢迎回来",
|
||||
"loginSubtitle": "请输入您的帐户信息以开始管理您的项目",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
"
|
||||
|
||||
@@ -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
1969
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user