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_BASE=/super/
|
||||||
VITE_GLOB_API_URL=http://xz.dhdjy.com
|
VITE_GLOB_API_URL=https://xz.dhdjy.com
|
||||||
VITE_ROUTER_HISTORY=history
|
VITE_ROUTER_HISTORY=history
|
||||||
VITE_COMPRESS=gzip
|
VITE_COMPRESS=gzip
|
||||||
VITE_PWA=false
|
VITE_PWA=false
|
||||||
|
|||||||
@@ -45,6 +45,8 @@
|
|||||||
"dayjs": "catalog:",
|
"dayjs": "catalog:",
|
||||||
"pinia": "catalog:",
|
"pinia": "catalog:",
|
||||||
"vue": "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 {
|
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_name: string;
|
||||||
student_mobile: string;
|
student_mobile: string;
|
||||||
select_time: 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
|
* GET /api/admin/classroom/getUnassignedStudents
|
||||||
*/
|
*/
|
||||||
export async function getUnassignedStudentsApi(params: ClassroomApi.UnassignedStudentsParams) {
|
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 './classroom';
|
||||||
export * from './booking';
|
|
||||||
export * from './student';
|
export * from './student';
|
||||||
export * from './class';
|
export * from './class';
|
||||||
export * from './teacher';
|
export * from './teacher';
|
||||||
|
|||||||
@@ -16,10 +16,6 @@
|
|||||||
"list": "教室列表",
|
"list": "教室列表",
|
||||||
"detail": "教室详情"
|
"detail": "教室详情"
|
||||||
},
|
},
|
||||||
"booking": {
|
|
||||||
"title": "预订管理",
|
|
||||||
"list": "预订列表"
|
|
||||||
},
|
|
||||||
"student": {
|
"student": {
|
||||||
"title": "学生管理",
|
"title": "学生管理",
|
||||||
"list": "学生列表",
|
"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>
|
<script lang="ts" setup>
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { Recordable } from '@vben/types';
|
|
||||||
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ const formSchema = computed((): VbenFormSchema[] => {
|
|||||||
* Asynchronously handle the login process
|
* Asynchronously handle the login process
|
||||||
* @param values 登录表单数据
|
* @param values 登录表单数据
|
||||||
*/
|
*/
|
||||||
async function handleLogin(values: Recordable<any>) {
|
async function handleLogin() {
|
||||||
// 处理验证码登录
|
// 处理验证码登录
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { VbenFormSchema } from '@vben/common-ui';
|
import type { VbenFormSchema } from '@vben/common-ui';
|
||||||
import type { BasicOption } from '@vben/types';
|
|
||||||
|
|
||||||
import { computed, markRaw } from 'vue';
|
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>
|
|
||||||
|
|
||||||
@@ -3,10 +3,10 @@ import { ref, reactive, onMounted } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Page } from '@vben/common-ui';
|
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 } from 'ant-design-vue';
|
||||||
import {
|
import {
|
||||||
getClassListApi,
|
getClassListApi,
|
||||||
deleteClassApi,
|
deleteClassApi,
|
||||||
type ClassApi
|
type ClassApi
|
||||||
} from '#/api';
|
} from '#/api';
|
||||||
|
|
||||||
defineOptions({ name: 'ClassList' });
|
defineOptions({ name: 'ClassList' });
|
||||||
@@ -53,7 +53,6 @@ const columns = [
|
|||||||
{
|
{
|
||||||
title: '教室',
|
title: '教室',
|
||||||
key: 'classroom_name',
|
key: 'classroom_name',
|
||||||
width: 150,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '学生人数',
|
title: '学生人数',
|
||||||
@@ -114,8 +113,8 @@ const handleAdd = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (record: ClassApi.ClassInfo) => {
|
const handleEdit = (record: ClassApi.ClassInfo) => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'ClassDetail',
|
name: 'ClassDetail',
|
||||||
params: { id: record.id }
|
params: { id: record.id }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { Page } from '@vben/common-ui';
|
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 {
|
import {
|
||||||
getClassroomDetailApi,
|
getClassroomDetailApi,
|
||||||
saveClassroomApi,
|
saveClassroomApi,
|
||||||
@@ -24,7 +25,10 @@ const schoolList = ref<SchoolApi.SchoolInfo[]>([]);
|
|||||||
const teacherList = ref<TeacherApi.TeacherInfo[]>([]);
|
const teacherList = ref<TeacherApi.TeacherInfo[]>([]);
|
||||||
const schoolListLoading = ref(false);
|
const schoolListLoading = ref(false);
|
||||||
const teacherListLoading = 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: '',
|
name: '',
|
||||||
building: '',
|
building: '',
|
||||||
floor: '',
|
floor: '',
|
||||||
@@ -35,6 +39,8 @@ const formData = ref<ClassroomApi.SaveParams>({
|
|||||||
teacher_id: undefined,
|
teacher_id: undefined,
|
||||||
description: '',
|
description: '',
|
||||||
status: 1,
|
status: 1,
|
||||||
|
fiexd_start_time: null,
|
||||||
|
fiexd_end_time: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isEdit = computed(() => !!route.params.id);
|
const isEdit = computed(() => !!route.params.id);
|
||||||
@@ -60,6 +66,13 @@ const handleSubmit = async () => {
|
|||||||
delete data.floor;
|
delete data.floor;
|
||||||
delete data.room;
|
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);
|
await saveClassroomApi(data);
|
||||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||||
router.back();
|
router.back();
|
||||||
@@ -93,6 +106,8 @@ const fetchDetail = async () => {
|
|||||||
teacher_id: data.teacher_id,
|
teacher_id: data.teacher_id,
|
||||||
description: data.description || '',
|
description: data.description || '',
|
||||||
status: data.status !== undefined ? data.status : 1,
|
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 {
|
} else {
|
||||||
message.error(res?.message || res?.msg || '获取教室详情失败');
|
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(() => {
|
onMounted(() => {
|
||||||
fetchSchoolList();
|
fetchSchoolList();
|
||||||
fetchTeacherList();
|
fetchTeacherList();
|
||||||
@@ -253,6 +279,35 @@ onMounted(() => {
|
|||||||
<Form.Item label="描述" name="description">
|
<Form.Item label="描述" name="description">
|
||||||
<Input.TextArea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
<Input.TextArea v-model:value="formData.description" placeholder="请输入描述" :rows="3" />
|
||||||
</Form.Item>
|
</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">
|
<Form.Item label="状态" name="status">
|
||||||
<Radio.Group v-model:value="formData.status">
|
<Radio.Group v-model:value="formData.status">
|
||||||
<Radio :value="1">启用</Radio>
|
<Radio :value="1">启用</Radio>
|
||||||
|
|||||||
@@ -9,9 +9,16 @@ import {
|
|||||||
batchDeleteClassroomApi,
|
batchDeleteClassroomApi,
|
||||||
getClassroomSeatListApi,
|
getClassroomSeatListApi,
|
||||||
getClassroomDetailApi,
|
getClassroomDetailApi,
|
||||||
|
getClassroomSchoolListApi,
|
||||||
|
getClassroomStatusApi,
|
||||||
|
cancelBookingApi,
|
||||||
|
cancelBookingAutoApi,
|
||||||
|
assignSeatApi,
|
||||||
|
getUnassignedStudentsApi,
|
||||||
type ClassroomApi
|
type ClassroomApi
|
||||||
} from '#/api';
|
} from '#/api';
|
||||||
import SeatLayoutEditor from '#/components/classroom/SeatLayoutEditor.vue';
|
import SeatLayoutEditor from '#/components/classroom/SeatLayoutEditor.vue';
|
||||||
|
import * as XLSX from 'xlsx-js-style';
|
||||||
|
|
||||||
|
|
||||||
defineOptions({ name: 'ClassroomList' });
|
defineOptions({ name: 'ClassroomList' });
|
||||||
@@ -41,10 +48,13 @@ const seatListPagination = reactive({
|
|||||||
current: 1,
|
current: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
const selectedSeatKeys = ref<string[]>([]);
|
||||||
const layoutVisible = ref(false);
|
const layoutVisible = ref(false);
|
||||||
const layoutLoading = ref(false);
|
const layoutLoading = ref(false);
|
||||||
const currentClassroomId = ref<number | null>(null);
|
const currentClassroomId = ref<number | null>(null);
|
||||||
const currentLayout = ref<ClassroomApi.ClassroomLayout | null>(null);
|
const currentLayout = ref<ClassroomApi.ClassroomLayout | null>(null);
|
||||||
|
const schoolList = ref<Array<{ id: number; name: string }>>([]);
|
||||||
|
const schoolListLoading = ref(false);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -59,27 +69,32 @@ const columns = [
|
|||||||
key: 'name',
|
key: 'name',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '楼栋',
|
title: '校区',
|
||||||
dataIndex: 'building',
|
dataIndex: 'school_name',
|
||||||
key: 'building',
|
key: 'school_name',
|
||||||
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
||||||
return record.location?.building || record.building || '-';
|
return (record as any).school_name || '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '楼层',
|
title: '地址',
|
||||||
dataIndex: 'floor',
|
dataIndex: 'address',
|
||||||
key: 'floor',
|
key: 'address',
|
||||||
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
||||||
return record.location?.floor || record.floor || '-';
|
const building = record.location?.building || record.building || '';
|
||||||
},
|
const floor = record.location?.floor || record.floor || '';
|
||||||
},
|
const room = record.location?.room || '';
|
||||||
{
|
const addressParts: string[] = [];
|
||||||
title: '房间号',
|
if (building && building.trim() !== '') {
|
||||||
dataIndex: 'room',
|
addressParts.push(building);
|
||||||
key: 'room',
|
}
|
||||||
customRender: ({ record }: { record: ClassroomApi.ClassroomInfo }) => {
|
if (floor && floor.trim() !== '') {
|
||||||
return record.location?.room || '-';
|
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;
|
seatListVisible.value = true;
|
||||||
|
currentClassroomId.value = record.id;
|
||||||
currentClassroomName.value = record.name || '';
|
currentClassroomName.value = record.name || '';
|
||||||
seatListLoading.value = true;
|
seatListLoading.value = true;
|
||||||
seatListData.value = [];
|
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 {
|
try {
|
||||||
const res = await getClassroomSeatListApi({ id: record.id });
|
const res = await getClassroomSeatListApi({ id: record.id });
|
||||||
if (res && (res.code === 0 || res.code === 200)) {
|
if (res && (res.code === 0 || res.code === 200)) {
|
||||||
@@ -288,6 +316,273 @@ const handleLayoutSaved = (layout: ClassroomApi.ClassroomLayout) => {
|
|||||||
// handleCloseLayout();
|
// 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) => {
|
const handleSeatListTableChange = (pag: any) => {
|
||||||
if (pag) {
|
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 = () => {
|
const handleExportSeatList = () => {
|
||||||
if (seatListData.value.length === 0) {
|
if (seatListData.value.length === 0) {
|
||||||
@@ -345,7 +848,28 @@ const handleExportSeatList = () => {
|
|||||||
message.success('导出成功');
|
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(() => {
|
onMounted(() => {
|
||||||
|
fetchSchoolList();
|
||||||
fetchData();
|
fetchData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -367,6 +891,27 @@ onMounted(() => {
|
|||||||
style="width: 200px"
|
style="width: 200px"
|
||||||
@press-enter="handleSearch"
|
@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
|
<Select
|
||||||
v-model:value="searchForm.type"
|
v-model:value="searchForm.type"
|
||||||
placeholder="教室类型"
|
placeholder="教室类型"
|
||||||
@@ -426,9 +971,14 @@ onMounted(() => {
|
|||||||
@cancel="handleCloseSeatList"
|
@cancel="handleCloseSeatList"
|
||||||
>
|
>
|
||||||
<div style="margin-bottom: 16px; text-align: right;">
|
<div style="margin-bottom: 16px; text-align: right;">
|
||||||
<Button type="primary" @click="handleExportSeatList">
|
<Space>
|
||||||
导出
|
<Button type="primary" @click="handleExportLayout">
|
||||||
</Button>
|
导出布局
|
||||||
|
</Button>
|
||||||
|
<Button type="primary" @click="handleExportSeatList">
|
||||||
|
导出
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
<Table
|
<Table
|
||||||
:columns="[
|
:columns="[
|
||||||
@@ -437,6 +987,7 @@ onMounted(() => {
|
|||||||
{ title: '学员姓名', dataIndex: 'student_name', key: 'student_name' },
|
{ title: '学员姓名', dataIndex: 'student_name', key: 'student_name' },
|
||||||
{ title: '学员手机号', dataIndex: 'student_mobile', key: 'student_mobile' },
|
{ title: '学员手机号', dataIndex: 'student_mobile', key: 'student_mobile' },
|
||||||
{ title: '选座时间', dataIndex: 'select_time', key: 'select_time' },
|
{ title: '选座时间', dataIndex: 'select_time', key: 'select_time' },
|
||||||
|
{ title: '操作', key: 'action', width: 120, fixed: 'right' },
|
||||||
]"
|
]"
|
||||||
:data-source="seatListData"
|
:data-source="seatListData"
|
||||||
:loading="seatListLoading"
|
:loading="seatListLoading"
|
||||||
@@ -467,10 +1018,76 @@ onMounted(() => {
|
|||||||
<template v-else-if="column.key === 'select_time'">
|
<template v-else-if="column.key === 'select_time'">
|
||||||
{{ record.select_time || '-' }}
|
{{ record.select_time || '-' }}
|
||||||
</template>
|
</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>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
</Modal>
|
</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
|
<Modal
|
||||||
v-model:open="layoutVisible"
|
v-model:open="layoutVisible"
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
import { ref, onMounted, computed } from 'vue';
|
import { ref, onMounted, computed } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { Page } from '@vben/common-ui';
|
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 {
|
import {
|
||||||
getSchoolDetailApi,
|
getSchoolDetailApi,
|
||||||
saveSchoolApi,
|
saveSchoolApi,
|
||||||
getSchoolAccountListApi,
|
|
||||||
saveSchoolAccountApi,
|
|
||||||
deleteSchoolAccountApi,
|
|
||||||
type SchoolApi
|
type SchoolApi
|
||||||
} from '#/api';
|
} from '#/api';
|
||||||
|
|
||||||
@@ -17,9 +14,7 @@ defineOptions({ name: 'SchoolDetail' });
|
|||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
const accountFormRef = ref();
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const accountLoading = ref(false);
|
|
||||||
const activeTab = ref('basic');
|
const activeTab = ref('basic');
|
||||||
const formData = ref<Partial<SchoolApi.SaveParams>>({
|
const formData = ref<Partial<SchoolApi.SaveParams>>({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -29,18 +24,7 @@ const formData = ref<Partial<SchoolApi.SaveParams>>({
|
|||||||
description: '',
|
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 isEdit = computed(() => !!route.params.id);
|
||||||
const schoolId = computed(() => isEdit.value ? Number(route.params.id) : 0);
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
try {
|
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(() => {
|
onMounted(() => {
|
||||||
if (isEdit.value) {
|
if (isEdit.value) {
|
||||||
fetchDetail();
|
fetchDetail();
|
||||||
fetchAccountList();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -240,78 +119,9 @@ onMounted(() => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Form>
|
</Form>
|
||||||
</Tabs.TabPane>
|
</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>
|
</Tabs>
|
||||||
</Card>
|
</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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,14 @@
|
|||||||
import { ref, reactive, onMounted } from 'vue';
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Page } from '@vben/common-ui';
|
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 {
|
import {
|
||||||
getSchoolListApi,
|
deleteSchoolAccountApi,
|
||||||
deleteSchoolApi,
|
deleteSchoolApi,
|
||||||
type SchoolApi
|
getSchoolAccountListApi,
|
||||||
|
getSchoolListApi,
|
||||||
|
saveSchoolAccountApi,
|
||||||
|
type SchoolApi
|
||||||
} from '#/api';
|
} from '#/api';
|
||||||
|
|
||||||
defineOptions({ name: 'SchoolList' });
|
defineOptions({ name: 'SchoolList' });
|
||||||
@@ -58,7 +61,7 @@ const columns = [
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 200,
|
width: 280,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -95,8 +98,8 @@ const handleSearch = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (record: SchoolApi.SchoolInfo) => {
|
const handleEdit = (record: SchoolApi.SchoolInfo) => {
|
||||||
router.push({
|
router.push({
|
||||||
name: 'SchoolDetail',
|
name: 'SchoolDetail',
|
||||||
params: { id: record.id }
|
params: { id: record.id }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -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) => {
|
const handleTableChange = (pag: any) => {
|
||||||
pagination.current = pag.current;
|
pagination.current = pag.current;
|
||||||
pagination.pageSize = pag.pageSize;
|
pagination.pageSize = pag.pageSize;
|
||||||
@@ -171,6 +281,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'action'">
|
<template v-else-if="column.key === 'action'">
|
||||||
<Space>
|
<Space>
|
||||||
|
<Button type="link" size="small" @click="openAccountModal(record)">账号管理</Button>
|
||||||
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
<Button type="link" size="small" @click="handleEdit(record)">编辑</Button>
|
||||||
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
|
<Button type="link" danger size="small" @click="handleDelete(record)">删除</Button>
|
||||||
</Space>
|
</Space>
|
||||||
@@ -178,6 +289,74 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
</Table>
|
</Table>
|
||||||
</Card>
|
</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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const loading = ref(false);
|
|||||||
const classList = ref<ClassApi.ClassInfo[]>([]);
|
const classList = ref<ClassApi.ClassInfo[]>([]);
|
||||||
const classListLoading = ref(false);
|
const classListLoading = ref(false);
|
||||||
const formData = ref<Partial<StudentApi.SaveParams>>({
|
const formData = ref<Partial<StudentApi.SaveParams>>({
|
||||||
student_id: '',
|
|
||||||
name: '',
|
name: '',
|
||||||
gender: undefined,
|
gender: undefined,
|
||||||
class_id: undefined,
|
class_id: undefined,
|
||||||
@@ -124,14 +123,14 @@ onMounted(() => {
|
|||||||
:wrapper-col="{ span: 20 }"
|
:wrapper-col="{ span: 20 }"
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
v-if="isEdit"
|
||||||
label="学员ID"
|
label="学员ID"
|
||||||
name="student_id"
|
name="student_id"
|
||||||
:rules="[{ required: true, message: '请输入学员ID' }]"
|
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
v-model:value="formData.student_id as string"
|
v-model:value="formData.student_id as string"
|
||||||
:placeholder="isEdit ? '学员ID' : '请输入学员ID'"
|
placeholder="学员ID"
|
||||||
:disabled="isEdit"
|
disabled
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
|
|||||||
@@ -179,9 +179,11 @@ const uploadedFile = ref<string>('');
|
|||||||
const selectedClassId = ref<number | undefined>(undefined);
|
const selectedClassId = ref<number | undefined>(undefined);
|
||||||
const classList = ref<ClassApi.ClassInfo[]>([]);
|
const classList = ref<ClassApi.ClassInfo[]>([]);
|
||||||
const fileList = ref<any[]>([]);
|
const fileList = ref<any[]>([]);
|
||||||
|
const classListLoading = ref(false);
|
||||||
|
|
||||||
// 获取班级列表
|
// 获取班级列表
|
||||||
const fetchClassList = async () => {
|
const fetchClassList = async () => {
|
||||||
|
classListLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getClassListApi({ page: 1, limit: 1000 });
|
const res = await getClassListApi({ page: 1, limit: 1000 });
|
||||||
if (res && (res.code === 0 || res.code === 200)) {
|
if (res && (res.code === 0 || res.code === 200)) {
|
||||||
@@ -196,7 +198,10 @@ const fetchClassList = async () => {
|
|||||||
classList.value = [];
|
classList.value = [];
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('获取班级列表失败:', error);
|
||||||
classList.value = [];
|
classList.value = [];
|
||||||
|
} finally {
|
||||||
|
classListLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -309,6 +314,7 @@ const handleCancelImport = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
fetchClassList();
|
||||||
fetchData();
|
fetchData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -339,6 +345,27 @@ onMounted(() => {
|
|||||||
allow-clear
|
allow-clear
|
||||||
@press-enter="handleSearch"
|
@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
|
<Select
|
||||||
v-model:value="searchForm.bind_status"
|
v-model:value="searchForm.bind_status"
|
||||||
placeholder="绑定状态"
|
placeholder="绑定状态"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export default defineConfig(async () => {
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://xz.dhdjy.com',
|
target: 'https://xz.dhdjy.com',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
secure: true, // https 接口设为 true
|
secure: true, // https 接口设为 true
|
||||||
ws: 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
|
* @zh_CN Vben Logo
|
||||||
*/
|
*/
|
||||||
export const VBEN_LOGO_URL =
|
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 首页地址
|
* @zh_CN Vben Admin 首页地址
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
|
|||||||
"logo": {
|
"logo": {
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"fit": "contain",
|
"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": {
|
"navigation": {
|
||||||
"accordion": true,
|
"accordion": true,
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const defaultPreferences: Preferences = {
|
|||||||
logo: {
|
logo: {
|
||||||
enable: true,
|
enable: true,
|
||||||
fit: 'contain',
|
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: {
|
navigation: {
|
||||||
accordion: true,
|
accordion: true,
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import { useVbenForm } from '@vben-core/form-ui';
|
|||||||
import { VbenButton, VbenCheckbox } from '@vben-core/shadcn-ui';
|
import { VbenButton, VbenCheckbox } from '@vben-core/shadcn-ui';
|
||||||
|
|
||||||
import Title from './auth-title.vue';
|
import Title from './auth-title.vue';
|
||||||
import ThirdPartyLogin from './third-party-login.vue';
|
|
||||||
|
|
||||||
interface Props extends AuthenticationProps {
|
interface Props extends AuthenticationProps {
|
||||||
formSchema?: VbenFormSchema[];
|
formSchema?: VbenFormSchema[];
|
||||||
@@ -123,14 +122,6 @@ defineExpose({
|
|||||||
{{ $t('authentication.rememberMe') }}
|
{{ $t('authentication.rememberMe') }}
|
||||||
</VbenCheckbox>
|
</VbenCheckbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span
|
|
||||||
v-if="showForgetPassword"
|
|
||||||
class="vben-link text-sm font-normal"
|
|
||||||
@click="handleGo(forgetPasswordPath)"
|
|
||||||
>
|
|
||||||
{{ $t('authentication.forgetPassword') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<VbenButton
|
<VbenButton
|
||||||
:class="{
|
:class="{
|
||||||
@@ -143,44 +134,5 @@ defineExpose({
|
|||||||
>
|
>
|
||||||
{{ submitButtonText || $t('common.login') }}
|
{{ submitButtonText || $t('common.login') }}
|
||||||
</VbenButton>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -31,18 +31,5 @@ withDefaults(defineProps<Props>(), {
|
|||||||
>
|
>
|
||||||
{{ icp }}
|
{{ icp }}
|
||||||
</a>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"welcomeBack": "欢迎回来",
|
"welcomeBack": "欢迎回来",
|
||||||
"pageTitle": "开箱即用的大型中后台管理系统",
|
"pageTitle": "东汇达教育座位预定系统",
|
||||||
"pageDesc": "工程化、高性能、跨组件库的前端模版",
|
"pageDesc": "内部选座专用系统",
|
||||||
"loginSuccess": "登录成功",
|
"loginSuccess": "登录成功",
|
||||||
"loginSuccessDesc": "欢迎回来",
|
"loginSuccessDesc": "欢迎回来",
|
||||||
"loginSubtitle": "请输入您的帐户信息以开始管理您的项目",
|
"loginSubtitle": "请输入您的帐户信息以开始管理您的项目",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { requestClient } from '../request';
|
|||||||
*/
|
*/
|
||||||
async function downloadFile1() {
|
async function downloadFile1() {
|
||||||
return requestClient.download<Blob>(
|
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() {
|
async function downloadFile2() {
|
||||||
return requestClient.download<RequestResponse<Blob>>(
|
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',
|
responseReturn: 'raw',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function getResponse() {
|
|||||||
@click="
|
@click="
|
||||||
downloadFileFromImageUrl({
|
downloadFileFromImageUrl({
|
||||||
source:
|
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',
|
fileName: 'vben-logo.png',
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -400,7 +400,7 @@ function handleSetFormValue() {
|
|||||||
name: 'example.png',
|
name: 'example.png',
|
||||||
status: 'done',
|
status: 'done',
|
||||||
uid: '-1',
|
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',
|
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