html: embed_local_images: false embed_svg: true offline: false toc: true
print_background: false export_on_save:
-- 技师表
CREATE TABLE technicians (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
status TINYINT DEFAULT 1 COMMENT '1:正常,0:停用',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
-- 技师工作规则表
CREATE TABLE technician_work_rules (
id INT PRIMARY KEY AUTO_INCREMENT,
technician_id INT NOT NULL,
work_days VARCHAR(50) NOT NULL COMMENT '工作日 例如:1,2,3,4,5 表示周一到周五',
start_time TIME NOT NULL DEFAULT '08:00' COMMENT '默认上班时间',
end_time TIME NOT NULL DEFAULT '18:00' COMMENT '默认下班时间',
is_active TINYINT DEFAULT 1 COMMENT '是否启用',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
UNIQUE KEY idx_technician (technician_id)
);
-- 技师特殊日期表(休假或特殊工作日)
CREATE TABLE technician_special_dates (
id INT PRIMARY KEY AUTO_INCREMENT,
technician_id INT NOT NULL,
date DATE NOT NULL,
is_working TINYINT DEFAULT 0 COMMENT '1:特殊工作日,0:休息日',
start_time TIME NULL COMMENT '特殊工作日的上班时间',
end_time TIME NULL COMMENT '特殊工作日的下班时间',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
UNIQUE KEY idx_technician_date (technician_id, date)
);
-- 技师时间槽表
CREATE TABLE technician_time_slots (
id INT PRIMARY KEY AUTO_INCREMENT,
technician_id INT NOT NULL,
date DATE NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
status TINYINT DEFAULT 1 COMMENT '1:可预约,0:已预约,2:预留间隔',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
INDEX idx_technician_date (technician_id, date, status)
);
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('technicians', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->tinyInteger('status')->default(1);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('technicians');
}
};
<?php
// app/Models/Technician.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
class Technician extends Model
{
protected $fillable = ['name', 'status'];
public function workRule(): HasOne
{
return $this->hasOne(TechnicianWorkRule::class);
}
public function specialDates(): HasMany
{
return $this->hasMany(TechnicianSpecialDate::class);
}
public function timeSlots(): HasMany
{
return $this->hasMany(TechnicianTimeSlot::class);
}
}
<?php
// app/Models/TechnicianWorkRule.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TechnicianWorkRule extends Model
{
protected $fillable = [
'technician_id',
'work_days',
'start_time',
'end_time',
'is_active'
];
protected $casts = [
'is_active' => 'boolean',
'start_time' => 'datetime',
'end_time' => 'datetime'
];
public function technician(): BelongsTo
{
return $this->belongsTo(Technician::class);
}
public function getWorkDaysArrayAttribute(): array
{
return array_map('intval', explode(',', $this->work_days));
}
}
<?php
// app/Models/TechnicianSpecialDate.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TechnicianSpecialDate extends Model
{
protected $fillable = [
'technician_id',
'date',
'is_working',
'start_time',
'end_time'
];
protected $casts = [
'date' => 'date',
'is_working' => 'boolean',
'start_time' => 'datetime',
'end_time' => 'datetime'
];
public function technician(): BelongsTo
{
return $this->belongsTo(Technician::class);
}
}
<?php
// app/Models/TechnicianTimeSlot.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TechnicianTimeSlot extends Model
{
protected $fillable = [
'technician_id',
'date',
'start_time',
'end_time',
'status'
];
protected $casts = [
'date' => 'date',
'start_time' => 'datetime',
'end_time' => 'datetime'
];
public function technician(): BelongsTo
{
return $this->belongsTo(Technician::class);
}
}
class TechnicianSchedule extends Model
{
protected $casts = [
'work_time' => 'array',
'work_date' => 'date',
];
public function technician()
{
return $this->belongsTo(Technician::class);
}
public function workPlans()
{
return $this->hasMany(TechnicianWorkPlan::class, 'technician_id', 'technician_id')
->where('work_date', $this->work_date);
}
}
<?php
// app/Services/TechnicianScheduleService.php
namespace App\Services;
use App\Models\TechnicianWorkRule;
use App\Models\TechnicianSpecialDate;
use App\Models\TechnicianTimeSlot;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use DB;
use Exception;
class TechnicianScheduleService
{
/**
* 设置技师工作规则
*/
public function setWorkRule(
int $technicianId,
array $workDays,
string $startTime = '08:00',
string $endTime = '18:00'
): void {
try {
DB::beginTransaction();
TechnicianWorkRule::updateOrCreate(
['technician_id' => $technicianId],
[
'work_days' => implode(',', $workDays),
'start_time' => $startTime,
'end_time' => $endTime,
'is_active' => true
]
);
// 生成未来30天的时间槽
$this->generateFutureTimeSlots($technicianId);
DB::commit();
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* 生成时间槽
*/
public function generateTimeSlots(int $technicianId, string $date): void
{
$dateObj = Carbon::parse($date);
$dayOfWeek = $dateObj->dayOfWeek;
// 获取工作规则
$workRule = TechnicianWorkRule::where('technician_id', $technicianId)
->where('is_active', true)
->first();
// 获取特殊日期设置
$specialDate = TechnicianSpecialDate::where('technician_id', $technicianId)
->where('date', $date)
->first();
// 判断是否工作日
$isWorkDay = false;
$startTime = null;
$endTime = null;
if ($specialDate) {
// 特殊日期优先
$isWorkDay = $specialDate->is_working;
$startTime = $specialDate->start_time;
$endTime = $specialDate->end_time;
} elseif ($workRule && in_array($dayOfWeek, $workRule->work_days_array)) {
// 普通工作日
$isWorkDay = true;
$startTime = $workRule->start_time;
$endTime = $workRule->end_time;
}
if (!$isWorkDay) {
return;
}
try {
DB::beginTransaction();
// 删除原有时间槽
TechnicianTimeSlot::where('technician_id', $technicianId)
->where('date', $date)
->delete();
// 生成30分钟时间槽
$currentTime = Carbon::parse($startTime);
$endDateTime = Carbon::parse($endTime);
while ($currentTime->copy()->addMinutes(30) <= $endDateTime) {
TechnicianTimeSlot::create([
'technician_id' => $technicianId,
'date' => $date,
'start_time' => $currentTime->format('H:i:s'),
'end_time' => $currentTime->copy()->addMinutes(30)->format('H:i:s'),
'status' => 1
]);
$currentTime->addMinutes(30);
}
DB::commit();
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
}
}
<?php
// app/Http/Controllers/Api/TechnicianScheduleController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\TechnicianScheduleService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Exception;
class TechnicianScheduleController extends Controller
{
private TechnicianScheduleService $scheduleService;
public function __construct(TechnicianScheduleService $scheduleService)
{
$this->scheduleService = $scheduleService;
}
/**
* 设置工作规则
*/
public function setWorkRule(Request $request): JsonResponse
{
$request->validate([
'technician_id' => 'required|exists:technicians,id',
'work_days' => 'required|array',
'work_days.*' => 'integer|between:0,6',
'start_time' => 'required|date_format:H:i',
'end_time' => 'required|date_format:H:i|after:start_time'
]);
try {
$this->scheduleService->setWorkRule(
$request->technician_id,
$request->work_days,
$request->start_time,
$request->end_time
);
return response()->json([
'message' => '工作规则设置成功'
]);
} catch (Exception $e) {
return response()->json([
'message' => '工作规则设置失败:' . $e->getMessage()
], 500);
}
}
/**
* 设置特殊日期
*/
public function setSpecialDate(Request $request): JsonResponse
{
$request->validate([
'technician_id' => 'required|exists:technicians,id',
'date' => 'required|date|after:today',
'is_working' => 'required|boolean',
'start_time' => 'required_if:is_working,true|date_format:H:i|nullable',
'end_time' => 'required_if:is_working,true|date_format:H:i|after:start_time|nullable'
]);
try {
$this->scheduleService->setSpecialDate(
$request->technician_id,
$request->date,
$request->is_working,
$request->start_time,
$request->end_time
);
return response()->json([
'message' => '特殊日期设置成功'
]);
} catch (Exception $e) {
return response()->json([
'message' => '特殊日期设置失败:' . $e->getMessage()
], 500);
}
}
/**
* 获取技师可预约时间段
* @param Request $request
* @return JsonResponse
*/
public function getAvailableTimeSlots(Request $request): JsonResponse
{
$request->validate([
'technician_id' => 'required|exists:technicians,id',
'date' => 'required|date|after_or_equal:today',
'service_duration' => 'required|integer|min:30', // 服务时长(分钟)
]);
try {
// 获取技师当天排班
$schedule = TechnicianSchedule::where('technician_id', $request->technician_id)
->where('work_date', $request->date)
->first();
if (!$schedule) {
return response()->json([
'message' => '技师当天未排班',
'data' => []
]);
}
// 获取已预约的时间段
$bookedTimeSlots = TechnicianWorkPlan::where('technician_id', $request->technician_id)
->where('date', $request->date)
->where('status', '!=', 'cancelled')
->get(['plan_start_time', 'plan_end_time'])
->toArray();
// 计算可用时间段
$availableTimeSlots = $this->calculateAvailableTimeSlots(
$schedule->work_time,
$bookedTimeSlots,
$request->service_duration
);
return response()->json([
'message' => 'success',
'data' => $availableTimeSlots
]);
} catch (\Exception $e) {
return response()->json([
'message' => $e->getMessage(),
'data' => []
], 500);
}
}
/**
* 计算可用时间段
* @param array $workTime 工作时间 [{"start":"09:00","end":"18:00"}]
* @param array $bookedTimeSlots 已预约时间段
* @param int $serviceDuration 服务时长(分钟)
* @return array
*/
private function calculateAvailableTimeSlots(array $workTime, array $bookedTimeSlots, int $serviceDuration): array
{
$availableSlots = [];
foreach ($workTime as $period) {
$startTime = strtotime($period['start']);
$endTime = strtotime($period['end']);
// 按30分钟间隔切分时间段
$currentSlot = $startTime;
while ($currentSlot + ($serviceDuration * 60) <= $endTime) {
$slotEnd = $currentSlot + ($serviceDuration * 60);
// 检查是否与已预约时间段冲突
$isAvailable = true;
foreach ($bookedTimeSlots as $bookedSlot) {
$bookedStart = strtotime($bookedSlot['plan_start_time']);
$bookedEnd = strtotime($bookedSlot['plan_end_time']);
if ($currentSlot < $bookedEnd && $slotEnd > $bookedStart) {
$isAvailable = false;
break;
}
}
if ($isAvailable) {
$availableSlots[] = [
'start_time' => date('H:i', $currentSlot),
'end_time' => date('H:i', $slotEnd)
];
}
$currentSlot += 1800; // 30分钟
}
}
return $availableSlots;
}
}
// routes/api.php
Route::prefix('technician')->group(function () {
// 技师工作规则设置
Route::post('work-rule', [TechnicianScheduleController::class, 'setWorkRule']);
// 特殊日期设置
Route::post('special-date', [TechnicianScheduleController::class, 'setSpecialDate']);
// 获取可用时间槽
Route::get('time-slots', [TechnicianScheduleController::class, 'getAvailableTimeSlots']);
// 预约时间槽
Route::post('book', [TechnicianScheduleController::class, 'bookTimeSlot']);
});
<!-- pages/technician/schedule/index.vue -->
<template>
<view class="schedule-setting">
<!-- 工作日设置 -->
<uni-section title="工作日设置" type="line">
<uni-list>
<uni-list-item v-for="day in weekDays" :key="day.value"
:title="day.label"
:switchChecked="workDays.includes(day.value)"
@switch-change="(e) => toggleWorkDay(day.value, e.value)"
showSwitch
/>
</uni-list>
</uni-section>
<!-- 工作时间设置 -->
<uni-section title="工作时间设置" type="line">
<uni-list>
<uni-list-item title="上班时间" showArrow @click="showStartTimePicker = true">
<template v-slot:footer>
<text>{{ startTime }}</text>
</template>
</uni-list-item>
<uni-list-item title="下班时间" showArrow @click="showEndTimePicker = true">
<template v-slot:footer>
<text>{{ endTime }}</text>
</template>
</uni-list-item>
</uni-list>
</uni-section>
<!-- 保存按钮 -->
<view class="button-group">
<button class="primary-button" @click="saveWorkRule">保存设置</button>
</view>
<!-- 特殊日期设置 -->
<uni-section title="特殊日期设置" type="line">
<button class="secondary-button" @click="showCalendar = true">
设置休息日/特殊工作日
</button>
</uni-section>
<!-- 时间选择器 -->
<uni-datetime-picker
v-model="showStartTimePicker"
type="time"
:value="startTime"
@confirm="onStartTimeChange"
/>
<uni-datetime-picker
v-model="showEndTimePicker"
type="time"
:value="endTime"
@confirm="onEndTimeChange"
/>
<!-- 日历选择器 -->
<uni-calendar
v-model="showCalendar"
:insert="false"
@confirm="onSelectSpecialDate"
:start-date="startDate"
:end-date="endDate"
/>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
// 工作日配置
const weekDays = [
{ label: '周一', value: 1 },
{ label: '周二', value: 2 },
{ label: '周三', value: 3 },
{ label: '周四', value: 4 },
{ label: '周五', value: 5 },
{ label: '周六', value: 6 },
{ label: '周日', value: 0 }
]
// 响应式数据
const workDays = ref([1, 2, 3, 4, 5])
const startTime = ref('08:00')
const endTime = ref('18:00')
const showStartTimePicker = ref(false)
const showEndTimePicker = ref(false)
const showCalendar = ref(false)
// 日期范围
const startDate = new Date()
const endDate = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000)
// 获取技师当前设置
const getCurrentSettings = async () => {
try {
const res = await uni.request({
url: '/api/technician/work-rule',
method: 'GET',
data: {
technician_id: getApp().globalData.technicianId
}
})
if (res.statusCode === 200) {
const { work_days, start_time, end_time } = res.data.data
workDays.value = work_days.split(',').map(Number)
startTime.value = start_time
endTime.value = end_time
}
} catch (error) {
uni.showToast({
title: '获取设置失败',
icon: 'none'
})
}
}
// 切换工作日
const toggleWorkDay = (day, checked) => {
if (checked) {
workDays.value.push(day)
} else {
const index = workDays.value.indexOf(day)
if (index > -1) {
workDays.value.splice(index, 1)
}
}
}
// 时间选择回调
const onStartTimeChange = (time) => {
startTime.value = time
}
const onEndTimeChange = (time) => {
endTime.value = time
}
// 保存工作规则
const saveWorkRule = async () => {
try {
const res = await uni.request({
url: '/api/technician/work-rule',
method: 'POST',
data: {
technician_id: getApp().globalData.technicianId,
work_days: workDays.value,
start_time: startTime.value,
end_time: endTime.value
}
})
if (res.statusCode === 200) {
uni.showToast({
title: '设置成功',
icon: 'success'
})
} else {
throw new Error(res.data.message)
}
} catch (error) {
uni.showToast({
title: error.message || '设置失败',
icon: 'none'
})
}
}
// 页面加载时获取当前设置
onLoad(() => {
getCurrentSettings()
})
</script>
<style lang="scss">
.schedule-setting {
padding: 20rpx;
.button-group {
margin: 30rpx 0;
}
.primary-button {
background-color: #007AFF;
color: #fff;
border-radius: 10rpx;
padding: 20rpx;
text-align: center;
}
.secondary-button {
background-color: #F8F8F8;
color: #333;
border: 1px solid #DDDDDD;
border-radius: 10rpx;
padding: 20rpx;
text-align: center;
margin-top: 20rpx;
}
}
</style>
<!-- pages/technician/schedule/special-date.vue -->
<template>
<view class="special-date">
<uni-forms ref="form" :modelValue="formData">
<uni-forms-item label="日期">
<text>{{ date }}</text>
</uni-forms-item>
<uni-forms-item label="是否工作">
<switch :checked="formData.is_working" @change="onWorkingChange" />
</uni-forms-item>
<template v-if="formData.is_working">
<uni-forms-item label="上班时间">
<view class="time-picker" @click="showStartTimePicker = true">
{{ formData.start_time || '请选择' }}
</view>
</uni-forms-item>
<uni-forms-item label="下班时间">
<view class="time-picker" @click="showEndTimePicker = true">
{{ formData.end_time || '请选择' }}
</view>
</uni-forms-item>
</template>
</uni-forms>
<view class="button-group">
<button class="primary-button" @click="saveSpecialDate">保存设置</button>
</view>
<!-- 时间选择器 -->
<uni-datetime-picker
v-model="showStartTimePicker"
type="time"
:value="formData.start_time"
@confirm="onStartTimeChange"
/>
<uni-datetime-picker
v-model="showEndTimePicker"
type="time"
:value="formData.end_time"
@confirm="onEndTimeChange"
/>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const date = ref('')
const formData = ref({
is_working: false,
start_time: '',
end_time: ''
})
const showStartTimePicker = ref(false)
const showEndTimePicker = ref(false)
// 获取特殊日期设置
const getSpecialDateSettings = async (date) => {
try {
const res = await uni.request({
url: '/api/technician/special-date',
method: 'GET',
data: {
technician_id: getApp().globalData.technicianId,
date: date
}
})
if (res.statusCode === 200) {
formData.value = res.data.data || {
is_working: false,
start_time: '',
end_time: ''
}
}
} catch (error) {
uni.showToast({
title: '获取设置失败',
icon: 'none'
})
}
}
// 工作状态切换
const onWorkingChange = (e) => {
formData.value.is_working = e.detail.value
}
// 时间选择回调
const onStartTimeChange = (time) => {
formData.value.start_time = time
}
const onEndTimeChange = (time) => {
formData.value.end_time = time
}
// 保存特殊日期设置
const saveSpecialDate = async () => {
try {
const res = await uni.request({
url: '/api/technician/special-date',
method: 'POST',
data: {
technician_id: getApp().globalData.technicianId,
date: date.value,
...formData.value
}
})
if (res.statusCode === 200) {
uni.showToast({
title: '设置成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} else {
throw new Error(res.data.message)
}
} catch (error) {
uni.showToast({
title: error.message || '设置失败',
icon: 'none'
})
}
}
// 页面加载
onLoad((options) => {
date.value = options.date
getSpecialDateSettings(options.date)
})
</script>
<style lang="scss">
.special-date {
padding: 20rpx;
.time-picker {
border: 1px solid #DDDDDD;
padding: 20rpx;
border-radius: 10rpx;
}
.button-group {
margin: 30rpx 0;
}
.primary-button {
background-color: #007AFF;
color: #fff;
border-radius: 10rpx;
padding: 20rpx;
text-align: center;
}
}
</style>
<template>
<div>
<van-datetime-picker
v-model="selectedDate"
type="date"
:min-date="minDate"
:max-date="maxDate"
@confirm="onDateSelected"
/>
<van-picker
v-if="timeSlots.length"
:columns="timeSlots"
@confirm="onTimeSelected"
/>
</div>
</template>
<script setup>
import { ref } from 'vue'
import axios from 'axios'
import { showToast } from 'vant'
const selectedDate = ref(new Date())
const minDate = new Date()
const maxDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
const timeSlots = ref([])
const getAvailableTimeSlots = async (date) => {
try {
const response = await axios.get('/api/technician/available-time-slots', {
params: {
technician_id: 1, // 技师ID
date: date,
service_duration: 60 // 服务时长(分钟)
}
})
timeSlots.value = response.data.data.map(slot =>
`${slot.start_time}-${slot.end_time}`
)
} catch (error) {
showToast(error.response?.data?.message || '获取可用时间失败')
}
}
const onDateSelected = (value) => {
const formatDate = value.toISOString().split('T')[0]
getAvailableTimeSlots(formatDate)
}
const onTimeSelected = (value) => {
// 处理时间段选择
console.log('Selected time slot:', value)
}
</script>
// utils/request.ts
import { baseURL } from '@/config'
interface RequestOptions extends UniApp.RequestOptions {
loading?: boolean;
}
const request = (options: RequestOptions) => {
const { loading = true } = options
if (loading) {
uni.showLoading({
title: '加载中'
})
}
return new Promise((resolve, reject) => {
uni.request({
...options,
url: baseURL + options.url,
header: {
...options.header,
'Authorization': `Bearer ${uni.getStorageSync('token')}`
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data)
} else {
reject(res.data)
}
},
fail: (err) => {
reject(err)
},
complete: () => {
if (loading) {
uni.hideLoading()
}
}
})
})
}
export default request
// api/technician.ts
import request from '@/utils/request'
// 获取工作规则
export const getWorkRule = (technicianId: number) => {
return request({
url: '/technician/work-rule',
method: 'GET',
data: { technician_id: technicianId }
})
}
// 设置工作规则
export const setWorkRule = (data: {
technician_id: number;
work_days: number[];
start_time: string;
end_time: string;
}) => {
return request({
url: '/technician/work-rule',
method: 'POST',
data
})
}
// 获取特殊日期设置
export const getSpecialDate = (technicianId: number, date: string) => {
return request({
url: '/technician/special-date',
method: 'GET',
data: {
technician_id: technicianId,
date
}
})
}
// 设置特殊日期
export const setSpecialDate = (data: {
technician_id: number;
date: string;
is_working: boolean;
start_time?: string;
end_time?: string;
}) => {
return request({
url: '/technician/special-date',
method: 'POST',
data
})
}
// 获取可用时间槽
export const getTimeSlots = (technicianId: number, date: string) => {
return request({
url: '/technician/time-slots',
method: 'GET',
data: {
technician_id: technicianId,
date
}
})
}
// pages.json
{
"pages": [
{
"path": "pages/technician/schedule/index",
"style": {
"navigationBarTitleText": "排班设置"
}
},
{
"path": "pages/technician/schedule/special-date",
"style": {
"navigationBarTitleText": "特殊日期设置"
}
}
]
}