Sure! Pl
This commit is contained in:
commit
d712732dd4
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[compose.yaml]
|
||||
indent_size = 4
|
||||
65
.env.example
Normal file
65
.env.example
Normal file
@ -0,0 +1,65 @@
|
||||
APP_NAME=Laravel
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
APP_LOCALE=en
|
||||
APP_FALLBACK_LOCALE=en
|
||||
APP_FAKER_LOCALE=en_US
|
||||
|
||||
APP_MAINTENANCE_DRIVER=file
|
||||
# APP_MAINTENANCE_STORE=database
|
||||
|
||||
# PHP_CLI_SERVER_WORKERS=4
|
||||
|
||||
BCRYPT_ROUNDS=12
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_STACK=single
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
LOG_LEVEL=debug
|
||||
|
||||
DB_CONNECTION=sqlite
|
||||
# DB_HOST=127.0.0.1
|
||||
# DB_PORT=3306
|
||||
# DB_DATABASE=laravel
|
||||
# DB_USERNAME=root
|
||||
# DB_PASSWORD=
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
SESSION_ENCRYPT=false
|
||||
SESSION_PATH=/
|
||||
SESSION_DOMAIN=null
|
||||
|
||||
BROADCAST_CONNECTION=log
|
||||
FILESYSTEM_DISK=local
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
CACHE_STORE=database
|
||||
# CACHE_PREFIX=
|
||||
|
||||
MEMCACHED_HOST=127.0.0.1
|
||||
|
||||
REDIS_CLIENT=phpredis
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_MAILER=log
|
||||
MAIL_SCHEME=null
|
||||
MAIL_HOST=127.0.0.1
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_FROM_ADDRESS="hello@example.com"
|
||||
MAIL_FROM_NAME="${APP_NAME}"
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
*.blade.php diff=html
|
||||
*.css diff=css
|
||||
*.html diff=html
|
||||
*.md diff=markdown
|
||||
*.php diff=php
|
||||
|
||||
/.github export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
.styleci.yml export-ignore
|
||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.backup
|
||||
.env.production
|
||||
.phpactor.json
|
||||
.phpunit.result.cache
|
||||
/.fleet
|
||||
/.idea
|
||||
/.nova
|
||||
/.phpunit.cache
|
||||
/.vscode
|
||||
/.zed
|
||||
/auth.json
|
||||
/node_modules
|
||||
/public/build
|
||||
/public/hot
|
||||
/public/storage
|
||||
/storage/*.key
|
||||
/storage/pail
|
||||
/vendor
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
Thumbs.db
|
||||
59
README.md
Normal file
59
README.md
Normal file
@ -0,0 +1,59 @@
|
||||
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
|
||||
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
|
||||
</p>
|
||||
|
||||
## About Laravel
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
- [Powerful dependency injection container](https://laravel.com/docs/container).
|
||||
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
|
||||
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
|
||||
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
|
||||
- [Robust background job processing](https://laravel.com/docs/queues).
|
||||
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
|
||||
|
||||
Laravel is accessible, powerful, and provides tools required for large, robust applications.
|
||||
|
||||
## Learning Laravel
|
||||
|
||||
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. You can also check out [Laravel Learn](https://laravel.com/learn), where you will be guided through building a modern Laravel application.
|
||||
|
||||
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
|
||||
|
||||
## Laravel Sponsors
|
||||
|
||||
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com).
|
||||
|
||||
### Premium Partners
|
||||
|
||||
- **[Vehikl](https://vehikl.com)**
|
||||
- **[Tighten Co.](https://tighten.co)**
|
||||
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
|
||||
- **[64 Robots](https://64robots.com)**
|
||||
- **[Curotec](https://www.curotec.com/services/technologies/laravel)**
|
||||
- **[DevSquad](https://devsquad.com/hire-laravel-developers)**
|
||||
- **[Redberry](https://redberry.international/laravel-development)**
|
||||
- **[Active Logic](https://activelogic.com)**
|
||||
|
||||
## Contributing
|
||||
|
||||
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
|
||||
|
||||
## Security Vulnerabilities
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
102
app/Http/Controllers/AuthController.php
Normal file
102
app/Http/Controllers/AuthController.php
Normal file
@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class AuthController extends Controller
|
||||
{
|
||||
/**
|
||||
* 显示登录页面
|
||||
*/
|
||||
public function showLogin()
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return redirect()->route('patients.reminders');
|
||||
}
|
||||
return view('auth.login');
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录
|
||||
*/
|
||||
public function login(Request $request)
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => 'required|email',
|
||||
'password' => 'required',
|
||||
], [
|
||||
'email.required' => '请输入邮箱',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'password.required' => '请输入密码',
|
||||
]);
|
||||
|
||||
$remember = $request->boolean('remember');
|
||||
|
||||
if (Auth::attempt($credentials, $remember)) {
|
||||
$request->session()->regenerate();
|
||||
return redirect()->intended(route('patients.reminders'));
|
||||
}
|
||||
|
||||
return back()->withErrors([
|
||||
'email' => '邮箱或密码错误',
|
||||
])->onlyInput('email');
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示注册页面
|
||||
*/
|
||||
public function showRegister()
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return redirect()->route('patients.reminders');
|
||||
}
|
||||
return view('auth.register');
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理注册
|
||||
*/
|
||||
public function register(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => 'required|string|max:255',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => ['required', 'confirmed', Password::min(6)],
|
||||
], [
|
||||
'name.required' => '请输入姓名',
|
||||
'email.required' => '请输入邮箱',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'email.unique' => '该邮箱已被注册',
|
||||
'password.required' => '请输入密码',
|
||||
'password.confirmed' => '两次密码输入不一致',
|
||||
'password.min' => '密码至少6位',
|
||||
]);
|
||||
|
||||
$user = User::create([
|
||||
'name' => $validated['name'],
|
||||
'email' => $validated['email'],
|
||||
'password' => Hash::make($validated['password']),
|
||||
]);
|
||||
|
||||
Auth::login($user);
|
||||
|
||||
return redirect()->route('patients.reminders')->with('success', '注册成功,欢迎使用!');
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
public function logout(Request $request)
|
||||
{
|
||||
Auth::logout();
|
||||
$request->session()->invalidate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('login');
|
||||
}
|
||||
}
|
||||
8
app/Http/Controllers/Controller.php
Normal file
8
app/Http/Controllers/Controller.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
500
app/Http/Controllers/PatientController.php
Normal file
500
app/Http/Controllers/PatientController.php
Normal file
@ -0,0 +1,500 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Patient;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\Response;
|
||||
|
||||
class PatientController extends Controller
|
||||
{
|
||||
/**
|
||||
* 获取当前用户的患者查询构建器
|
||||
*/
|
||||
private function userPatients()
|
||||
{
|
||||
return Patient::where('user_id', Auth::id());
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示患者列表页面
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = $this->userPatients();
|
||||
|
||||
// 搜索功能
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->input('search');
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%")
|
||||
->orWhere('phone', 'like', "%{$search}%")
|
||||
->orWhere('address', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// 诊断筛选
|
||||
if ($request->filled('diagnosis')) {
|
||||
$query->where('diagnosis', 'like', "%{$request->input('diagnosis')}%");
|
||||
}
|
||||
|
||||
$patients = $query->orderBy('discharge_date', 'desc')->paginate(20);
|
||||
|
||||
return view('patients.index', compact('patients'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示需要随访提醒的患者
|
||||
*/
|
||||
public function reminders(Request $request)
|
||||
{
|
||||
$patients = $this->userPatients()->get();
|
||||
|
||||
// 筛选需要随访的患者
|
||||
$reminders = $patients->filter(function ($patient) {
|
||||
return !$patient->isCompleted();
|
||||
})->map(function ($patient) {
|
||||
$nextDate = $patient->getNextFollowUpDate();
|
||||
return [
|
||||
'patient' => $patient,
|
||||
'next_follow_up_date' => $nextDate,
|
||||
'next_follow_up_number' => $patient->getNextFollowUpNumber(),
|
||||
'status' => $patient->getFollowUpStatus(),
|
||||
'needs_attention' => $patient->needsFollowUp(),
|
||||
'days_until' => $nextDate ? Carbon::today()->diffInDays($nextDate, false) : null,
|
||||
];
|
||||
})->sortBy('days_until');
|
||||
|
||||
// 筛选类型
|
||||
$filter = $request->input('filter', 'all');
|
||||
|
||||
if ($filter === 'overdue') {
|
||||
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] < 0);
|
||||
} elseif ($filter === 'today') {
|
||||
$reminders = $reminders->filter(fn($r) => $r['days_until'] === 0);
|
||||
} elseif ($filter === 'upcoming') {
|
||||
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] > 0 && $r['days_until'] <= 7);
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
$stats = [
|
||||
'total' => $this->userPatients()->count(),
|
||||
'overdue' => $patients->filter(fn($p) => !$p->isCompleted() && $p->getNextFollowUpDate()?->lt(Carbon::today()))->count(),
|
||||
'today' => $patients->filter(fn($p) => !$p->isCompleted() && $p->getNextFollowUpDate()?->isToday())->count(),
|
||||
'upcoming' => $patients->filter(function ($p) {
|
||||
if ($p->isCompleted()) return false;
|
||||
$next = $p->getNextFollowUpDate();
|
||||
if (!$next) return false;
|
||||
$diff = Carbon::today()->diffInDays($next, false);
|
||||
return $diff > 0 && $diff <= 7;
|
||||
})->count(),
|
||||
'completed' => $patients->filter(fn($p) => $p->isCompleted())->count(),
|
||||
];
|
||||
|
||||
return view('patients.reminders', compact('reminders', 'stats', 'filter'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示导入页面
|
||||
*/
|
||||
public function showImport()
|
||||
{
|
||||
return view('patients.import');
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理Excel导入
|
||||
*/
|
||||
public function import(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:xlsx,xls,csv|max:10240',
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$path = $file->getRealPath();
|
||||
|
||||
try {
|
||||
$data = $this->parseExcel($path, $file->getClientOriginalExtension());
|
||||
|
||||
$imported = 0;
|
||||
$errors = [];
|
||||
$headerSkipped = false;
|
||||
|
||||
foreach ($data as $index => $row) {
|
||||
try {
|
||||
// 跳过空行
|
||||
if (empty($row) || !is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$firstCell = trim((string)($row[0] ?? ''));
|
||||
|
||||
// 跳过表头行(第一行或包含"姓名"的行)
|
||||
if (!$headerSkipped && ($index === 0 || mb_strpos($firstCell, '姓名') !== false)) {
|
||||
$headerSkipped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 跳过空行
|
||||
if (empty($firstCell)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 解析数据
|
||||
$patientData = $this->parseRow($row);
|
||||
|
||||
if ($patientData) {
|
||||
// 关联到当前用户
|
||||
$patientData['user_id'] = Auth::id();
|
||||
Patient::create($patientData);
|
||||
$imported++;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = "第 " . ($index + 1) . " 行导入失败: " . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$message = "成功导入 {$imported} 条记录。";
|
||||
if (count($errors) > 0) {
|
||||
$message .= " " . count($errors) . " 条记录导入失败。";
|
||||
}
|
||||
|
||||
return redirect()->route('patients.index')
|
||||
->with('success', $message)
|
||||
->with('errors', $errors);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return back()->withErrors(['file' => '文件解析失败: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Excel/CSV文件 (使用 xlswriter 扩展)
|
||||
*/
|
||||
private function parseExcel(string $path, string $extension): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
if ($extension === 'csv') {
|
||||
// 检测文件编码并转换
|
||||
$content = file_get_contents($path);
|
||||
$encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
|
||||
|
||||
if ($encoding && $encoding !== 'UTF-8') {
|
||||
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
|
||||
$tempPath = sys_get_temp_dir() . '/converted_' . time() . '.csv';
|
||||
file_put_contents($tempPath, $content);
|
||||
$path = $tempPath;
|
||||
}
|
||||
|
||||
$handle = fopen($path, 'r');
|
||||
// 跳过 BOM
|
||||
$bom = fread($handle, 3);
|
||||
if ($bom !== "\xEF\xBB\xBF") {
|
||||
rewind($handle);
|
||||
}
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
$data[] = $row;
|
||||
}
|
||||
fclose($handle);
|
||||
|
||||
// 清理临时文件
|
||||
if (isset($tempPath) && file_exists($tempPath)) {
|
||||
@unlink($tempPath);
|
||||
}
|
||||
} elseif ($extension === 'xls') {
|
||||
// .xls 是旧版 Excel 格式,xlswriter 不支持读取
|
||||
// 尝试使用 PhpSpreadsheet 或提示用户转换格式
|
||||
if (class_exists(\PhpOffice\PhpSpreadsheet\IOFactory::class)) {
|
||||
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path);
|
||||
$worksheet = $spreadsheet->getActiveSheet();
|
||||
foreach ($worksheet->getRowIterator() as $row) {
|
||||
$rowData = [];
|
||||
$cellIterator = $row->getCellIterator();
|
||||
$cellIterator->setIterateOnlyExistingCells(false);
|
||||
foreach ($cellIterator as $cell) {
|
||||
$rowData[] = $cell->getValue();
|
||||
}
|
||||
$data[] = $rowData;
|
||||
}
|
||||
} else {
|
||||
throw new \Exception('不支持 .xls 格式(Excel 97-2003)。请将文件另存为 .xlsx 格式(Excel 2007+)或 .csv 格式后重新上传。操作方法:在 Excel 中打开文件 → 文件 → 另存为 → 选择"Excel 工作簿 (*.xlsx)"或"CSV UTF-8"');
|
||||
}
|
||||
} else {
|
||||
// 使用 xlswriter 扩展解析 xlsx
|
||||
if (!class_exists(\Vtiful\Kernel\Excel::class)) {
|
||||
throw new \Exception('xlswriter 扩展未安装,请安装后重试');
|
||||
}
|
||||
|
||||
$config = ['path' => sys_get_temp_dir()];
|
||||
$excel = new \Vtiful\Kernel\Excel($config);
|
||||
|
||||
// 复制文件到临时目录
|
||||
$tempFile = sys_get_temp_dir() . '/' . uniqid('import_') . '.' . $extension;
|
||||
copy($path, $tempFile);
|
||||
|
||||
// 读取Excel数据
|
||||
$data = $excel->openFile(basename($tempFile))
|
||||
->openSheet()
|
||||
->getSheetData();
|
||||
|
||||
// 如果没有读取到数据,可能是格式问题
|
||||
if (empty($data)) {
|
||||
throw new \Exception('无法读取 Excel 文件内容。请确保文件是有效的 .xlsx 格式,或尝试另存为 .csv 格式后重新上传。');
|
||||
}
|
||||
|
||||
// 释放 Excel 对象
|
||||
unset($excel);
|
||||
|
||||
// 清理临时文件 (使用 @ 抑制 Windows 上的文件锁定警告)
|
||||
if (file_exists($tempFile)) {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析单行数据
|
||||
*/
|
||||
private function parseRow(array $row): ?array
|
||||
{
|
||||
// 期望的列顺序: 姓名, 性别, 年龄, 出院诊断, 转诊时间, 户籍地址, 联系方式, 备注
|
||||
if (count($row) < 5) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$name = trim((string)($row[0] ?? ''));
|
||||
if (empty($name) || mb_strpos($name, '姓名') !== false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析日期
|
||||
$dateValue = $row[4] ?? '';
|
||||
$dischargeDate = $this->parseDate($dateValue);
|
||||
|
||||
if (!$dischargeDate) {
|
||||
throw new \Exception("日期格式无效: {$dateValue}");
|
||||
}
|
||||
|
||||
// 处理年龄(可能是数字或字符串)
|
||||
$age = $row[2] ?? 0;
|
||||
if (is_string($age)) {
|
||||
$age = (int)preg_replace('/[^0-9]/', '', $age);
|
||||
}
|
||||
|
||||
// 处理电话号码(可能是科学计数法或数字)
|
||||
$phone = $row[6] ?? '';
|
||||
if (is_numeric($phone)) {
|
||||
$phone = number_format($phone, 0, '', '');
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'gender' => trim((string)($row[1] ?? '未知')),
|
||||
'age' => (int)$age,
|
||||
'diagnosis' => trim((string)($row[3] ?? '')),
|
||||
'discharge_date' => $dischargeDate,
|
||||
'address' => trim((string)($row[5] ?? '')),
|
||||
'phone' => trim((string)$phone),
|
||||
'remark' => trim((string)($row[7] ?? '')),
|
||||
'follow_up_count' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日期
|
||||
*/
|
||||
private function parseDate($value): ?string
|
||||
{
|
||||
if (empty($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim((string)$value);
|
||||
|
||||
// Excel 日期序列号(纯数字且大于25569)
|
||||
if (is_numeric($value) && (float)$value > 25569 && (float)$value < 50000) {
|
||||
// Excel日期是从1900年1月1日开始的天数
|
||||
// 25569 是 1970-01-01 的Excel序列号
|
||||
$unixTimestamp = ((float)$value - 25569) * 86400;
|
||||
return date('Y-m-d', (int)$unixTimestamp);
|
||||
}
|
||||
|
||||
// 字符串格式日期 - 各种格式
|
||||
$formats = [
|
||||
'Y.m.d', // 2025.12.01
|
||||
'Y-m-d', // 2025-12-01
|
||||
'Y/m/d', // 2025/12/01
|
||||
'Y.n.j', // 2025.1.1 (无前导零)
|
||||
'Y-n-j', // 2025-1-1
|
||||
'Y/n/j', // 2025/1/1
|
||||
'd/m/Y', // 01/12/2025
|
||||
'm/d/Y', // 12/01/2025
|
||||
];
|
||||
|
||||
foreach ($formats as $format) {
|
||||
$date = \DateTime::createFromFormat($format, $value);
|
||||
if ($date) {
|
||||
// 验证日期是否有效
|
||||
$errors = \DateTime::getLastErrors();
|
||||
if (empty($errors['warning_count']) && empty($errors['error_count'])) {
|
||||
return $date->format('Y-m-d');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试通过 strtotime(处理更多格式)
|
||||
$timestamp = strtotime(str_replace('.', '-', $value));
|
||||
if ($timestamp && $timestamp > 0) {
|
||||
return date('Y-m-d', $timestamp);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出提醒列表 (使用 xlswriter 生成 Excel)
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$filter = $request->input('filter', 'all');
|
||||
|
||||
$patients = $this->userPatients()->get();
|
||||
|
||||
// 筛选需要随访的患者
|
||||
$reminders = $patients->filter(function ($patient) {
|
||||
return !$patient->isCompleted();
|
||||
})->map(function ($patient) {
|
||||
$nextDate = $patient->getNextFollowUpDate();
|
||||
return [
|
||||
'patient' => $patient,
|
||||
'next_follow_up_date' => $nextDate,
|
||||
'next_follow_up_number' => $patient->getNextFollowUpNumber(),
|
||||
'status' => $patient->getFollowUpStatus(),
|
||||
'days_until' => $nextDate ? Carbon::today()->diffInDays($nextDate, false) : null,
|
||||
];
|
||||
})->sortBy('days_until');
|
||||
|
||||
// 根据筛选条件过滤
|
||||
if ($filter === 'overdue') {
|
||||
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] < 0);
|
||||
} elseif ($filter === 'today') {
|
||||
$reminders = $reminders->filter(fn($r) => $r['days_until'] === 0);
|
||||
} elseif ($filter === 'upcoming') {
|
||||
$reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] > 0 && $r['days_until'] <= 7);
|
||||
}
|
||||
|
||||
// 使用 xlswriter 生成 Excel 文件
|
||||
$config = ['path' => sys_get_temp_dir()];
|
||||
$filename = '随访提醒_' . date('Y-m-d_His') . '.xlsx';
|
||||
|
||||
$excel = new \Vtiful\Kernel\Excel($config);
|
||||
|
||||
// 创建文件并设置表头
|
||||
$fileHandle = $excel->fileName($filename)
|
||||
->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '下次随访日期', '第几次随访', '状态', '户籍地址', '联系方式', '备注']);
|
||||
|
||||
// 设置表头样式
|
||||
$headerStyle = $fileHandle->getHandle();
|
||||
$format = new \Vtiful\Kernel\Format($headerStyle);
|
||||
$boldFormat = $format->bold()->toResource();
|
||||
|
||||
// 添加数据行
|
||||
$rows = [];
|
||||
foreach ($reminders as $reminder) {
|
||||
$p = $reminder['patient'];
|
||||
$rows[] = [
|
||||
$p->name,
|
||||
$p->gender,
|
||||
$p->age,
|
||||
$p->diagnosis,
|
||||
$p->discharge_date->format('Y-m-d'),
|
||||
$reminder['next_follow_up_date']?->format('Y-m-d') ?? '',
|
||||
'第' . $reminder['next_follow_up_number'] . '次',
|
||||
$reminder['status'],
|
||||
$p->address ?? '',
|
||||
$p->phone ?? '',
|
||||
$p->remark ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
$fileHandle->data($rows)->output();
|
||||
|
||||
$filePath = $config['path'] . '/' . $filename;
|
||||
|
||||
return Response::download($filePath, $filename, [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
])->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记已随访
|
||||
*/
|
||||
public function markFollowedUp(Patient $patient)
|
||||
{
|
||||
// 验证患者属于当前用户
|
||||
if ($patient->user_id !== Auth::id()) {
|
||||
abort(403, '无权操作此患者');
|
||||
}
|
||||
|
||||
if (!$patient->isCompleted()) {
|
||||
$patient->follow_up_count += 1;
|
||||
$patient->last_follow_up_date = Carbon::today();
|
||||
$patient->save();
|
||||
}
|
||||
|
||||
return back()->with('success', "{$patient->name} 已标记为完成第 {$patient->follow_up_count} 次随访");
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除患者
|
||||
*/
|
||||
public function destroy(Patient $patient)
|
||||
{
|
||||
// 验证患者属于当前用户
|
||||
if ($patient->user_id !== Auth::id()) {
|
||||
abort(403, '无权操作此患者');
|
||||
}
|
||||
|
||||
$name = $patient->name;
|
||||
$patient->delete();
|
||||
|
||||
return back()->with('success', "已删除患者: {$name}");
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载导入模板 (使用 xlswriter 生成 Excel)
|
||||
*/
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$config = ['path' => sys_get_temp_dir()];
|
||||
$filename = '导入模板.xlsx';
|
||||
|
||||
$excel = new \Vtiful\Kernel\Excel($config);
|
||||
|
||||
// 创建文件
|
||||
$fileHandle = $excel->fileName($filename)
|
||||
->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '户籍地址', '联系方式', '备注']);
|
||||
|
||||
// 添加示例数据
|
||||
$data = [
|
||||
['张三', '男', 65, '脑卒中', '2025.12.01', '北京市朝阳区', '13800138000', '常住'],
|
||||
['李四', '女', 70, '慢性肾脏病', '2025.11.15', '上海市浦东新区', '13900139000', ''],
|
||||
['王五', '男', 58, '心肌梗塞', '2025.10.20', '广州市天河区', '13700137000', ''],
|
||||
];
|
||||
|
||||
$fileHandle->data($data)->output();
|
||||
|
||||
$filePath = $config['path'] . '/' . $filename;
|
||||
|
||||
return Response::download($filePath, $filename, [
|
||||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
])->deleteFileAfterSend(true);
|
||||
}
|
||||
}
|
||||
140
app/Models/Patient.php
Normal file
140
app/Models/Patient.php
Normal file
@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class Patient extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'name',
|
||||
'gender',
|
||||
'age',
|
||||
'diagnosis',
|
||||
'discharge_date',
|
||||
'address',
|
||||
'phone',
|
||||
'remark',
|
||||
'follow_up_count',
|
||||
'last_follow_up_date',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取患者所属用户
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
protected $casts = [
|
||||
'discharge_date' => 'date',
|
||||
'last_follow_up_date' => 'date',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取随访时间规则(月数)
|
||||
* 脑卒中/心肌梗塞: 1, 3, 6, 12 个月
|
||||
* 慢性肾脏病: 1, 2, 3, 6 个月
|
||||
*/
|
||||
public function getFollowUpSchedule(): array
|
||||
{
|
||||
$diagnosis = $this->diagnosis;
|
||||
|
||||
if (str_contains($diagnosis, '慢性肾') || str_contains($diagnosis, '肾脏病')) {
|
||||
return [1, 2, 3, 6]; // 慢性肾脏病
|
||||
}
|
||||
|
||||
// 脑卒中、心肌梗塞及其他
|
||||
return [1, 3, 6, 12];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下次随访日期
|
||||
*/
|
||||
public function getNextFollowUpDate(): ?Carbon
|
||||
{
|
||||
$schedule = $this->getFollowUpSchedule();
|
||||
$nextIndex = $this->follow_up_count;
|
||||
|
||||
if ($nextIndex >= count($schedule)) {
|
||||
return null; // 已完成所有随访
|
||||
}
|
||||
|
||||
$months = $schedule[$nextIndex];
|
||||
return $this->discharge_date->copy()->addMonths($months);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下次随访是第几次
|
||||
*/
|
||||
public function getNextFollowUpNumber(): int
|
||||
{
|
||||
return $this->follow_up_count + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要随访(到期或已过期)
|
||||
*/
|
||||
public function needsFollowUp(): bool
|
||||
{
|
||||
$nextDate = $this->getNextFollowUpDate();
|
||||
|
||||
if (!$nextDate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $nextDate->lte(Carbon::today());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随访状态
|
||||
*/
|
||||
public function getFollowUpStatus(): string
|
||||
{
|
||||
$nextDate = $this->getNextFollowUpDate();
|
||||
|
||||
if (!$nextDate) {
|
||||
return '已完成';
|
||||
}
|
||||
|
||||
$today = Carbon::today();
|
||||
$diff = $today->diffInDays($nextDate, false);
|
||||
|
||||
if ($diff < 0) {
|
||||
return '已过期 ' . abs($diff) . ' 天';
|
||||
} elseif ($diff == 0) {
|
||||
return '今日到期';
|
||||
} elseif ($diff <= 7) {
|
||||
return '即将到期(' . $diff . '天后)';
|
||||
} else {
|
||||
return '未到期';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否已完成所有随访
|
||||
*/
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->follow_up_count >= count($this->getFollowUpSchedule());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取诊断类型名称
|
||||
*/
|
||||
public function getDiagnosisType(): string
|
||||
{
|
||||
if (str_contains($this->diagnosis, '脑卒中')) {
|
||||
return '脑卒中';
|
||||
} elseif (str_contains($this->diagnosis, '心肌梗') || str_contains($this->diagnosis, '心梗')) {
|
||||
return '心肌梗塞';
|
||||
} elseif (str_contains($this->diagnosis, '慢性肾') || str_contains($this->diagnosis, '肾脏病')) {
|
||||
return '慢性肾脏病';
|
||||
}
|
||||
return $this->diagnosis;
|
||||
}
|
||||
}
|
||||
57
app/Models/User.php
Normal file
57
app/Models/User.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有患者
|
||||
*/
|
||||
public function patients(): HasMany
|
||||
{
|
||||
return $this->hasMany(Patient::class);
|
||||
}
|
||||
}
|
||||
24
app/Providers/AppServiceProvider.php
Normal file
24
app/Providers/AppServiceProvider.php
Normal file
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
18
artisan
Normal file
18
artisan
Normal file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the command...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||
|
||||
$status = $app->handleCommand(new ArgvInput);
|
||||
|
||||
exit($status);
|
||||
20
bootstrap/app.php
Normal file
20
bootstrap/app.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
// 配置重定向
|
||||
$middleware->redirectGuestsTo('/login');
|
||||
$middleware->redirectUsersTo('/patients/reminders');
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
})->create();
|
||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
5
bootstrap/providers.php
Normal file
5
bootstrap/providers.php
Normal file
@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
];
|
||||
86
composer.json
Normal file
86
composer.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"$schema": "https://getcomposer.org/schema.json",
|
||||
"name": "laravel/laravel",
|
||||
"type": "project",
|
||||
"description": "The skeleton application for the Laravel framework.",
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"laravel/framework": "^12.0",
|
||||
"laravel/tinker": "^2.10.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.23",
|
||||
"laravel/pail": "^1.2.2",
|
||||
"laravel/pint": "^1.24",
|
||||
"laravel/sail": "^1.41",
|
||||
"mockery/mockery": "^1.6",
|
||||
"nunomaduro/collision": "^8.6",
|
||||
"phpunit/phpunit": "^11.5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "app/",
|
||||
"Database\\Factories\\": "database/factories/",
|
||||
"Database\\Seeders\\": "database/seeders/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"setup": [
|
||||
"composer install",
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||
"@php artisan key:generate",
|
||||
"@php artisan migrate --force",
|
||||
"npm install",
|
||||
"npm run build"
|
||||
],
|
||||
"dev": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||
"@php artisan package:discover --ansi"
|
||||
],
|
||||
"post-update-cmd": [
|
||||
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||
],
|
||||
"post-root-package-install": [
|
||||
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||
],
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi",
|
||||
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||
"@php artisan migrate --graceful --ansi"
|
||||
],
|
||||
"pre-package-uninstall": [
|
||||
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||
]
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"sort-packages": true,
|
||||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true
|
||||
}
|
||||
},
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
8373
composer.lock
generated
Normal file
8373
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
126
config/app.php
Normal file
126
config/app.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the name of your application, which will be used when the
|
||||
| framework needs to place the application's name in a notification or
|
||||
| other UI elements where an application name needs to be displayed.
|
||||
|
|
||||
*/
|
||||
|
||||
'name' => env('APP_NAME', 'Laravel'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Environment
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the "environment" your application is currently
|
||||
| running in. This may determine how you prefer to configure various
|
||||
| services the application utilizes. Set this in your ".env" file.
|
||||
|
|
||||
*/
|
||||
|
||||
'env' => env('APP_ENV', 'production'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Debug Mode
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When your application is in debug mode, detailed error messages with
|
||||
| stack traces will be shown on every error that occurs within your
|
||||
| application. If disabled, a simple generic error page is shown.
|
||||
|
|
||||
*/
|
||||
|
||||
'debug' => (bool) env('APP_DEBUG', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application URL
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This URL is used by the console to properly generate URLs when using
|
||||
| the Artisan command line tool. You should set this to the root of
|
||||
| the application so that it's available within Artisan commands.
|
||||
|
|
||||
*/
|
||||
|
||||
'url' => env('APP_URL', 'http://localhost'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Timezone
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default timezone for your application, which
|
||||
| will be used by the PHP date and date-time functions. The timezone
|
||||
| is set to "UTC" by default as it is suitable for most use cases.
|
||||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application Locale Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The application locale determines the default locale that will be used
|
||||
| by Laravel's translation / localization methods. This option can be
|
||||
| set to any locale for which you plan to have translation strings.
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Encryption Key
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This key is utilized by Laravel's encryption services and should be set
|
||||
| to a random, 32 character string to ensure that all encrypted values
|
||||
| are secure. You should do this prior to deploying the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'cipher' => 'AES-256-CBC',
|
||||
|
||||
'key' => env('APP_KEY'),
|
||||
|
||||
'previous_keys' => [
|
||||
...array_filter(
|
||||
explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
|
||||
),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Maintenance Mode Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options determine the driver used to determine and
|
||||
| manage Laravel's "maintenance mode" status. The "cache" driver will
|
||||
| allow maintenance mode to be controlled across multiple machines.
|
||||
|
|
||||
| Supported drivers: "file", "cache"
|
||||
|
|
||||
*/
|
||||
|
||||
'maintenance' => [
|
||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||
],
|
||||
|
||||
];
|
||||
115
config/auth.php
Normal file
115
config/auth.php
Normal file
@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Defaults
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default authentication "guard" and password
|
||||
| reset "broker" for your application. You may change these values
|
||||
| as required, but they're a perfect start for most applications.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Guards
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Next, you may define every authentication guard for your application.
|
||||
| Of course, a great default configuration has been defined for you
|
||||
| which utilizes session storage plus the Eloquent user provider.
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| Supported: "session"
|
||||
|
|
||||
*/
|
||||
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| All authentication guards have a user provider, which defines how the
|
||||
| users are actually retrieved out of your database or other storage
|
||||
| system used by the application. Typically, Eloquent is utilized.
|
||||
|
|
||||
| If you have multiple user tables or models you may configure multiple
|
||||
| providers to represent the model / table. These providers may then
|
||||
| be assigned to any extra authentication guards you have defined.
|
||||
|
|
||||
| Supported: "database", "eloquent"
|
||||
|
|
||||
*/
|
||||
|
||||
'providers' => [
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => env('AUTH_MODEL', App\Models\User::class),
|
||||
],
|
||||
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Resetting Passwords
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These configuration options specify the behavior of Laravel's password
|
||||
| reset functionality, including the table utilized for token storage
|
||||
| and the user provider that is invoked to actually retrieve users.
|
||||
|
|
||||
| The expiry time is the number of minutes that each reset token will be
|
||||
| considered valid. This security feature keeps tokens short-lived so
|
||||
| they have less time to be guessed. You may change this as needed.
|
||||
|
|
||||
| The throttle setting is the number of seconds a user must wait before
|
||||
| generating more password reset tokens. This prevents the user from
|
||||
| quickly generating a very large amount of password reset tokens.
|
||||
|
|
||||
*/
|
||||
|
||||
'passwords' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
|
||||
'expire' => 60,
|
||||
'throttle' => 60,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Password Confirmation Timeout
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the number of seconds before a password confirmation
|
||||
| window expires and users are asked to re-enter their password via the
|
||||
| confirmation screen. By default, the timeout lasts for three hours.
|
||||
|
|
||||
*/
|
||||
|
||||
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
|
||||
|
||||
];
|
||||
117
config/cache.php
Normal file
117
config/cache.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default cache store that will be used by the
|
||||
| framework. This connection is utilized if another isn't explicitly
|
||||
| specified when running a cache operation inside the application.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('CACHE_STORE', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Stores
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the cache "stores" for your application as
|
||||
| well as their drivers. You may even define multiple stores for the
|
||||
| same cache driver to group types of items stored in your caches.
|
||||
|
|
||||
| Supported drivers: "array", "database", "file", "memcached",
|
||||
| "redis", "dynamodb", "octane",
|
||||
| "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'stores' => [
|
||||
|
||||
'array' => [
|
||||
'driver' => 'array',
|
||||
'serialize' => false,
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_CACHE_CONNECTION'),
|
||||
'table' => env('DB_CACHE_TABLE', 'cache'),
|
||||
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
|
||||
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
|
||||
],
|
||||
|
||||
'file' => [
|
||||
'driver' => 'file',
|
||||
'path' => storage_path('framework/cache/data'),
|
||||
'lock_path' => storage_path('framework/cache/data'),
|
||||
],
|
||||
|
||||
'memcached' => [
|
||||
'driver' => 'memcached',
|
||||
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
|
||||
'sasl' => [
|
||||
env('MEMCACHED_USERNAME'),
|
||||
env('MEMCACHED_PASSWORD'),
|
||||
],
|
||||
'options' => [
|
||||
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
|
||||
],
|
||||
'servers' => [
|
||||
[
|
||||
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
|
||||
'port' => env('MEMCACHED_PORT', 11211),
|
||||
'weight' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
|
||||
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
|
||||
],
|
||||
|
||||
'dynamodb' => [
|
||||
'driver' => 'dynamodb',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
|
||||
'endpoint' => env('DYNAMODB_ENDPOINT'),
|
||||
],
|
||||
|
||||
'octane' => [
|
||||
'driver' => 'octane',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'stores' => [
|
||||
'database',
|
||||
'array',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Cache Key Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
|
||||
| stores, there might be other applications using the same cache. For
|
||||
| that reason, you may prefix every cache key to avoid collisions.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
|
||||
|
||||
];
|
||||
183
config/database.php
Normal file
183
config/database.php
Normal file
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify which of the database connections below you wish
|
||||
| to use as your default connection for database operations. This is
|
||||
| the connection which will be utilized unless another connection
|
||||
| is explicitly specified when you execute a query / statement.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('DB_CONNECTION', 'sqlite'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Database Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below are all of the database connections defined for your application.
|
||||
| An example configuration is provided for each database system which
|
||||
| is supported by Laravel. You're free to add / remove connections.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sqlite' => [
|
||||
'driver' => 'sqlite',
|
||||
'url' => env('DB_URL'),
|
||||
'database' => env('DB_DATABASE', database_path('database.sqlite')),
|
||||
'prefix' => '',
|
||||
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
|
||||
'busy_timeout' => null,
|
||||
'journal_mode' => null,
|
||||
'synchronous' => null,
|
||||
'transaction_mode' => 'DEFERRED',
|
||||
],
|
||||
|
||||
'mysql' => [
|
||||
'driver' => 'mysql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'mariadb' => [
|
||||
'driver' => 'mariadb',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '3306'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'unix_socket' => env('DB_SOCKET', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8mb4'),
|
||||
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'strict' => true,
|
||||
'engine' => null,
|
||||
'options' => extension_loaded('pdo_mysql') ? array_filter([
|
||||
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
|
||||
]) : [],
|
||||
],
|
||||
|
||||
'pgsql' => [
|
||||
'driver' => 'pgsql',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', '127.0.0.1'),
|
||||
'port' => env('DB_PORT', '5432'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
'search_path' => 'public',
|
||||
'sslmode' => env('DB_SSLMODE', 'prefer'),
|
||||
],
|
||||
|
||||
'sqlsrv' => [
|
||||
'driver' => 'sqlsrv',
|
||||
'url' => env('DB_URL'),
|
||||
'host' => env('DB_HOST', 'localhost'),
|
||||
'port' => env('DB_PORT', '1433'),
|
||||
'database' => env('DB_DATABASE', 'laravel'),
|
||||
'username' => env('DB_USERNAME', 'root'),
|
||||
'password' => env('DB_PASSWORD', ''),
|
||||
'charset' => env('DB_CHARSET', 'utf8'),
|
||||
'prefix' => '',
|
||||
'prefix_indexes' => true,
|
||||
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
|
||||
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Migration Repository Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This table keeps track of all the migrations that have already run for
|
||||
| your application. Using this information, we can determine which of
|
||||
| the migrations on disk haven't actually been run on the database.
|
||||
|
|
||||
*/
|
||||
|
||||
'migrations' => [
|
||||
'table' => 'migrations',
|
||||
'update_date_on_publish' => true,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Redis Databases
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Redis is an open source, fast, and advanced key-value store that also
|
||||
| provides a richer body of commands than a typical key-value system
|
||||
| such as Memcached. You may define your connection settings here.
|
||||
|
|
||||
*/
|
||||
|
||||
'redis' => [
|
||||
|
||||
'client' => env('REDIS_CLIENT', 'phpredis'),
|
||||
|
||||
'options' => [
|
||||
'cluster' => env('REDIS_CLUSTER', 'redis'),
|
||||
'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
|
||||
'persistent' => env('REDIS_PERSISTENT', false),
|
||||
],
|
||||
|
||||
'default' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
'cache' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'database' => env('REDIS_CACHE_DB', '1'),
|
||||
'max_retries' => env('REDIS_MAX_RETRIES', 3),
|
||||
'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
|
||||
'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
|
||||
'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
80
config/filesystems.php
Normal file
80
config/filesystems.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Filesystem Disk
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the default filesystem disk that should be used
|
||||
| by the framework. The "local" disk, as well as a variety of cloud
|
||||
| based disks are available to your application for file storage.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('FILESYSTEM_DISK', 'local'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Filesystem Disks
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Below you may configure as many filesystem disks as necessary, and you
|
||||
| may even configure multiple disks for the same driver. Examples for
|
||||
| most supported storage drivers are configured here for reference.
|
||||
|
|
||||
| Supported drivers: "local", "ftp", "sftp", "s3"
|
||||
|
|
||||
*/
|
||||
|
||||
'disks' => [
|
||||
|
||||
'local' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/private'),
|
||||
'serve' => true,
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/public'),
|
||||
'url' => rtrim(env('APP_URL'), '/').'/storage',
|
||||
'visibility' => 'public',
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
's3' => [
|
||||
'driver' => 's3',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION'),
|
||||
'bucket' => env('AWS_BUCKET'),
|
||||
'url' => env('AWS_URL'),
|
||||
'endpoint' => env('AWS_ENDPOINT'),
|
||||
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
|
||||
'throw' => false,
|
||||
'report' => false,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Symbolic Links
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the symbolic links that will be created when the
|
||||
| `storage:link` Artisan command is executed. The array keys should be
|
||||
| the locations of the links and the values should be their targets.
|
||||
|
|
||||
*/
|
||||
|
||||
'links' => [
|
||||
public_path('storage') => storage_path('app/public'),
|
||||
],
|
||||
|
||||
];
|
||||
132
config/logging.php
Normal file
132
config/logging.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
use Monolog\Handler\NullHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Handler\SyslogUdpHandler;
|
||||
use Monolog\Processor\PsrLogMessageProcessor;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option defines the default log channel that is utilized to write
|
||||
| messages to your logs. The value provided here should match one of
|
||||
| the channels present in the list of "channels" configured below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('LOG_CHANNEL', 'stack'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Deprecations Log Channel
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the log channel that should be used to log warnings
|
||||
| regarding deprecated PHP and library features. This allows you to get
|
||||
| your application ready for upcoming major versions of dependencies.
|
||||
|
|
||||
*/
|
||||
|
||||
'deprecations' => [
|
||||
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
|
||||
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Log Channels
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the log channels for your application. Laravel
|
||||
| utilizes the Monolog PHP logging library, which includes a variety
|
||||
| of powerful log handlers and formatters that you're free to use.
|
||||
|
|
||||
| Available drivers: "single", "daily", "slack", "syslog",
|
||||
| "errorlog", "monolog", "custom", "stack"
|
||||
|
|
||||
*/
|
||||
|
||||
'channels' => [
|
||||
|
||||
'stack' => [
|
||||
'driver' => 'stack',
|
||||
'channels' => explode(',', (string) env('LOG_STACK', 'single')),
|
||||
'ignore_exceptions' => false,
|
||||
],
|
||||
|
||||
'single' => [
|
||||
'driver' => 'single',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'daily' => [
|
||||
'driver' => 'daily',
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'days' => env('LOG_DAILY_DAYS', 14),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'driver' => 'slack',
|
||||
'url' => env('LOG_SLACK_WEBHOOK_URL'),
|
||||
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
|
||||
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
|
||||
'level' => env('LOG_LEVEL', 'critical'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'papertrail' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
|
||||
'handler_with' => [
|
||||
'host' => env('PAPERTRAIL_URL'),
|
||||
'port' => env('PAPERTRAIL_PORT'),
|
||||
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
|
||||
],
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'stderr' => [
|
||||
'driver' => 'monolog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'handler' => StreamHandler::class,
|
||||
'handler_with' => [
|
||||
'stream' => 'php://stderr',
|
||||
],
|
||||
'formatter' => env('LOG_STDERR_FORMATTER'),
|
||||
'processors' => [PsrLogMessageProcessor::class],
|
||||
],
|
||||
|
||||
'syslog' => [
|
||||
'driver' => 'syslog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'errorlog' => [
|
||||
'driver' => 'errorlog',
|
||||
'level' => env('LOG_LEVEL', 'debug'),
|
||||
'replace_placeholders' => true,
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'monolog',
|
||||
'handler' => NullHandler::class,
|
||||
],
|
||||
|
||||
'emergency' => [
|
||||
'path' => storage_path('logs/laravel.log'),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
118
config/mail.php
Normal file
118
config/mail.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Mailer
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default mailer that is used to send all email
|
||||
| messages unless another mailer is explicitly specified when sending
|
||||
| the message. All additional mailers can be configured within the
|
||||
| "mailers" array. Examples of each type of mailer are provided.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('MAIL_MAILER', 'log'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mailer Configurations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure all of the mailers used by your application plus
|
||||
| their respective settings. Several examples have been configured for
|
||||
| you and you are free to add your own as your application requires.
|
||||
|
|
||||
| Laravel supports a variety of mail "transport" drivers that can be used
|
||||
| when delivering an email. You may specify which one you're using for
|
||||
| your mailers below. You may also add additional mailers if needed.
|
||||
|
|
||||
| Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
|
||||
| "postmark", "resend", "log", "array",
|
||||
| "failover", "roundrobin"
|
||||
|
|
||||
*/
|
||||
|
||||
'mailers' => [
|
||||
|
||||
'smtp' => [
|
||||
'transport' => 'smtp',
|
||||
'scheme' => env('MAIL_SCHEME'),
|
||||
'url' => env('MAIL_URL'),
|
||||
'host' => env('MAIL_HOST', '127.0.0.1'),
|
||||
'port' => env('MAIL_PORT', 2525),
|
||||
'username' => env('MAIL_USERNAME'),
|
||||
'password' => env('MAIL_PASSWORD'),
|
||||
'timeout' => null,
|
||||
'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'transport' => 'ses',
|
||||
],
|
||||
|
||||
'postmark' => [
|
||||
'transport' => 'postmark',
|
||||
// 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
|
||||
// 'client' => [
|
||||
// 'timeout' => 5,
|
||||
// ],
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'transport' => 'resend',
|
||||
],
|
||||
|
||||
'sendmail' => [
|
||||
'transport' => 'sendmail',
|
||||
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'transport' => 'log',
|
||||
'channel' => env('MAIL_LOG_CHANNEL'),
|
||||
],
|
||||
|
||||
'array' => [
|
||||
'transport' => 'array',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'transport' => 'failover',
|
||||
'mailers' => [
|
||||
'smtp',
|
||||
'log',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
'roundrobin' => [
|
||||
'transport' => 'roundrobin',
|
||||
'mailers' => [
|
||||
'ses',
|
||||
'postmark',
|
||||
],
|
||||
'retry_after' => 60,
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Global "From" Address
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| You may wish for all emails sent by your application to be sent from
|
||||
| the same address. Here you may specify a name and address that is
|
||||
| used globally for all emails that are sent by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'from' => [
|
||||
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
|
||||
'name' => env('MAIL_FROM_NAME', 'Example'),
|
||||
],
|
||||
|
||||
];
|
||||
129
config/queue.php
Normal file
129
config/queue.php
Normal file
@ -0,0 +1,129 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Queue Connection Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Laravel's queue supports a variety of backends via a single, unified
|
||||
| API, giving you convenient access to each backend using identical
|
||||
| syntax for each. The default queue connection is defined below.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('QUEUE_CONNECTION', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may configure the connection options for every queue backend
|
||||
| used by your application. An example configuration is provided for
|
||||
| each backend supported by Laravel. You're also free to add more.
|
||||
|
|
||||
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
|
||||
| "deferred", "background", "failover", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'sync' => [
|
||||
'driver' => 'sync',
|
||||
],
|
||||
|
||||
'database' => [
|
||||
'driver' => 'database',
|
||||
'connection' => env('DB_QUEUE_CONNECTION'),
|
||||
'table' => env('DB_QUEUE_TABLE', 'jobs'),
|
||||
'queue' => env('DB_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'beanstalkd' => [
|
||||
'driver' => 'beanstalkd',
|
||||
'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
|
||||
'queue' => env('BEANSTALKD_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => 0,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'sqs' => [
|
||||
'driver' => 'sqs',
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
|
||||
'queue' => env('SQS_QUEUE', 'default'),
|
||||
'suffix' => env('SQS_SUFFIX'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
|
||||
'queue' => env('REDIS_QUEUE', 'default'),
|
||||
'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
|
||||
'block_for' => null,
|
||||
'after_commit' => false,
|
||||
],
|
||||
|
||||
'deferred' => [
|
||||
'driver' => 'deferred',
|
||||
],
|
||||
|
||||
'background' => [
|
||||
'driver' => 'background',
|
||||
],
|
||||
|
||||
'failover' => [
|
||||
'driver' => 'failover',
|
||||
'connections' => [
|
||||
'database',
|
||||
'deferred',
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Batching
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The following options configure the database and table that store job
|
||||
| batching information. These options can be updated to any database
|
||||
| connection and table which has been defined by your application.
|
||||
|
|
||||
*/
|
||||
|
||||
'batching' => [
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'job_batches',
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Failed Queue Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These options configure the behavior of failed queue job logging so you
|
||||
| can control how and where failed jobs are stored. Laravel ships with
|
||||
| support for storing failed jobs in a simple file or in a database.
|
||||
|
|
||||
| Supported drivers: "database-uuids", "dynamodb", "file", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'failed' => [
|
||||
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
|
||||
'database' => env('DB_CONNECTION', 'sqlite'),
|
||||
'table' => 'failed_jobs',
|
||||
],
|
||||
|
||||
];
|
||||
38
config/services.php
Normal file
38
config/services.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Third Party Services
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This file is for storing the credentials for third party services such
|
||||
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||
| location for this type of information, allowing packages to have
|
||||
| a conventional file to locate the various service credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'postmark' => [
|
||||
'key' => env('POSTMARK_API_KEY'),
|
||||
],
|
||||
|
||||
'resend' => [
|
||||
'key' => env('RESEND_API_KEY'),
|
||||
],
|
||||
|
||||
'ses' => [
|
||||
'key' => env('AWS_ACCESS_KEY_ID'),
|
||||
'secret' => env('AWS_SECRET_ACCESS_KEY'),
|
||||
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'notifications' => [
|
||||
'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
|
||||
'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
217
config/session.php
Normal file
217
config/session.php
Normal file
@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Session Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines the default session driver that is utilized for
|
||||
| incoming requests. Laravel supports a variety of storage options to
|
||||
| persist session data. Database storage is a great default choice.
|
||||
|
|
||||
| Supported: "file", "cookie", "database", "memcached",
|
||||
| "redis", "dynamodb", "array"
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => env('SESSION_DRIVER', 'database'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Lifetime
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may specify the number of minutes that you wish the session
|
||||
| to be allowed to remain idle before it expires. If you want them
|
||||
| to expire immediately when the browser is closed then you may
|
||||
| indicate that via the expire_on_close configuration option.
|
||||
|
|
||||
*/
|
||||
|
||||
'lifetime' => (int) env('SESSION_LIFETIME', 120),
|
||||
|
||||
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Encryption
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to easily specify that all of your session data
|
||||
| should be encrypted before it's stored. All encryption is performed
|
||||
| automatically by Laravel and you may use the session like normal.
|
||||
|
|
||||
*/
|
||||
|
||||
'encrypt' => env('SESSION_ENCRYPT', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session File Location
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When utilizing the "file" session driver, the session files are placed
|
||||
| on disk. The default storage location is defined here; however, you
|
||||
| are free to provide another location where they should be stored.
|
||||
|
|
||||
*/
|
||||
|
||||
'files' => storage_path('framework/sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" or "redis" session drivers, you may specify a
|
||||
| connection that should be used to manage these sessions. This should
|
||||
| correspond to a connection in your database configuration options.
|
||||
|
|
||||
*/
|
||||
|
||||
'connection' => env('SESSION_CONNECTION'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Database Table
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using the "database" session driver, you may specify the table to
|
||||
| be used to store sessions. Of course, a sensible default is defined
|
||||
| for you; however, you're welcome to change this to another table.
|
||||
|
|
||||
*/
|
||||
|
||||
'table' => env('SESSION_TABLE', 'sessions'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cache Store
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When using one of the framework's cache driven session backends, you may
|
||||
| define the cache store which should be used to store the session data
|
||||
| between requests. This must match one of your defined cache stores.
|
||||
|
|
||||
| Affects: "dynamodb", "memcached", "redis"
|
||||
|
|
||||
*/
|
||||
|
||||
'store' => env('SESSION_STORE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Sweeping Lottery
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Some session drivers must manually sweep their storage location to get
|
||||
| rid of old sessions from storage. Here are the chances that it will
|
||||
| happen on a given request. By default, the odds are 2 out of 100.
|
||||
|
|
||||
*/
|
||||
|
||||
'lottery' => [2, 100],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Name
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may change the name of the session cookie that is created by
|
||||
| the framework. Typically, you should not need to change this value
|
||||
| since doing so does not grant a meaningful security improvement.
|
||||
|
|
||||
*/
|
||||
|
||||
'cookie' => env(
|
||||
'SESSION_COOKIE',
|
||||
Str::slug((string) env('APP_NAME', 'laravel')).'-session'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The session cookie path determines the path for which the cookie will
|
||||
| be regarded as available. Typically, this will be the root path of
|
||||
| your application, but you're free to change this when necessary.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('SESSION_PATH', '/'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Session Cookie Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value determines the domain and subdomains the session cookie is
|
||||
| available to. By default, the cookie will be available to the root
|
||||
| domain without subdomains. Typically, this shouldn't be changed.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('SESSION_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTPS Only Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By setting this option to true, session cookies will only be sent back
|
||||
| to the server if the browser has a HTTPS connection. This will keep
|
||||
| the cookie from being sent to you when it can't be done securely.
|
||||
|
|
||||
*/
|
||||
|
||||
'secure' => env('SESSION_SECURE_COOKIE'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| HTTP Access Only
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will prevent JavaScript from accessing the
|
||||
| value of the cookie and the cookie will only be accessible through
|
||||
| the HTTP protocol. It's unlikely you should disable this option.
|
||||
|
|
||||
*/
|
||||
|
||||
'http_only' => env('SESSION_HTTP_ONLY', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Same-Site Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option determines how your cookies behave when cross-site requests
|
||||
| take place, and can be used to mitigate CSRF attacks. By default, we
|
||||
| will set this value to "lax" to permit secure cross-site requests.
|
||||
|
|
||||
| See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
|
||||
|
|
||||
| Supported: "lax", "strict", "none", null
|
||||
|
|
||||
*/
|
||||
|
||||
'same_site' => env('SESSION_SAME_SITE', 'lax'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Partitioned Cookies
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Setting this value to true will tie the cookie to the top-level site for
|
||||
| a cross-site context. Partitioned cookies are accepted by the browser
|
||||
| when flagged "secure" and the Same-Site attribute is set to "none".
|
||||
|
|
||||
*/
|
||||
|
||||
'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
|
||||
|
||||
];
|
||||
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
*.sqlite*
|
||||
44
database/factories/UserFactory.php
Normal file
44
database/factories/UserFactory.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
|
||||
*/
|
||||
class UserFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* The current password being used by the factory.
|
||||
*/
|
||||
protected static ?string $password;
|
||||
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'remember_token' => Str::random(10),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model's email address should be unverified.
|
||||
*/
|
||||
public function unverified(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'email_verified_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('cache', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->mediumText('value');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
|
||||
Schema::create('cache_locks', function (Blueprint $table) {
|
||||
$table->string('key')->primary();
|
||||
$table->string('owner');
|
||||
$table->integer('expiration');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cache');
|
||||
Schema::dropIfExists('cache_locks');
|
||||
}
|
||||
};
|
||||
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
57
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('queue')->index();
|
||||
$table->longText('payload');
|
||||
$table->unsignedTinyInteger('attempts');
|
||||
$table->unsignedInteger('reserved_at')->nullable();
|
||||
$table->unsignedInteger('available_at');
|
||||
$table->unsignedInteger('created_at');
|
||||
});
|
||||
|
||||
Schema::create('job_batches', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->string('name');
|
||||
$table->integer('total_jobs');
|
||||
$table->integer('pending_jobs');
|
||||
$table->integer('failed_jobs');
|
||||
$table->longText('failed_job_ids');
|
||||
$table->mediumText('options')->nullable();
|
||||
$table->integer('cancelled_at')->nullable();
|
||||
$table->integer('created_at');
|
||||
$table->integer('finished_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('failed_jobs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('uuid')->unique();
|
||||
$table->text('connection');
|
||||
$table->text('queue');
|
||||
$table->longText('payload');
|
||||
$table->longText('exception');
|
||||
$table->timestamp('failed_at')->useCurrent();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jobs');
|
||||
Schema::dropIfExists('job_batches');
|
||||
Schema::dropIfExists('failed_jobs');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('patients', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name')->comment('姓名');
|
||||
$table->string('gender')->comment('性别');
|
||||
$table->integer('age')->comment('年龄');
|
||||
$table->string('diagnosis')->comment('出院诊断');
|
||||
$table->date('discharge_date')->comment('转诊时间');
|
||||
$table->string('address')->nullable()->comment('户籍地址');
|
||||
$table->string('phone')->nullable()->comment('联系方式');
|
||||
$table->string('remark')->nullable()->comment('备注');
|
||||
$table->integer('follow_up_count')->default(0)->comment('已随访次数');
|
||||
$table->date('last_follow_up_date')->nullable()->comment('上次随访日期');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('patients');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('patients', function (Blueprint $table) {
|
||||
$table->foreignId('user_id')->after('id')->constrained()->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('patients', function (Blueprint $table) {
|
||||
$table->dropForeign(['user_id']);
|
||||
$table->dropColumn('user_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
25
database/seeders/DatabaseSeeder.php
Normal file
25
database/seeders/DatabaseSeeder.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
use WithoutModelEvents;
|
||||
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
]);
|
||||
}
|
||||
}
|
||||
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "https://www.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"concurrently": "^9.0.1",
|
||||
"laravel-vite-plugin": "^2.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.0.7"
|
||||
}
|
||||
}
|
||||
35
phpunit.xml
Normal file
35
phpunit.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Unit">
|
||||
<directory>tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory>tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="APP_MAINTENANCE_DRIVER" value="file"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||
<env name="CACHE_STORE" value="array"/>
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="TELESCOPE_ENABLED" value="false"/>
|
||||
<env name="NIGHTWATCH_ENABLED" value="false"/>
|
||||
</php>
|
||||
</phpunit>
|
||||
25
public/.htaccess
Normal file
25
public/.htaccess
Normal file
@ -0,0 +1,25 @@
|
||||
<IfModule mod_rewrite.c>
|
||||
<IfModule mod_negotiation.c>
|
||||
Options -MultiViews -Indexes
|
||||
</IfModule>
|
||||
|
||||
RewriteEngine On
|
||||
|
||||
# Handle Authorization Header
|
||||
RewriteCond %{HTTP:Authorization} .
|
||||
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
|
||||
|
||||
# Handle X-XSRF-Token Header
|
||||
RewriteCond %{HTTP:x-xsrf-token} .
|
||||
RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
|
||||
|
||||
# Redirect Trailing Slashes If Not A Folder...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_URI} (.+)/$
|
||||
RewriteRule ^ %1 [L,R=301]
|
||||
|
||||
# Send Requests To Front Controller...
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [L]
|
||||
</IfModule>
|
||||
0
public/favicon.ico
Normal file
0
public/favicon.ico
Normal file
20
public/index.php
Normal file
20
public/index.php
Normal file
@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
define('LARAVEL_START', microtime(true));
|
||||
|
||||
// Determine if the application is in maintenance mode...
|
||||
if (file_exists($maintenance = __DIR__.'/../storage/framework/maintenance.php')) {
|
||||
require $maintenance;
|
||||
}
|
||||
|
||||
// Register the Composer autoloader...
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
// Bootstrap Laravel and handle the request...
|
||||
/** @var Application $app */
|
||||
$app = require_once __DIR__.'/../bootstrap/app.php';
|
||||
|
||||
$app->handleRequest(Request::capture());
|
||||
316
public/install.php
Normal file
316
public/install.php
Normal file
@ -0,0 +1,316 @@
|
||||
<?php
|
||||
/**
|
||||
* 数据库安装脚本
|
||||
* 访问此文件创建/更新数据库表
|
||||
* 安装完成后请删除此文件
|
||||
*/
|
||||
|
||||
// 数据库配置 - 根据你的 .env 文件配置
|
||||
$host = '127.0.0.1';
|
||||
$port = '3306';
|
||||
$database = 'reminder';
|
||||
$username = 'root';
|
||||
$password = '';
|
||||
|
||||
// 尝试从 Laravel .env 文件读取配置
|
||||
$envPath = __DIR__ . '/../.env';
|
||||
if (file_exists($envPath)) {
|
||||
$envContent = file_get_contents($envPath);
|
||||
|
||||
if (preg_match('/DB_HOST=(.*)/', $envContent, $matches)) {
|
||||
$host = trim($matches[1]);
|
||||
}
|
||||
if (preg_match('/DB_PORT=(.*)/', $envContent, $matches)) {
|
||||
$port = trim($matches[1]);
|
||||
}
|
||||
if (preg_match('/DB_DATABASE=(.*)/', $envContent, $matches)) {
|
||||
$database = trim($matches[1]);
|
||||
}
|
||||
if (preg_match('/DB_USERNAME=(.*)/', $envContent, $matches)) {
|
||||
$username = trim($matches[1]);
|
||||
}
|
||||
if (preg_match('/DB_PASSWORD=(.*)/', $envContent, $matches)) {
|
||||
$password = trim($matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
$messages = [];
|
||||
$error = '';
|
||||
|
||||
try {
|
||||
$dsn = "mysql:host={$host};port={$port};dbname={$database};charset=utf8mb4";
|
||||
$pdo = new PDO($dsn, $username, $password);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// 1. 创建 users 表
|
||||
$sql = "
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(255) NOT NULL COMMENT '姓名',
|
||||
`email` varchar(255) NOT NULL COMMENT '邮箱',
|
||||
`email_verified_at` timestamp NULL DEFAULT NULL,
|
||||
`password` varchar(255) NOT NULL COMMENT '密码',
|
||||
`remember_token` varchar(100) DEFAULT NULL,
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `users_email_unique` (`email`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
";
|
||||
$pdo->exec($sql);
|
||||
$messages[] = '✅ 用户表 users 创建/已存在';
|
||||
|
||||
// 2. 检查 patients 表是否存在
|
||||
$stmt = $pdo->query("SHOW TABLES LIKE 'patients'");
|
||||
$patientsTableExists = $stmt->rowCount() > 0;
|
||||
|
||||
if ($patientsTableExists) {
|
||||
// 表已存在,检查是否有 user_id 字段
|
||||
$stmt = $pdo->query("SHOW COLUMNS FROM `patients` LIKE 'user_id'");
|
||||
$hasUserId = $stmt->rowCount() > 0;
|
||||
|
||||
if (!$hasUserId) {
|
||||
// 检查是否有数据
|
||||
$countStmt = $pdo->query("SELECT COUNT(*) FROM `patients`");
|
||||
$count = $countStmt->fetchColumn();
|
||||
|
||||
if ($count > 0) {
|
||||
// 有数据,需要先创建一个默认用户
|
||||
// 检查是否已有用户
|
||||
$userCountStmt = $pdo->query("SELECT COUNT(*) FROM `users`");
|
||||
$userCount = $userCountStmt->fetchColumn();
|
||||
|
||||
if ($userCount == 0) {
|
||||
// 创建默认管理员用户
|
||||
$defaultPassword = password_hash('admin123', PASSWORD_BCRYPT);
|
||||
$pdo->exec("INSERT INTO `users` (`name`, `email`, `password`, `created_at`, `updated_at`)
|
||||
VALUES ('管理员', 'admin@example.com', '{$defaultPassword}', NOW(), NOW())");
|
||||
$messages[] = '✅ 已创建默认用户: admin@example.com (密码: admin123)';
|
||||
}
|
||||
|
||||
// 获取第一个用户的 ID
|
||||
$firstUserStmt = $pdo->query("SELECT id FROM `users` ORDER BY id ASC LIMIT 1");
|
||||
$firstUserId = $firstUserStmt->fetchColumn();
|
||||
|
||||
// 添加 user_id 字段(允许 NULL)
|
||||
$pdo->exec("ALTER TABLE `patients` ADD COLUMN `user_id` bigint(20) unsigned NULL AFTER `id`");
|
||||
$messages[] = '✅ 已添加 user_id 字段到 patients 表';
|
||||
|
||||
// 将现有数据关联到第一个用户
|
||||
$pdo->exec("UPDATE `patients` SET `user_id` = {$firstUserId} WHERE `user_id` IS NULL");
|
||||
$messages[] = "✅ 已将现有 {$count} 条患者数据关联到用户 ID: {$firstUserId}";
|
||||
|
||||
// 修改字段为 NOT NULL
|
||||
$pdo->exec("ALTER TABLE `patients` MODIFY COLUMN `user_id` bigint(20) unsigned NOT NULL");
|
||||
|
||||
// 添加外键约束
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE `patients` ADD KEY `patients_user_id_foreign` (`user_id`)");
|
||||
$pdo->exec("ALTER TABLE `patients` ADD CONSTRAINT `patients_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE");
|
||||
$messages[] = '✅ 已添加外键约束';
|
||||
} catch (PDOException $e) {
|
||||
// 外键可能已存在,忽略
|
||||
$messages[] = '⚠️ 外键约束已存在或无法添加';
|
||||
}
|
||||
} else {
|
||||
// 没有数据,直接添加字段和约束
|
||||
$pdo->exec("ALTER TABLE `patients` ADD COLUMN `user_id` bigint(20) unsigned NOT NULL AFTER `id`");
|
||||
try {
|
||||
$pdo->exec("ALTER TABLE `patients` ADD KEY `patients_user_id_foreign` (`user_id`)");
|
||||
$pdo->exec("ALTER TABLE `patients` ADD CONSTRAINT `patients_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE");
|
||||
} catch (PDOException $e) {
|
||||
// 忽略
|
||||
}
|
||||
$messages[] = '✅ 已添加 user_id 字段到 patients 表';
|
||||
}
|
||||
} else {
|
||||
$messages[] = '✅ patients 表已有 user_id 字段';
|
||||
}
|
||||
} else {
|
||||
// 创建新的 patients 表(包含 user_id)
|
||||
$sql = "
|
||||
CREATE TABLE `patients` (
|
||||
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
`user_id` bigint(20) unsigned NOT NULL COMMENT '所属用户',
|
||||
`name` varchar(255) NOT NULL COMMENT '姓名',
|
||||
`gender` varchar(255) NOT NULL COMMENT '性别',
|
||||
`age` int(11) NOT NULL COMMENT '年龄',
|
||||
`diagnosis` varchar(255) NOT NULL COMMENT '出院诊断',
|
||||
`discharge_date` date NOT NULL COMMENT '转诊时间',
|
||||
`address` varchar(255) DEFAULT NULL COMMENT '户籍地址',
|
||||
`phone` varchar(255) DEFAULT NULL COMMENT '联系方式',
|
||||
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
|
||||
`follow_up_count` int(11) NOT NULL DEFAULT 0 COMMENT '已随访次数',
|
||||
`last_follow_up_date` date DEFAULT NULL COMMENT '上次随访日期',
|
||||
`created_at` timestamp NULL DEFAULT NULL,
|
||||
`updated_at` timestamp NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `patients_user_id_foreign` (`user_id`),
|
||||
CONSTRAINT `patients_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
";
|
||||
$pdo->exec($sql);
|
||||
$messages[] = '✅ 患者表 patients 创建成功';
|
||||
}
|
||||
|
||||
// 3. 创建 sessions 表
|
||||
$sql = "
|
||||
CREATE TABLE IF NOT EXISTS `sessions` (
|
||||
`id` varchar(255) NOT NULL,
|
||||
`user_id` bigint(20) unsigned DEFAULT NULL,
|
||||
`ip_address` varchar(45) DEFAULT NULL,
|
||||
`user_agent` text DEFAULT NULL,
|
||||
`payload` longtext NOT NULL,
|
||||
`last_activity` int(11) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `sessions_user_id_index` (`user_id`),
|
||||
KEY `sessions_last_activity_index` (`last_activity`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
";
|
||||
$pdo->exec($sql);
|
||||
$messages[] = '✅ 会话表 sessions 创建/已存在';
|
||||
|
||||
// 显示表结构
|
||||
$stmt = $pdo->query("SHOW TABLES");
|
||||
$tables = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||
$messages[] = '📋 当前数据库表: ' . implode(', ', $tables);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$error = '❌ 数据库操作失败: ' . $e->getMessage();
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>数据库安装 - 病例回访提醒系统</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #f0f4f8 0%, #e8f2ff 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 40px;
|
||||
max-width: 650px;
|
||||
width: 100%;
|
||||
box-shadow: 0 20px 60px rgba(0, 102, 204, 0.15);
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
.success {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border: 1px solid #10b981;
|
||||
color: #059669;
|
||||
}
|
||||
.error {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
.warning {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid #f59e0b;
|
||||
color: #d97706;
|
||||
}
|
||||
.info {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border: 1px solid #6366f1;
|
||||
color: #666;
|
||||
}
|
||||
.info strong { color: #333; }
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
margin-top: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
.btn-secondary {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
.config {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin: 20px 0;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.config div { margin: 4px 0; }
|
||||
.config span { color: #6366f1; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🏥 数据库安装/升级</h1>
|
||||
|
||||
<?php if (!empty($messages)): ?>
|
||||
<?php foreach ($messages as $msg): ?>
|
||||
<?php
|
||||
$class = 'success';
|
||||
if (strpos($msg, '⚠️') !== false) $class = 'warning';
|
||||
if (strpos($msg, '📋') !== false) $class = 'info';
|
||||
?>
|
||||
<div class="message <?= $class ?>"><?= $msg ?></div>
|
||||
<?php endforeach; ?>
|
||||
<div class="message info">
|
||||
<strong>⚠️ 安全提示:</strong>安装完成后,请删除此文件 (install.php) 以确保系统安全。
|
||||
</div>
|
||||
<div>
|
||||
<a href="/login" class="btn">进入登录页 →</a>
|
||||
<a href="/register" class="btn btn-secondary">注册新用户 →</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="message error"><?= htmlspecialchars($error) ?></div>
|
||||
<div class="config">
|
||||
<div>当前数据库配置:</div>
|
||||
<div>Host: <span><?= htmlspecialchars($host) ?></span></div>
|
||||
<div>Port: <span><?= htmlspecialchars($port) ?></span></div>
|
||||
<div>Database: <span><?= htmlspecialchars($database) ?></span></div>
|
||||
<div>Username: <span><?= htmlspecialchars($username) ?></span></div>
|
||||
</div>
|
||||
<div class="message info">
|
||||
<strong>解决方法:</strong><br>
|
||||
1. 确保 MySQL 服务已启动<br>
|
||||
2. 确保数据库 "<?= htmlspecialchars($database) ?>" 已创建<br>
|
||||
3. 检查 .env 文件中的数据库配置是否正确
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1026
public/reminder.php
Normal file
1026
public/reminder.php
Normal file
File diff suppressed because it is too large
Load Diff
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow:
|
||||
11
resources/css/app.css
Normal file
11
resources/css/app.css
Normal file
@ -0,0 +1,11 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
1
resources/js/app.js
Normal file
1
resources/js/app.js
Normal file
@ -0,0 +1 @@
|
||||
import './bootstrap';
|
||||
4
resources/js/bootstrap.js
vendored
Normal file
4
resources/js/bootstrap.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
360
resources/views/auth/login.blade.php
Normal file
360
resources/views/auth/login.blade.php
Normal file
@ -0,0 +1,360 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - 患者随访管理系统</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-primary: #0066cc;
|
||||
--color-primary-light: #e8f2ff;
|
||||
--color-primary-dark: #004d99;
|
||||
--color-success: #28a745;
|
||||
--color-danger: #dc3545;
|
||||
--color-danger-light: #fce8e8;
|
||||
--color-text: #333;
|
||||
--color-text-light: #666;
|
||||
--color-bg: #f0f4f8;
|
||||
--color-white: #ffffff;
|
||||
--color-border: #dce3eb;
|
||||
--shadow-sm: 0 2px 8px rgba(0,102,204,0.08);
|
||||
--shadow-md: 0 8px 24px rgba(0,102,204,0.12);
|
||||
--shadow-lg: 0 16px 48px rgba(0,102,204,0.15);
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #e8f2ff 0%, #f0f4f8 50%, #e8f0f5 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 48px 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.auth-logo-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 8px 20px rgba(0,102,204,0.25);
|
||||
}
|
||||
|
||||
.auth-logo h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.auth-logo p {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 15px;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 4px var(--color-primary-light);
|
||||
}
|
||||
|
||||
.form-input.error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.form-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-light);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-checkbox input {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark) 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(0,102,204,0.3);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,102,204,0.4);
|
||||
}
|
||||
|
||||
.btn-primary:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.auth-footer p {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auth-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
text-align: right;
|
||||
margin-top: -12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.forgot-password a {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-light);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.forgot-password a:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.auth-card {
|
||||
padding: 32px 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.auth-logo-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.auth-logo h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.auth-logo p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.auth-footer p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-logo-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-logo h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
<div class="auth-logo-icon">👨⚕️</div>
|
||||
<h1>患者随访管理系统</h1>
|
||||
<p>登录您的账户</p>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="error-message">
|
||||
@foreach ($errors->all() as $error)
|
||||
{{ $error }}
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('login') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">邮箱地址</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="form-input @error('email') error @enderror"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="请输入邮箱"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="form-input @error('password') error @enderror"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-checkbox">
|
||||
<input type="checkbox" name="remember" {{ old('remember') ? 'checked' : '' }}>
|
||||
记住我
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">登 录</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>还没有账户? <a href="{{ route('register') }}">立即注册</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
358
resources/views/auth/register.blade.php
Normal file
358
resources/views/auth/register.blade.php
Normal file
@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>注册 - 患者随访管理系统</title>
|
||||
<style>
|
||||
:root {
|
||||
--color-primary: #0066cc;
|
||||
--color-primary-light: #e8f2ff;
|
||||
--color-primary-dark: #004d99;
|
||||
--color-success: #28a745;
|
||||
--color-danger: #dc3545;
|
||||
--color-danger-light: #fce8e8;
|
||||
--color-text: #333;
|
||||
--color-text-light: #666;
|
||||
--color-bg: #f0f4f8;
|
||||
--color-white: #ffffff;
|
||||
--color-border: #dce3eb;
|
||||
--shadow-sm: 0 2px 8px rgba(0,102,204,0.08);
|
||||
--shadow-md: 0 8px 24px rgba(0,102,204,0.12);
|
||||
--shadow-lg: 0 16px 48px rgba(0,102,204,0.15);
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 20px;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
background: linear-gradient(135deg, #e8f2ff 0%, #f0f4f8 50%, #e8f0f5 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.auth-container {
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: var(--color-white);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
padding: 48px 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.auth-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--color-success) 0%, #20c997 100%);
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.auth-logo-icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, var(--color-success) 0%, #20c997 100%);
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 28px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 8px 20px rgba(40,167,69,0.25);
|
||||
}
|
||||
|
||||
.auth-logo h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.auth-logo p {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
border: 2px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 15px;
|
||||
transition: all 0.2s ease;
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-success);
|
||||
box-shadow: 0 0 0 4px rgba(40,167,69,0.15);
|
||||
}
|
||||
|
||||
.form-input.error {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-light);
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px 24px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, var(--color-success) 0%, #20c997 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 12px rgba(40,167,69,0.3);
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(40,167,69,0.4);
|
||||
}
|
||||
|
||||
.btn-success:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
text-align: center;
|
||||
margin-top: 24px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.auth-footer p {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.auth-footer a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.auth-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.auth-card {
|
||||
padding: 32px 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.auth-logo-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.auth-logo h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.auth-logo p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.auth-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.auth-footer p {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 360px) {
|
||||
body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.auth-logo-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-logo h1 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="auth-container">
|
||||
<div class="auth-card">
|
||||
<div class="auth-logo">
|
||||
<div class="auth-logo-icon">✨</div>
|
||||
<h1>创建新账户</h1>
|
||||
<p>加入患者随访管理系统</p>
|
||||
</div>
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="error-message">
|
||||
@foreach ($errors->all() as $error)
|
||||
<div>{{ $error }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('register') }}">
|
||||
@csrf
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="name">姓名</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
class="form-input @error('name') error @enderror"
|
||||
value="{{ old('name') }}"
|
||||
placeholder="请输入您的姓名"
|
||||
required
|
||||
autofocus
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="email">邮箱地址</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
class="form-input @error('email') error @enderror"
|
||||
value="{{ old('email') }}"
|
||||
placeholder="请输入邮箱"
|
||||
required
|
||||
>
|
||||
<div class="form-hint">用于登录和找回密码</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password">设置密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
class="form-input @error('password') error @enderror"
|
||||
placeholder="请设置密码(至少6位)"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="password_confirmation">确认密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password_confirmation"
|
||||
name="password_confirmation"
|
||||
class="form-input"
|
||||
placeholder="请再次输入密码"
|
||||
required
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-success">注 册</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-footer">
|
||||
<p>已有账户? <a href="{{ route('login') }}">立即登录</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
1642
resources/views/layouts/app.blade.php
Normal file
1642
resources/views/layouts/app.blade.php
Normal file
File diff suppressed because it is too large
Load Diff
270
resources/views/patients/import.blade.php
Normal file
270
resources/views/patients/import.blade.php
Normal file
@ -0,0 +1,270 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '导入数据 - 病例回访提醒系统')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">导入患者数据</h1>
|
||||
<p class="page-subtitle">支持 Excel (.xlsx) 和 CSV 格式的文件导入</p>
|
||||
</div>
|
||||
|
||||
<!-- .xls 格式提示 -->
|
||||
<div class="card" style="background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%); border-color: #f59e0b;">
|
||||
<div style="display: flex; gap: 14px; align-items: flex-start;">
|
||||
<span style="font-size: 28px;">⚠️</span>
|
||||
<div>
|
||||
<div style="font-weight: 600; color: #b45309; margin-bottom: 8px;">关于 .xls 格式(Excel 97-2003)</div>
|
||||
<div style="color: #92400e; font-size: 14px; line-height: 1.8;">
|
||||
如果你的文件是 <strong>.xls 格式</strong>,请先转换为新格式:<br>
|
||||
1. 在 Excel 中打开 .xls 文件<br>
|
||||
2. 点击 <strong>文件 → 另存为</strong><br>
|
||||
3. 选择 <strong>"Excel 工作簿 (*.xlsx)"</strong> 或 <strong>"CSV UTF-8"</strong><br>
|
||||
4. 保存后重新上传
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">上传文件</span>
|
||||
<a href="{{ route('patients.template') }}" class="btn btn-outline btn-sm">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
下载导入模板 (Excel)
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form action="{{ route('patients.import.store') }}" method="POST" enctype="multipart/form-data" id="importForm">
|
||||
@csrf
|
||||
|
||||
<div class="file-upload" id="dropZone">
|
||||
<svg class="file-upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
<div class="file-upload-text">点击或拖拽文件到此处上传</div>
|
||||
<div class="file-upload-hint">支持 .xlsx 和 .csv 格式,最大 10MB</div>
|
||||
<input type="file" name="file" id="fileInput" accept=".xlsx,.csv" required>
|
||||
</div>
|
||||
|
||||
<div id="fileInfo" style="display: none; margin-top: 20px; padding: 16px; background: var(--color-bg-secondary); border-radius: var(--radius-sm); border: 1px solid var(--color-border);">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="24" height="24" style="color: var(--color-success);">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div id="fileName" style="font-weight: 500;"></div>
|
||||
<div id="fileSize" style="font-size: 13px; color: var(--color-text-muted);"></div>
|
||||
</div>
|
||||
<button type="button" id="removeFile" style="margin-left: auto; background: none; border: none; cursor: pointer; color: var(--color-text-muted);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 24px;">
|
||||
<button type="submit" class="btn btn-primary" id="submitBtn" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
开始导入
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 导入说明 -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">📋 导入说明</span>
|
||||
</div>
|
||||
|
||||
<div style="line-height: 1.8;">
|
||||
<h4 style="margin-bottom: 12px; color: var(--color-text);">文件格式要求</h4>
|
||||
<p style="color: var(--color-text-secondary); margin-bottom: 16px;">
|
||||
Excel 或 CSV 文件需按以下列顺序排列:
|
||||
</p>
|
||||
|
||||
<div class="table-container" style="margin-bottom: 24px;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>列序号</th>
|
||||
<th>字段名称</th>
|
||||
<th>说明</th>
|
||||
<th>必填</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>姓名</td>
|
||||
<td>患者姓名</td>
|
||||
<td><span class="badge badge-danger">是</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>性别</td>
|
||||
<td>男/女</td>
|
||||
<td><span class="badge badge-danger">是</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>年龄</td>
|
||||
<td>数字</td>
|
||||
<td><span class="badge badge-danger">是</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>出院诊断</td>
|
||||
<td>脑卒中、心肌梗塞、慢性肾脏病 等</td>
|
||||
<td><span class="badge badge-danger">是</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>转诊时间</td>
|
||||
<td>支持格式:2025.12.01、2025-12-01、2025/12/01</td>
|
||||
<td><span class="badge badge-danger">是</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>户籍地址</td>
|
||||
<td>详细地址</td>
|
||||
<td><span class="badge badge-info">否</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>联系方式</td>
|
||||
<td>电话号码</td>
|
||||
<td><span class="badge badge-info">否</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>备注</td>
|
||||
<td>其他备注信息</td>
|
||||
<td><span class="badge badge-info">否</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h4 style="margin-bottom: 12px; color: var(--color-text);">随访时间规则</h4>
|
||||
<div style="display: grid; gap: 12px; margin-bottom: 24px;">
|
||||
<div style="padding: 16px; background: var(--color-bg-secondary); border-radius: var(--radius-sm); border-left: 4px solid var(--color-primary);">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">🧠 脑卒中 / 心肌梗塞</div>
|
||||
<div style="color: var(--color-text-secondary);">
|
||||
第1次: 起病后 <strong>1个月</strong> ·
|
||||
第2次: 起病后 <strong>3个月</strong> ·
|
||||
第3次: 起病后 <strong>6个月</strong> ·
|
||||
第4次: 起病后 <strong>12个月</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 16px; background: var(--color-bg-secondary); border-radius: var(--radius-sm); border-left: 4px solid var(--color-warning);">
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">🫘 慢性肾脏病</div>
|
||||
<div style="color: var(--color-text-secondary);">
|
||||
第1次: 起病后 <strong>1个月</strong> ·
|
||||
第2次: 起病后 <strong>2个月</strong> ·
|
||||
第3次: 起病后 <strong>3个月</strong> ·
|
||||
第4次: 起病后 <strong>6个月</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="padding: 16px; background: rgba(29, 155, 240, 0.1); border-radius: var(--radius-sm);">
|
||||
<div style="display: flex; gap: 12px; align-items: flex-start;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20" style="color: var(--color-primary); flex-shrink: 0; margin-top: 2px;">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>
|
||||
<div style="color: var(--color-text-secondary); font-size: 14px;">
|
||||
<strong style="color: var(--color-text);">提示:</strong>系统会根据出院诊断自动判断随访时间规则。
|
||||
如果诊断信息中包含"肾"字,将按慢性肾脏病规则处理;
|
||||
否则按脑卒中/心肌梗塞规则处理。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const dropZone = document.getElementById('dropZone');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileInfo = document.getElementById('fileInfo');
|
||||
const fileName = document.getElementById('fileName');
|
||||
const fileSize = document.getElementById('fileSize');
|
||||
const removeFile = document.getElementById('removeFile');
|
||||
const submitBtn = document.getElementById('submitBtn');
|
||||
|
||||
// 点击上传区域触发文件选择
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
|
||||
// 拖拽事件
|
||||
dropZone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.add('dragover');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('dragleave', () => {
|
||||
dropZone.classList.remove('dragover');
|
||||
});
|
||||
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropZone.classList.remove('dragover');
|
||||
|
||||
const files = e.dataTransfer.files;
|
||||
if (files.length > 0) {
|
||||
fileInput.files = files;
|
||||
updateFileInfo(files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// 文件选择变化
|
||||
fileInput.addEventListener('change', function() {
|
||||
if (this.files.length > 0) {
|
||||
updateFileInfo(this.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
// 更新文件信息显示
|
||||
function updateFileInfo(file) {
|
||||
fileName.textContent = file.name;
|
||||
fileSize.textContent = formatFileSize(file.size);
|
||||
fileInfo.style.display = 'block';
|
||||
dropZone.style.display = 'none';
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
removeFile.addEventListener('click', () => {
|
||||
fileInput.value = '';
|
||||
fileInfo.style.display = 'none';
|
||||
dropZone.style.display = 'block';
|
||||
submitBtn.disabled = true;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
495
resources/views/patients/index.blade.php
Normal file
495
resources/views/patients/index.blade.php
Normal file
@ -0,0 +1,495 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '患者列表 - 病例回访提醒系统')
|
||||
|
||||
@section('content')
|
||||
<style>
|
||||
/* 移动端卡片列表 */
|
||||
.mobile-patient-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-patient-card {
|
||||
background: var(--color-bg-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mobile-patient-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mobile-patient-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.mobile-patient-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-patient-avatar.male {
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
}
|
||||
|
||||
.mobile-patient-avatar.female {
|
||||
background: linear-gradient(135deg, #ec4899 0%, #f472b6 100%);
|
||||
}
|
||||
|
||||
.mobile-patient-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--color-text);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.mobile-patient-meta {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.mobile-patient-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background: var(--color-bg);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mobile-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mobile-info-label {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.mobile-info-value {
|
||||
font-size: 13px;
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mobile-info-value a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.mobile-patient-progress {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mobile-progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.mobile-progress-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.mobile-progress-value {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.mobile-progress-bar {
|
||||
height: 6px;
|
||||
background: var(--color-border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-success), #34d399);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-patient-remark {
|
||||
padding: 10px 12px;
|
||||
background: var(--color-warning-light);
|
||||
border-left: 3px solid var(--color-warning);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
color: var(--color-warning);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mobile-patient-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mobile-patient-actions .btn {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.mobile-patient-actions form {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mobile-patient-actions form .btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 移动端显示控制 */
|
||||
@media (max-width: 768px) {
|
||||
.table-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-patient-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.custom-pagination {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.pagination-info {
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pagination-links {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">患者列表</h1>
|
||||
<p class="page-subtitle">管理所有患者信息,查看随访状态</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="search-box">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
<form action="{{ route('patients.index') }}" method="GET">
|
||||
<input type="text" class="form-control" name="search" placeholder="搜索姓名、电话、地址..." value="{{ request('search') }}">
|
||||
</form>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<a href="{{ route('patients.import') }}" class="btn btn-primary">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>
|
||||
导入数据
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($patients->isEmpty())
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="9" cy="7" r="4"/>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||
</svg>
|
||||
<div class="empty-state-title">暂无患者数据</div>
|
||||
<p>请先导入患者数据,支持 Excel (.xlsx) 和 CSV 格式</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<a href="{{ route('patients.import') }}" class="btn btn-primary">导入患者数据</a>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- 桌面端表格 -->
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>姓名</th>
|
||||
<th>性别</th>
|
||||
<th>年龄</th>
|
||||
<th>出院诊断</th>
|
||||
<th>转诊日期</th>
|
||||
<th>随访进度</th>
|
||||
<th>状态</th>
|
||||
<th>联系方式</th>
|
||||
<th>备注</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($patients as $patient)
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ $patient->name }}</strong>
|
||||
</td>
|
||||
<td>{{ $patient->gender }}</td>
|
||||
<td>{{ $patient->age }}岁</td>
|
||||
<td>{{ $patient->getDiagnosisType() }}</td>
|
||||
<td>{{ $patient->discharge_date->format('Y-m-d') }}</td>
|
||||
<td>
|
||||
@php
|
||||
$total = count($patient->getFollowUpSchedule());
|
||||
$completed = $patient->follow_up_count;
|
||||
@endphp
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<div style="width: 60px; height: 6px; background: var(--color-border); border-radius: 3px; overflow: hidden;">
|
||||
<div style="width: {{ ($completed / $total) * 100 }}%; height: 100%; background: var(--color-success); border-radius: 3px;"></div>
|
||||
</div>
|
||||
<span style="font-size: 13px; color: var(--color-text-secondary);">{{ $completed }}/{{ $total }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@php
|
||||
$status = $patient->getFollowUpStatus();
|
||||
$nextDate = $patient->getNextFollowUpDate();
|
||||
@endphp
|
||||
@if($patient->isCompleted())
|
||||
<span class="badge badge-success">已完成</span>
|
||||
@elseif(str_contains($status, '过期'))
|
||||
<span class="badge badge-danger">{{ $status }}</span>
|
||||
@elseif(str_contains($status, '今日'))
|
||||
<span class="badge badge-warning">{{ $status }}</span>
|
||||
@elseif(str_contains($status, '即将'))
|
||||
<span class="badge badge-info">{{ $status }}</span>
|
||||
@else
|
||||
<span class="badge badge-info">{{ $status }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($patient->phone)
|
||||
<a href="tel:{{ $patient->phone }}" style="color: var(--color-primary); text-decoration: none;">
|
||||
{{ $patient->phone }}
|
||||
</a>
|
||||
@else
|
||||
<span style="color: var(--color-text-muted);">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($patient->remark)
|
||||
<span style="color: var(--color-warning); font-weight: 500;">{{ $patient->remark }}</span>
|
||||
@else
|
||||
<span style="color: var(--color-text-muted);">-</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
@if(!$patient->isCompleted())
|
||||
<form action="{{ route('patients.follow-up', $patient) }}" method="POST" style="display: inline;">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-outline btn-sm" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $patient->getNextFollowUpNumber() }}次随访?')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
随访
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ route('patients.destroy', $patient) }}" method="POST" style="display: inline;">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-outline btn-sm" style="color: var(--color-danger);" onclick="return confirm('确认删除患者 {{ $patient->name }}?此操作不可恢复。')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 移动端卡片列表 -->
|
||||
<div class="mobile-patient-list">
|
||||
@foreach($patients as $patient)
|
||||
@php
|
||||
$total = count($patient->getFollowUpSchedule());
|
||||
$completed = $patient->follow_up_count;
|
||||
$status = $patient->getFollowUpStatus();
|
||||
@endphp
|
||||
<div class="mobile-patient-card">
|
||||
<div class="mobile-patient-header">
|
||||
<div class="mobile-patient-info">
|
||||
<div class="mobile-patient-avatar {{ $patient->gender == '女' ? 'female' : 'male' }}">
|
||||
{{ mb_substr($patient->name, 0, 1) }}
|
||||
</div>
|
||||
<div>
|
||||
<div class="mobile-patient-name">{{ $patient->name }}</div>
|
||||
<div class="mobile-patient-meta">{{ $patient->gender }} · {{ $patient->age }}岁</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($patient->isCompleted())
|
||||
<span class="badge badge-success">已完成</span>
|
||||
@elseif(str_contains($status, '过期'))
|
||||
<span class="badge badge-danger">{{ $status }}</span>
|
||||
@elseif(str_contains($status, '今日'))
|
||||
<span class="badge badge-warning">{{ $status }}</span>
|
||||
@else
|
||||
<span class="badge badge-info">{{ $status }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mobile-patient-body">
|
||||
<div class="mobile-info-item">
|
||||
<span class="mobile-info-label">诊断</span>
|
||||
<span class="mobile-info-value">{{ $patient->getDiagnosisType() }}</span>
|
||||
</div>
|
||||
<div class="mobile-info-item">
|
||||
<span class="mobile-info-label">转诊日期</span>
|
||||
<span class="mobile-info-value">{{ $patient->discharge_date->format('Y-m-d') }}</span>
|
||||
</div>
|
||||
<div class="mobile-info-item">
|
||||
<span class="mobile-info-label">联系方式</span>
|
||||
<span class="mobile-info-value">
|
||||
@if($patient->phone)
|
||||
<a href="tel:{{ $patient->phone }}">{{ $patient->phone }}</a>
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
<div class="mobile-info-item">
|
||||
<span class="mobile-info-label">地址</span>
|
||||
<span class="mobile-info-value">{{ $patient->address ?: '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($patient->remark)
|
||||
<div class="mobile-patient-remark">
|
||||
📝 {{ $patient->remark }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mobile-patient-progress">
|
||||
<div class="mobile-progress-header">
|
||||
<span class="mobile-progress-label">随访进度</span>
|
||||
<span class="mobile-progress-value">{{ $completed }}/{{ $total }}</span>
|
||||
</div>
|
||||
<div class="mobile-progress-bar">
|
||||
<div class="mobile-progress-fill" style="width: {{ ($completed / $total) * 100 }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-patient-actions">
|
||||
@if($patient->phone)
|
||||
<a href="tel:{{ $patient->phone }}" class="btn btn-outline btn-sm">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
拨打电话
|
||||
</a>
|
||||
@endif
|
||||
@if(!$patient->isCompleted())
|
||||
<form action="{{ route('patients.follow-up', $patient) }}" method="POST">
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-primary btn-sm" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $patient->getNextFollowUpNumber() }}次随访?')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
已随访
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
<form action="{{ route('patients.destroy', $patient) }}" method="POST">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-outline btn-sm" style="color: var(--color-danger); border-color: var(--color-danger);" onclick="return confirm('确认删除患者 {{ $patient->name }}?')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
@if($patients->hasPages())
|
||||
<div class="custom-pagination">
|
||||
<div class="pagination-info">
|
||||
显示 {{ $patients->firstItem() }} - {{ $patients->lastItem() }} 条,共 {{ $patients->total() }} 条
|
||||
</div>
|
||||
<div class="pagination-links">
|
||||
{{-- 上一页 --}}
|
||||
@if($patients->onFirstPage())
|
||||
<span class="pagination-btn disabled">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
</span>
|
||||
@else
|
||||
<a href="{{ $patients->previousPageUrl() }}" class="pagination-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
</a>
|
||||
@endif
|
||||
|
||||
{{-- 页码 --}}
|
||||
@foreach($patients->getUrlRange(1, $patients->lastPage()) as $page => $url)
|
||||
@if($page == $patients->currentPage())
|
||||
<span class="pagination-btn active">{{ $page }}</span>
|
||||
@else
|
||||
<a href="{{ $url }}" class="pagination-btn">{{ $page }}</a>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
{{-- 下一页 --}}
|
||||
@if($patients->hasMorePages())
|
||||
<a href="{{ $patients->nextPageUrl() }}" class="pagination-btn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</a>
|
||||
@else
|
||||
<span class="pagination-btn disabled">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
||||
<polyline points="9 18 15 12 9 6"/>
|
||||
</svg>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endsection
|
||||
172
resources/views/patients/reminders.blade.php
Normal file
172
resources/views/patients/reminders.blade.php
Normal file
@ -0,0 +1,172 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '随访提醒 - 病例回访提醒系统')
|
||||
|
||||
@section('content')
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">随访提醒</h1>
|
||||
<p class="page-subtitle">查看需要随访的患者,及时完成回访工作</p>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-grid">
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'all']) }}" class="stat-card {{ $filter === 'all' ? 'active' : '' }}">
|
||||
<div class="stat-value">{{ $stats['total'] }}</div>
|
||||
<div class="stat-label">总患者数</div>
|
||||
</a>
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'overdue']) }}" class="stat-card danger {{ $filter === 'overdue' ? 'active' : '' }}">
|
||||
<div class="stat-value">{{ $stats['overdue'] }}</div>
|
||||
<div class="stat-label">已过期</div>
|
||||
</a>
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'today']) }}" class="stat-card warning {{ $filter === 'today' ? 'active' : '' }}">
|
||||
<div class="stat-value">{{ $stats['today'] }}</div>
|
||||
<div class="stat-label">今日到期</div>
|
||||
</a>
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'upcoming']) }}" class="stat-card info {{ $filter === 'upcoming' ? 'active' : '' }}">
|
||||
<div class="stat-value">{{ $stats['upcoming'] }}</div>
|
||||
<div class="stat-label">7天内到期</div>
|
||||
</a>
|
||||
<a href="#" class="stat-card success">
|
||||
<div class="stat-value">{{ $stats['completed'] }}</div>
|
||||
<div class="stat-label">已完成全部随访</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- 筛选标签 -->
|
||||
<div class="filter-tabs">
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'all']) }}" class="filter-tab {{ $filter === 'all' ? 'active' : '' }}">全部</a>
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'overdue']) }}" class="filter-tab {{ $filter === 'overdue' ? 'active' : '' }}">已过期</a>
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'today']) }}" class="filter-tab {{ $filter === 'today' ? 'active' : '' }}">今日到期</a>
|
||||
<a href="{{ route('patients.reminders', ['filter' => 'upcoming']) }}" class="filter-tab {{ $filter === 'upcoming' ? 'active' : '' }}">即将到期</a>
|
||||
</div>
|
||||
|
||||
<!-- 导出按钮 -->
|
||||
<div style="margin-bottom: 24px;">
|
||||
<a href="{{ route('patients.export', ['filter' => $filter]) }}" class="btn btn-outline">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>
|
||||
导出当前列表
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@if($reminders->isEmpty())
|
||||
<div class="card">
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2"/>
|
||||
<line x1="9" y1="9" x2="9.01" y2="9"/>
|
||||
<line x1="15" y1="9" x2="15.01" y2="9"/>
|
||||
</svg>
|
||||
<div class="empty-state-title">暂无需要随访的患者</div>
|
||||
<p>当前筛选条件下没有需要随访的患者,您可以切换筛选条件或导入新的患者数据。</p>
|
||||
<div style="margin-top: 20px;">
|
||||
<a href="{{ route('patients.import') }}" class="btn btn-primary">导入患者数据</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<!-- 提醒卡片列表 - 两列布局 -->
|
||||
<div class="reminder-grid">
|
||||
@foreach($reminders as $reminder)
|
||||
@php
|
||||
$patient = $reminder['patient'];
|
||||
$nextDate = $reminder['next_follow_up_date'];
|
||||
$daysUntil = $reminder['days_until'];
|
||||
@endphp
|
||||
<div class="reminder-card fade-in">
|
||||
{{-- 卡片头部 --}}
|
||||
<div class="reminder-card-header">
|
||||
<div class="reminder-patient-info">
|
||||
<div class="reminder-avatar">{{ mb_substr($patient->name, 0, 1) }}</div>
|
||||
<div>
|
||||
<div class="reminder-name">{{ $patient->name }}</div>
|
||||
<div class="reminder-meta">{{ $patient->gender }} · {{ $patient->age }}岁</div>
|
||||
</div>
|
||||
</div>
|
||||
@if($daysUntil !== null && $daysUntil < 0)
|
||||
<span class="badge badge-danger">过期 {{ abs($daysUntil) }} 天</span>
|
||||
@elseif($daysUntil === 0)
|
||||
<span class="badge badge-warning">今日到期</span>
|
||||
@elseif($daysUntil !== null && $daysUntil <= 7)
|
||||
<span class="badge badge-info">{{ $daysUntil }} 天后</span>
|
||||
@else
|
||||
<span class="badge badge-success">未到期</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 诊断标签 --}}
|
||||
<div class="reminder-diagnosis">
|
||||
<span class="diagnosis-tag">{{ $patient->getDiagnosisType() }}</span>
|
||||
<span class="follow-up-tag">第{{ $reminder['next_follow_up_number'] }}次随访</span>
|
||||
</div>
|
||||
|
||||
{{-- 关键信息 --}}
|
||||
<div class="reminder-details">
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">📅</span>
|
||||
<span class="detail-label">随访日期</span>
|
||||
<span class="detail-value">{{ $nextDate ? $nextDate->format('Y-m-d') : '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">📞</span>
|
||||
<span class="detail-label">联系方式</span>
|
||||
<span class="detail-value">{{ $patient->phone ?: '-' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">📍</span>
|
||||
<span class="detail-label">地址</span>
|
||||
<span class="detail-value text-truncate">{{ $patient->address ?: '-' }}</span>
|
||||
</div>
|
||||
@if($patient->remark)
|
||||
<div class="detail-row">
|
||||
<span class="detail-icon">📝</span>
|
||||
<span class="detail-label">备注</span>
|
||||
<span class="detail-value remark-text">{{ $patient->remark }}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 进度条 --}}
|
||||
<div class="reminder-progress">
|
||||
@php
|
||||
$total = count($patient->getFollowUpSchedule());
|
||||
$completed = $patient->follow_up_count;
|
||||
$percent = ($completed / $total) * 100;
|
||||
@endphp
|
||||
<div class="progress-header">
|
||||
<span>随访进度</span>
|
||||
<span>{{ $completed }}/{{ $total }}</span>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" style="width: {{ $percent }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 操作按钮 --}}
|
||||
<div class="reminder-actions">
|
||||
@if($patient->phone)
|
||||
<a href="tel:{{ $patient->phone }}" class="action-btn action-call" title="拨打电话">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
|
||||
</svg>
|
||||
</a>
|
||||
@endif
|
||||
<form action="{{ route('patients.follow-up', $patient) }}" method="POST" class="action-form">
|
||||
@csrf
|
||||
<button type="submit" class="action-btn action-complete" title="标记已随访" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $reminder['next_follow_up_number'] }}次随访?')">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
<span>已随访</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endsection
|
||||
277
resources/views/welcome.blade.php
Normal file
277
resources/views/welcome.blade.php
Normal file
File diff suppressed because one or more lines are too long
8
routes/console.php
Normal file
8
routes/console.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
46
routes/web.php
Normal file
46
routes/web.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\PatientController;
|
||||
use App\Http\Controllers\AuthController;
|
||||
|
||||
// 认证路由(未登录可访问)
|
||||
Route::middleware('guest')->group(function () {
|
||||
Route::get('/login', [AuthController::class, 'showLogin'])->name('login');
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
Route::get('/register', [AuthController::class, 'showRegister'])->name('register');
|
||||
Route::post('/register', [AuthController::class, 'register']);
|
||||
});
|
||||
|
||||
// 登出路由
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout')->middleware('auth');
|
||||
|
||||
// 首页重定向
|
||||
Route::get('/', function () {
|
||||
return redirect()->route('patients.reminders');
|
||||
});
|
||||
|
||||
// 患者管理路由(需要登录)
|
||||
Route::prefix('patients')->name('patients.')->middleware('auth')->group(function () {
|
||||
// 列表
|
||||
Route::get('/', [PatientController::class, 'index'])->name('index');
|
||||
|
||||
// 随访提醒
|
||||
Route::get('/reminders', [PatientController::class, 'reminders'])->name('reminders');
|
||||
|
||||
// 导入
|
||||
Route::get('/import', [PatientController::class, 'showImport'])->name('import');
|
||||
Route::post('/import', [PatientController::class, 'import'])->name('import.store');
|
||||
|
||||
// 下载模板
|
||||
Route::get('/template', [PatientController::class, 'downloadTemplate'])->name('template');
|
||||
|
||||
// 导出
|
||||
Route::get('/export', [PatientController::class, 'export'])->name('export');
|
||||
|
||||
// 标记已随访
|
||||
Route::post('/{patient}/follow-up', [PatientController::class, 'markFollowedUp'])->name('follow-up');
|
||||
|
||||
// 删除
|
||||
Route::delete('/{patient}', [PatientController::class, 'destroy'])->name('destroy');
|
||||
});
|
||||
4
storage/app/.gitignore
vendored
Normal file
4
storage/app/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
*
|
||||
!private/
|
||||
!public/
|
||||
!.gitignore
|
||||
2
storage/app/private/.gitignore
vendored
Normal file
2
storage/app/private/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/app/public/.gitignore
vendored
Normal file
2
storage/app/public/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
9
storage/framework/.gitignore
vendored
Normal file
9
storage/framework/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
compiled.php
|
||||
config.php
|
||||
down
|
||||
events.scanned.php
|
||||
maintenance.php
|
||||
routes.php
|
||||
routes.scanned.php
|
||||
schedule-*
|
||||
services.json
|
||||
3
storage/framework/cache/.gitignore
vendored
Normal file
3
storage/framework/cache/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*
|
||||
!data/
|
||||
!.gitignore
|
||||
2
storage/framework/cache/data/.gitignore
vendored
Normal file
2
storage/framework/cache/data/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/framework/sessions/.gitignore
vendored
Normal file
2
storage/framework/sessions/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/framework/testing/.gitignore
vendored
Normal file
2
storage/framework/testing/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/framework/views/.gitignore
vendored
Normal file
2
storage/framework/views/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
2
storage/logs/.gitignore
vendored
Normal file
2
storage/logs/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
19
tests/Feature/ExampleTest.php
Normal file
19
tests/Feature/ExampleTest.php
Normal file
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
// use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_the_application_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get('/');
|
||||
|
||||
$response->assertStatus(200);
|
||||
}
|
||||
}
|
||||
10
tests/TestCase.php
Normal file
10
tests/TestCase.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
16
tests/Unit/ExampleTest.php
Normal file
16
tests/Unit/ExampleTest.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
18
vite.config.js
Normal file
18
vite.config.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import laravel from 'laravel-vite-plugin';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
laravel({
|
||||
input: ['resources/css/app.css', 'resources/js/app.js'],
|
||||
refresh: true,
|
||||
}),
|
||||
tailwindcss(),
|
||||
],
|
||||
server: {
|
||||
watch: {
|
||||
ignored: ['**/storage/framework/views/**'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user