first commit

This commit is contained in:
chenhaizhao 2026-01-15 15:05:29 +08:00
commit 0ec405e137
66 changed files with 8572 additions and 0 deletions

31
.eslintrc.js Normal file
View File

@ -0,0 +1,31 @@
/*
* Eslint config file
* Documentation: https://eslint.org/docs/user-guide/configuring/
* Install the Eslint extension before using this feature.
*/
module.exports = {
env: {
es6: true,
browser: true,
node: true,
},
ecmaFeatures: {
modules: true,
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
},
globals: {
wx: true,
App: true,
Page: true,
getCurrentPages: true,
getApp: true,
Component: true,
requirePlugin: true,
requireMiniProgram: true,
},
// extends: 'eslint:recommended',
rules: {},
}

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
miniprogram_npm

80
app.js Normal file
View File

@ -0,0 +1,80 @@
// app.js
const { userApi } = require("./utils/api");
const AuthUtil = require("./utils/auth");
App({
globalData: {
userInfo: null,
isLoggedIn: false,
},
onLaunch() {
// 检查小程序版本更新
this.checkForUpdate();
// 检查并验证token
this.checkToken();
},
// 检查小程序版本更新
checkForUpdate() {
const updateManager = wx.getUpdateManager()
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
console.log(res.hasUpdate)
})
updateManager.onUpdateReady(function () {
wx.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(function () {
// 新版本下载失败
})
},
/**
* 检查token并获取最新用户数据
*/
async checkToken() {
if (!AuthUtil.hasToken()) {
return;
}
try {
const isValid = await AuthUtil.validateUser();
if (isValid) {
const userInfo = AuthUtil.getUserInfo();
// 启动token自动刷新
AuthUtil.startTokenRefresh();
// 检查用户状态
if (userInfo.user_status < 1) {
wx.showToast({
title: "您的账户需要等待管理员审核",
icon: "none",
duration: 2000,
});
// 清除登录状态但不跳转,让用户自己选择
AuthUtil.clearAuth();
}
} else {
// 用户验证失败,已清除登录状态
}
} catch (err) {
// Token验证异常
}
},
});

24
app.json Normal file
View File

@ -0,0 +1,24 @@
{
"pages": ["pages/login/index", "pages/mine/index", "pages/edit-profile/index", "pages/device_status/index", "pages/surgery/index", "pages/surgery/detail/index", "pages/surgery/create/index", "pages/device_maintenance/index", "pages/device_maintenance/create/index", "pages/device_management/index", "pages/device_management_detail/index", "pages/device_subdevices/index"],
"tabBar": {
"list": [
{
"pagePath": "pages/surgery/index",
"text": "手术"
},
{
"pagePath": "pages/device_management/index",
"text": "设备"
},
{
"pagePath": "pages/mine/index",
"text": "我的"
}
]
},
"window": {
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black"
},
"lazyCodeLoading": "requiredComponents"
}

34
package-lock.json generated Normal file
View File

@ -0,0 +1,34 @@
{
"name": "his-weapp",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"tdesign-miniprogram": "^1.8.8"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/tdesign-miniprogram": {
"version": "1.8.8",
"resolved": "https://registry.npmmirror.com/tdesign-miniprogram/-/tdesign-miniprogram-1.8.8.tgz",
"integrity": "sha512-3Ci/2YJYUDjP04MQHanmqPwQsoIcOORcVMO8aGbnMDkMgDNiJi5hZc0z9AwpiHsHjBAiKhWIdUo2MRacgl4vcg==",
"license": "MIT",
"dependencies": {
"dayjs": "^1.10.7",
"tinycolor2": "^1.4.2"
}
},
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==",
"license": "MIT"
}
}
}

6
package.json Normal file
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"dayjs": "^1.11.13",
"tdesign-miniprogram": "^1.8.8"
}
}

View File

@ -0,0 +1,302 @@
// pages/device_maintenance/create/index.js
const { deviceApi, deviceMaintenanceApi, dictionaryApi } = require("../../../utils/api");
const dayjs = require("../../../miniprogram_npm/dayjs/index");
Page({
data: {
loading: true,
submitting: false,
isEditMode: false,
maintenanceId: null,
subDevice: {}, // 子设备信息
maintenanceTypes: [], // 保养类型
typeIndex: -1,
form: {
sub_device_id: "",
maintenance_type: "",
start_time: "",
start_time_display: "",
end_time: "",
end_time_display: "",
notes: "",
},
customType: "", // 自定义类型
showCustomInput: false,
// 选择器可见性
typePickerVisible: false,
startTimePickerVisible: false,
endTimePickerVisible: false,
currentDate: dayjs().format("YYYY-MM-DD HH:mm"),
},
onLoad: function (options) {
const sub_device_id = options.sub_device_id;
const maintenance_id = options.maintenance_id;
this.setData({
isEditMode: !!maintenance_id,
maintenanceId: maintenance_id || null,
"form.sub_device_id": parseInt(sub_device_id),
});
wx.setNavigationBarTitle({
title: this.data.isEditMode ? "编辑保养记录" : "创建保养记录",
});
this.initializePage(sub_device_id, maintenance_id);
},
async initializePage(sub_device_id, maintenance_id) {
try {
await this.loadMaintenanceTypes();
if (this.data.isEditMode) {
await this.loadMaintenanceData(maintenance_id);
} else {
await this.loadSubDeviceInfo(sub_device_id);
this.setData({
"form.start_time": new Date().getTime(),
"form.start_time_display": dayjs().format("YYYY-MM-DD HH:mm"),
});
}
} catch (error) {
this.showMessage("页面加载失败,请返回重试", "error");
} finally {
this.setData({ loading: false });
}
},
async loadMaintenanceTypes() {
try {
const res = await dictionaryApi.getDictionaryByType("maintenance_type");
if (res.code === 200) {
const types = res.data.map(item => ({
value: item.value,
label: item.value,
}));
types.push({ value: "custom", label: "手动输入" });
this.setData({ maintenanceTypes: types });
if (!this.data.isEditMode && types.length > 1) {
this.setData({
typeIndex: 0,
"form.maintenance_type": types[0].value,
});
} else if (types.length === 1) {
// 只有手动输入选项
this.setData({
typeIndex: 0,
showCustomInput: true,
"form.maintenance_type": "",
});
}
}
} catch (error) {
this.setData({
maintenanceTypes: [{ value: "custom", label: "手动输入" }],
typeIndex: 0,
showCustomInput: true,
"form.maintenance_type": "",
});
}
},
async loadSubDeviceInfo(sub_device_id) {
try {
const res = await deviceApi.getSubDeviceById(sub_device_id);
if (res.code === 200 || res.code === 0) {
this.setData({ subDevice: res.data });
} else {
throw new Error('获取子设备信息失败');
}
} catch (error) {
this.showMessage("加载设备信息失败", "error");
this.setData({ subDevice: { name: '加载失败' } });
}
},
async loadMaintenanceData(maintenance_id) {
try {
const res = await deviceMaintenanceApi.getMaintenance(maintenance_id);
if (res.code === 200) {
const maintenance = res.data;
await this.loadSubDeviceInfo(maintenance.sub_device_id);
this.setData({
form: {
sub_device_id: maintenance.sub_device_id,
maintenance_type: maintenance.maintenance_type,
start_time: new Date(maintenance.start_time).getTime(),
start_time_display: dayjs(maintenance.start_time).format("YYYY-MM-DD HH:mm"),
end_time: maintenance.end_time ? new Date(maintenance.end_time).getTime() : "",
end_time_display: maintenance.end_time ? dayjs(maintenance.end_time).format("YYYY-MM-DD HH:mm") : "",
notes: maintenance.notes || "",
},
});
} else {
throw new Error('获取保养记录失败');
}
} catch (error) {
this.showMessage("加载保养记录失败", "error");
}
},
showTypePicker() {
this.setData({ typePickerVisible: true });
},
showStartTimePicker() {
this.setData({ startTimePickerVisible: true });
},
showEndTimePicker() {
this.setData({ endTimePickerVisible: true });
},
onTypeConfirm(e) {
const selectedValue = e.detail.value[0];
const selectedIndex = this.data.maintenanceTypes.findIndex(type => type.value === selectedValue);
const selectedType = this.data.maintenanceTypes[selectedIndex];
if (!selectedType) {
this.showMessage("选择的保养类型无效", "error");
return;
}
if (selectedType.value === "custom") {
this.setData({
typeIndex: selectedIndex,
showCustomInput: true,
"form.maintenance_type": "",
typePickerVisible: false,
});
} else {
this.setData({
typeIndex: selectedIndex,
showCustomInput: false,
"form.maintenance_type": selectedType.value,
typePickerVisible: false,
});
}
},
onTypePickerCancel() {
this.setData({ typePickerVisible: false });
},
onTypeChange(e) {
const selectedValue = e.detail.value[0];
const selectedIndex = this.data.maintenanceTypes.findIndex(type => type.value === selectedValue);
this.setData({ typeIndex: selectedIndex });
},
onCustomTypeChange(e) {
const customType = e.detail.value;
this.setData({
customType: customType,
"form.maintenance_type": customType,
});
},
onStartTimeConfirm(e) {
const { value } = e.detail;
// 将ISO格式转换为iOS兼容格式
const formattedValue = value.replace('T', ' ').substring(0, 16);
this.setData({
"form.start_time": new Date(value).getTime(),
"form.start_time_display": dayjs(value).format("YYYY-MM-DD HH:mm"),
startTimePickerVisible: false,
});
},
onStartTimeCancel() {
this.setData({ startTimePickerVisible: false });
},
onEndTimeConfirm(e) {
const { value } = e.detail;
// 将ISO格式转换为iOS兼容格式
const formattedValue = value.replace('T', ' ').substring(0, 16);
this.setData({
"form.end_time": new Date(value).getTime(),
"form.end_time_display": dayjs(value).format("YYYY-MM-DD HH:mm"),
endTimePickerVisible: false,
});
},
onEndTimeCancel() {
this.setData({ endTimePickerVisible: false });
},
onNotesChange(e) {
this.setData({ "form.notes": e.detail.value });
},
validateForm() {
const { sub_device_id, maintenance_type, start_time } = this.data.form;
if (!sub_device_id) {
this.showMessage("子设备信息加载失败", "error");
return false;
}
if (!maintenance_type) {
this.showMessage("请选择或输入保养类型", "error");
return false;
}
if (!start_time) {
this.showMessage("请选择开始时间", "error");
return false;
}
return true;
},
async submitForm() {
if (!this.validateForm()) {
return;
}
this.setData({ submitting: true });
try {
const formData = {
sub_device_id: this.data.form.sub_device_id,
maintenance_type: this.data.form.maintenance_type,
start_time: dayjs(this.data.form.start_time).format("YYYY-MM-DD HH:mm:ss"),
end_time: this.data.form.end_time ? dayjs(this.data.form.end_time).format("YYYY-MM-DD HH:mm:ss") : null,
notes: this.data.form.notes || '',
};
let res;
if (this.data.isEditMode) {
res = await deviceMaintenanceApi.updateMaintenance(this.data.maintenanceId, formData);
} else {
res = await deviceMaintenanceApi.createMaintenance(formData);
}
if (res.code === 200 || res.code === 0) {
this.showMessage(this.data.isEditMode ? "更新成功" : "添加成功", "success");
setTimeout(() => {
wx.navigateBack();
}, 1500);
} else {
throw new Error(res.message || (this.data.isEditMode ? "更新失败" : "添加失败"));
}
} catch (error) {
this.showMessage(error.message || (this.data.isEditMode ? "更新失败" : "添加失败"), "error");
} finally {
this.setData({ submitting: false });
}
},
showMessage(message, type = "info") {
wx.showToast({
title: message,
icon: type === "success" ? "success" : type === "error" ? "error" : "none",
duration: 2000,
});
},
});

View File

@ -0,0 +1,14 @@
{
"navigationBarTitleText": "创建保养记录",
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button",
"t-input": "tdesign-miniprogram/input/input",
"t-textarea": "tdesign-miniprogram/textarea/textarea",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-message": "tdesign-miniprogram/message/message",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-picker": "tdesign-miniprogram/picker/picker",
"t-picker-item": "tdesign-miniprogram/picker-item/picker-item",
"t-date-time-picker": "tdesign-miniprogram/date-time-picker/date-time-picker"
}
}

View File

@ -0,0 +1,141 @@
<view class="container">
<t-message id="t-message" />
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<t-loading theme="circular" size="40rpx" loading />
<text>加载中...</text>
</view>
<!-- 表单内容 -->
<block wx:else>
<view class="form-group">
<!-- 子设备信息显示 -->
<view class="form-item">
<text class="label">子设备 <text class="required">*</text></text>
<t-input
value="{{subDevice.name || '加载中...'}}"
placeholder="加载中..."
disabled
/>
</view>
<!-- 保养类型选择 -->
<view class="form-item">
<text class="label">保养类型 <text class="required">*</text></text>
<view class="datetime-picker-wrapper" bindtap="showTypePicker">
<t-cell
title="{{form.maintenance_type || '请选择保养类型'}}"
arrow
hover
note="{{form.maintenance_type ? '' : '必选'}}"
t-class="picker-cell"
/>
</view>
</view>
<!-- 手动输入保养类型 -->
<view class="form-item" wx:if="{{showCustomInput}}">
<text class="label">自定义类型</text>
<t-input
value="{{customType}}"
placeholder="请输入自定义保养类型"
bind:change="onCustomTypeChange"
/>
</view>
<!-- 开始时间选择 -->
<view class="form-item">
<text class="label">开始时间 <text class="required">*</text></text>
<view class="datetime-picker-wrapper" bindtap="showStartTimePicker">
<t-cell
title="{{form.start_time_display || '请选择开始时间'}}"
arrow
hover
note="{{form.start_time ? '' : '必选'}}"
t-class="picker-cell"
/>
</view>
</view>
<!-- 结束时间选择 -->
<view class="form-item">
<text class="label">结束时间</text>
<view class="datetime-picker-wrapper" bindtap="showEndTimePicker">
<t-cell
title="{{form.end_time_display || '请选择结束时间'}}"
arrow
hover
note="{{form.end_time ? '' : '可选'}}"
t-class="picker-cell"
/>
</view>
</view>
<!-- 备注信息 -->
<view class="form-item">
<text class="label">备注</text>
<t-textarea
value="{{form.notes}}"
placeholder="请输入保养备注信息(选填)"
maxlength="200"
indicator
bind:change="onNotesChange"
/>
</view>
</view>
<view class="submit-container">
<t-button
theme="primary"
size="large"
loading="{{submitting}}"
disabled="{{submitting}}"
bind:tap="submitForm"
block
>{{isEditMode ? '更新保养记录' : '添加保养记录'}}</t-button>
</view>
</block>
<!-- 保养类型选择器 -->
<t-picker
title="选择保养类型"
visible="{{typePickerVisible}}"
value="{{[form.maintenance_type]}}"
bind:change="onTypeChange"
bind:cancel="onTypePickerCancel"
bind:confirm="onTypeConfirm"
>
<t-picker-item options="{{maintenanceTypes}}" />
</t-picker>
<!-- 开始时间选择器 -->
<t-date-time-picker
title="选择开始时间"
visible="{{startTimePickerVisible}}"
mode="minute"
format="YYYY-MM-DD HH:mm"
value="{{form.start_time || currentDate}}"
confirm-btn="确认"
cancel-btn="取消"
bind:confirm="onStartTimeConfirm"
bind:cancel="onStartTimeCancel"
auto-close
show-week
/>
<!-- 结束时间选择器 -->
<t-date-time-picker
title="选择结束时间"
visible="{{endTimePickerVisible}}"
mode="minute"
format="YYYY-MM-DD HH:mm"
value="{{form.end_time || currentDate}}"
confirm-btn="确认"
cancel-btn="取消"
bind:confirm="onEndTimeConfirm"
bind:cancel="onEndTimeCancel"
auto-close
show-week
/>
</view>

View File

@ -0,0 +1,246 @@
/* pages/surgery/create/index.wxss */
.container {
padding: 30rpx;
background-color: #f6f6f6;
min-height: 100vh;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300rpx;
}
.form-group {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
margin-bottom: 16rpx;
color: #333;
}
.required {
color: #e34d59;
}
.picker-cell {
padding: 20rpx !important;
border-radius: 8rpx;
background-color: #f8f8f8 !important;
}
/* 原生选择器样式 */
.picker-view {
padding: 24rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
position: relative;
}
.time-picker {
margin-top: 16rpx;
}
.picker-note {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
padding-left: 24rpx;
}
/* 设备选择相关样式 - 更新 */
.device-selection-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.device-selection-header text {
font-size: 28rpx;
font-weight: bold;
}
.add-device-btn {
color: #06a56c;
font-size: 26rpx;
padding: 10rpx 20rpx;
display: inline-flex;
align-items: center;
justify-content: center;
}
.selected-devices-list {
margin-top: 12rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
padding: 12rpx;
display: block; /* 确保设备列表容器显示 */
}
.device-item {
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 12rpx;
padding: 8rpx;
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05);
display: block; /* 确保设备项显示 */
}
.device-item:last-child {
margin-bottom: 0;
}
.device-item-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
}
.device-info {
flex: 1;
}
.device-name {
font-size: 28rpx;
color: #333;
}
.device-action {
display: flex;
align-items: center;
}
.delete-btn {
padding: 8rpx 20rpx;
background-color: #e34d59;
color: #fff;
border-radius: 4rpx;
font-size: 24rpx;
}
.remove-icon {
padding: 10rpx;
color: #e34d59;
}
.no-devices {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 30rpx 0;
background-color: #f8f8f8;
border-radius: 8rpx;
}
.submit-container {
margin-top: 60rpx;
padding-bottom: 40rpx;
}
/* 年月日时分秒选择器样式 */
.datetime-picker-mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.datetime-picker-container {
position: fixed;
bottom: -500rpx;
left: 0;
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
z-index: 1001;
transition: all 0.3s ease;
}
.datetime-picker-container.show {
bottom: 0;
}
.datetime-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.datetime-picker-header .title {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
.datetime-picker-header .cancel-btn,
.datetime-picker-header .confirm-btn {
font-size: 28rpx;
padding: 8rpx 10rpx;
}
.datetime-picker-header .cancel-btn {
color: #999;
}
.datetime-picker-header .confirm-btn {
color: #0052d9;
}
.datetime-picker-body {
padding: 20rpx 0;
height: 300rpx;
}
.picker-item {
line-height: 50px;
text-align: center;
}
.time-display {
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
}
/* 自定义级联选择器样式 */
:host {
--td-cascader-active-color: #0052d9;
}
/* 设备选择器的自定义样式 */
.custom-picker {
--td-picker-confirm-color: #0052d9;
}
.custom-confirm-btn {
color: #0052d9 !important;
}
.custom-cancel-btn {
color: #999 !important;
}

View File

@ -0,0 +1,148 @@
const { deviceMaintenanceApi } = require("../../utils/api");
const dayjs = require("dayjs");
Page({
data: {
sub_device_id: null,
subDeviceInfo: null,
maintenanceList: [],
loading: false,
maintenanceTypes: [
{ value: '常规保养', label: '常规保养' },
{ value: '故障维修', label: '故障维修' },
{ value: '定期检查', label: '定期检查' },
{ value: '设备升级', label: '设备升级' },
],
},
onLoad(options) {
if (options.sub_device_id) {
this.setData({ sub_device_id: options.sub_device_id });
this.loadSubDeviceInfo();
this.fetchMaintenanceList();
} else {
wx.showToast({
title: '缺少设备ID',
icon: 'error',
complete: () => wx.navigateBack(),
});
}
},
onShow() {
if (this.data.sub_device_id) {
this.fetchMaintenanceList();
}
},
// 加载子设备信息
loadSubDeviceInfo() {
const app = getApp();
const subDevice = app.globalData.subDevices?.find(
d => d.id === parseInt(this.data.sub_device_id)
);
if (subDevice) {
this.setData({
subDeviceInfo: subDevice,
deviceStatus: subDevice.status || 'normal',
selectedStatus: subDevice.status || 'normal'
});
}
},
// 获取保养记录列表
fetchMaintenanceList() {
this.setData({ loading: true });
deviceMaintenanceApi.getMaintenanceBySubDeviceId(this.data.sub_device_id)
.then(res => {
const formattedList = res.data.list.map(item => {
item.start_time_formatted = dayjs(item.start_time).format('YYYY-MM-DD HH:mm');
item.end_time_formatted = item.end_time ? dayjs(item.end_time).format('YYYY-MM-DD HH:mm') : '';
item.maintenance_by = item.operator?.nickname || '-';
item.notes = item.notes || '无';
item.device_name = item.subDevice?.device?.name || '未知设备';
item.sub_device_name = item.subDevice?.name || '未知子设备';
return item;
});
this.setData({
maintenanceList: formattedList,
loading: false
});
})
.catch(err => {
wx.showToast({
title: '获取记录失败',
icon: 'error',
});
this.setData({ loading: false });
});
},
// 添加保养记录
addMaintenance() {
wx.navigateTo({
url: `/pages/device_maintenance/create/index?sub_device_id=${this.data.sub_device_id}`
});
},
// 显示编辑对话框 - 改为跳转到编辑页面
showEditDialog(e) {
const record = e.currentTarget.dataset.record;
wx.navigateTo({
url: `/pages/device_maintenance/create/index?sub_device_id=${this.data.sub_device_id}&maintenance_id=${record.id}`
});
},
// 删除保养记录
deleteMaintenance(e) {
const record = e.currentTarget.dataset.record;
wx.showModal({
title: '确认删除',
content: '确定要删除此保养记录吗?此操作不可恢复!',
success: (res) => {
if (res.confirm) {
deviceMaintenanceApi.deleteMaintenance(record.id)
.then(() => {
wx.showToast({ title: '删除成功', icon: 'success' });
this.fetchMaintenanceList();
})
.catch(err => {
wx.showToast({
title: err.response?.data?.msg || '删除失败',
icon: 'error',
});
});
}
}
});
},
// 滑动单元格操作
onActionClick(e) {
const record = e.currentTarget.dataset.item;
const { text } = e.detail;
// 构造一个与旧函数兼容的事件对象
const mockEvent = {
currentTarget: {
dataset: {
record: record
}
}
};
if (text === '编辑') {
this.showEditDialog(mockEvent);
} else if (text === '删除') {
this.deleteMaintenance(mockEvent);
}
},
// 返回上一页
onBack() {
wx.navigateBack();
},
});

View File

@ -0,0 +1,14 @@
{
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-input": "tdesign-miniprogram/input/input",
"t-textarea": "tdesign-miniprogram/textarea/textarea",
"t-picker": "tdesign-miniprogram/picker/picker",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell"
},
"navigationBarTitleText": "保养记录"
}

View File

@ -0,0 +1,80 @@
<view class="maintenance-page">
<!-- 页面头部 -->
<view class="page-header">
<view class="page-title">保养记录</view>
<view class="header-actions">
<t-button
size="small"
theme="primary"
bind:tap="addMaintenance">
添加保养记录
</t-button>
</view>
</view>
<!-- 保养记录列表 -->
<view class="maintenance-list">
<view wx:if="{{loading}}" class="loading-container">
<t-loading theme="circular" size="80rpx" />
<view class="loading-text">加载中...</view>
</view>
<block wx:elif="{{maintenanceList.length > 0}}">
<t-swipe-cell
wx:for="{{maintenanceList}}"
wx:key="id"
right="{{[{text: '编辑', className: 't-swipe-cell-demo-btn edit-btn'}, {text: '删除', className: 't-swipe-cell-demo-btn delete-btn'}]}}"
bind:click="onActionClick"
data-item="{{item}}"
>
<view class="maintenance-item">
<view class="item-body">
<view class="field-row">
<text class="field-label">所属设备</text>
<text class="field-value">{{item.device_name}}</text>
</view>
<view class="field-row">
<text class="field-label">子设备名称</text>
<text class="field-value">{{item.sub_device_name}}</text>
</view>
<view class="field-row">
<text class="field-label">保养类型</text>
<view class="field-value">
<t-tag
variant="light"
size="small">
{{item.maintenance_type}}
</t-tag>
</view>
</view>
<view class="field-row">
<text class="field-label">开始时间</text>
<text class="field-value">{{item.start_time_formatted}}</text>
</view>
<view class="field-row">
<text class="field-label">结束时间</text>
<text class="field-value">{{item.end_time_formatted || '进行中...'}}</text>
</view>
<view class="field-row">
<text class="field-label">操作者</text>
<text class="field-value">{{item.maintenance_by}}</text>
</view>
<view class="field-row">
<text class="field-label">备注</text>
<text class="field-value notes-value">{{item.notes}}</text>
</view>
</view>
</view>
</t-swipe-cell>
</block>
<t-empty wx:else description="暂无保养记录" />
</view>
</view>

View File

@ -0,0 +1,206 @@
.maintenance-page {
padding: 0;
background: #f5f5f5;
min-height: 100vh;
}
/* 页面头部 */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #e8e8e8;
position: sticky;
top: 0;
z-index: 10;
}
.page-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
}
/* 保养记录列表 */
.maintenance-list {
padding: 20rpx 30rpx;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 0;
}
.loading-text {
margin-top: 20rpx;
font-size: 26rpx;
color: #666;
}
.maintenance-item {
display: block;
background: #fff;
border-radius: 12rpx;
margin-bottom: 20rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.maintenance-type,
.item-status {
display: flex;
align-items: center;
}
.item-body {
margin-bottom: 20rpx;
}
.field-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.field-row:last-child {
border-bottom: none;
}
.field-label {
font-size: 26rpx;
color: #666;
min-width: 120rpx;
}
.field-value {
font-size: 26rpx;
color: #333;
text-align: right;
flex: 1;
}
.notes-value {
white-space: pre-wrap;
word-break: break-all;
}
.t-swipe-cell-demo-btn {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
padding: 0 40rpx;
font-size: 28rpx;
}
.edit-btn {
background-color: #0052d9;
}
.delete-btn {
background-color: #e34d59;
}
/* 弹窗表单 */
.dialog-form {
padding: 20rpx 0;
}
.dialog-form .t-cell {
margin-bottom: 20rpx;
}
.dialog-form .t-input,
.dialog-form .t-textarea {
margin-bottom: 30rpx;
}
/* 响应式设计 */
@media (max-width: 750rpx) {
.stats-section {
flex-wrap: wrap;
}
.stat-card {
flex: 1 1 50%;
margin-bottom: 20rpx;
}
.item-actions {
justify-content: flex-start;
}
.item-actions .t-button {
flex: 1;
min-width: auto;
}
}
.maintenance-item:active {
transform: scale(0.98);
}
.stat-number {
transition: all 0.3s ease;
}
/* 空状态 */
.t-empty {
padding: 100rpx 0;
}
/* 标签样式 */
.t-tag {
border-radius: 8rpx;
font-weight: 500;
}
/* 按钮组样式优化 */
.item-actions .t-button {
border-radius: 8rpx;
font-size: 24rpx;
height: 60rpx;
line-height: 60rpx;
}
/* 图标颜色 */
.t-icon[name="time"] {
color: #409eff;
}
.t-icon[name="user"] {
color: #67c23a;
}
.t-icon[name="money-circle"] {
color: #e6a23c;
}
.field-value .t-tag {
display: inline-flex;
}
.t-icon[name="notes"] {
color: #909399;
}

View File

@ -0,0 +1,97 @@
// pages/device_management/index.js
const { deviceApi } = require("../../utils/api");
const Toast = require("../../miniprogram_npm/tdesign-miniprogram/toast/index");
Page({
/**
* 页面的初始数据
*/
data: {
devices: [], // 设备列表数据
loading: true, // 加载状态
loadError: false, // 加载错误状态
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
this.fetchDevices();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 页面显示时也刷新数据
this.fetchDevices();
},
/**
* 获取设备列表数据
*/
async fetchDevices() {
this.setData({
loading: true,
loadError: false,
});
try {
const response = await deviceApi.getDevices();
// 处理设备数据,计算子设备数量
const deviceList = response.data.list || response.data || [];
const formattedDevices = deviceList.map((device) => {
// 计算子设备数量
const subDeviceCount = device.subdevices ? device.subdevices.length : 0;
return {
...device,
subDeviceCount: subDeviceCount,
};
});
this.setData({
devices: formattedDevices,
loading: false,
});
} catch (error) {
this.setData({
loadError: true,
loading: false,
});
}
},
/**
* 查看子设备
*/
viewSubDevices(e) {
const device = e.currentTarget.dataset.device;
wx.navigateTo({
url: `/pages/device_subdevices/index?device_id=${device.id}`
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.fetchDevices();
setTimeout(() => {
wx.stopPullDownRefresh();
}, 1000);
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: "设备管理",
path: "/pages/device_management/index",
};
}
});

View File

@ -0,0 +1,16 @@
{
"navigationBarTitleText": "设备管理",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f7f8fa",
"enablePullDownRefresh": true,
"usingComponents": {
"t-toast": "tdesign-miniprogram/toast/toast",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell",
"t-fab": "tdesign-miniprogram/fab/fab",
"t-input": "tdesign-miniprogram/input/input"
}
}

View File

@ -0,0 +1,44 @@
<view class="device-container">
<block wx:if="{{loading}}">
<!-- 加载中骨架屏 -->
<view wx:for="{{5}}" wx:key="index" class="skeleton-item">
<t-skeleton theme="paragraph" loading></t-skeleton>
</view>
</block>
<block wx:elif="{{loadError}}">
<!-- 加载错误提示 -->
<view class="error-container">
<t-empty icon="error-circle" description="加载失败,请下拉刷新重试" />
</view>
</block>
<block wx:elif="{{devices && devices.length > 0}}">
<!-- 设备列表 -->
<view class="device-list">
<block wx:for="{{devices}}" wx:key="id">
<view class="device-card" bindtap="viewSubDevices" data-device="{{item}}">
<view class="device-title">{{item.name}}</view>
<view class="device-info">
<view class="info-item usage">
<text>使用次数: {{item.usage_count || 0}}次</text>
</view>
<!-- 子设备数量显示 -->
<view class="info-item subdevice-count">
<text class="count">子设备: {{item.subDeviceCount}}个</text>
</view>
</view>
<view class="device-id-tag">ID: {{item.id}}</view>
</view>
</block>
</view>
</block>
<block wx:else>
<!-- 空数据提示 -->
<view class="empty-container">
<t-empty icon="device" description="暂无设备数据" />
</view>
</block>
</view>
<!-- Toast 消息提示 -->
<t-toast id="t-toast" />

View File

@ -0,0 +1,154 @@
/* pages/device_management/index.wxss */
page {
background-color: #f7f8fa;
}
.device-container {
padding: 24rpx 0;
}
/* 骨架屏样式 */
.skeleton-item {
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx;
margin: 16rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
/* 设备列表样式 */
.device-list {
padding: 0 24rpx;
}
/* 滑动单元格容器 */
.swipe-cell-container {
margin-bottom: 16rpx;
border-radius: 8rpx;
overflow: hidden;
display: block;
}
.device-card {
background-color: #ffffff;
border-radius: 8rpx;
padding: 24rpx;
position: relative;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
width: 100%;
box-sizing: border-box;
}
.device-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 16rpx;
padding-right: 100rpx;
}
.device-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
/* 使用次数样式 */
.usage {
margin-top: 8rpx;
}
.usage text {
font-size: 28rpx;
color: #0052d9;
background-color: #eef4ff;
padding: 4rpx 12rpx;
border-radius: 4rpx;
display: inline-block;
}
/* 子设备数量样式 */
.subdevice-count {
margin-top: 8rpx;
}
.subdevice-count .count {
font-size: 28rpx;
color: #06a56c;
background-color: #e8f6f1;
padding: 4rpx 12rpx;
border-radius: 4rpx;
display: inline-block;
}
.device-id-tag {
position: absolute;
top: 24rpx;
right: 24rpx;
font-size: 24rpx;
color: #999;
}
/* 空状态和错误容器 */
.empty-container,
.error-container {
padding: 100rpx 24rpx;
}
/* 侧滑按钮样式 */
.swipe-cell-btn {
height: 100% !important;
width: 120rpx !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 28rpx !important;
}
.edit-btn {
background-color: #0052d9 !important;
color: white !important;
}
.delete-btn {
background-color: #e34d59 !important;
color: white !important;
}
/* 修改TDesign原生样式确保按钮高度与卡片一致 */
.device-list .t-swipe-cell__right {
height: 100% !important;
display: flex !important;
align-items: stretch !important;
}
/* 对话框内容样式 */
.dialog-content {
padding: 20rpx 0;
}
.t-input {
margin-bottom: 20rpx;
}
/* 悬浮按钮样式 */
.t-fab {
position: fixed !important;
bottom: 120rpx !important;
right: 32rpx !important;
}
/* 对话框内容样式 */
.dialog-content {
padding: 20rpx 0;
}
.t-input {
margin-bottom: 20rpx;
}

View File

@ -0,0 +1,402 @@
const { deviceApi } = require("../../utils/api");
const Toast = require("../../miniprogram_npm/tdesign-miniprogram/toast/index");
Page({
data: {
device: null,
device_id: null,
loading: true,
loadError: false,
// 设备状态相关
deviceStatus: 'normal',
deviceStatusMap: {
normal: '正常',
maintenance: '保养中',
repair: '维修中',
fault: '故障',
},
statusDialogVisible: false,
selectedStatus: 'normal',
// 设备编辑相关
editDialogVisible: false,
editForm: {
name: '',
description: '',
},
// 子设备相关
subDevices: [],
subDeviceLoading: false,
// 保养记录相关
maintenanceStats: {
total: 0,
completed: 0,
inProgress: 0,
},
// 操作选项
actions: [
{
icon: 'setting',
text: '更改状态',
color: '#0052d9',
action: 'changeStatus'
},
{
icon: 'add-circle',
text: '添加保养记录',
color: '#06a56c',
action: 'addMaintenance'
},
{
icon: 'time',
text: '保养记录',
color: '#fa8c16',
action: 'viewMaintenance'
},
{
icon: 'edit',
text: '编辑设备',
color: '#722ed1',
action: 'editDevice'
},
{
icon: 'view-list',
text: '管理子设备',
color: '#0052d9',
action: 'manageSubDevices'
},
{
icon: 'delete',
text: '删除设备',
color: '#e34d59',
action: 'deleteDevice'
}
]
},
onLoad(options) {
if (options.device_id) {
this.setData({ device_id: options.device_id });
this.loadDeviceInfo();
this.loadSubDevices();
this.loadMaintenanceStats();
} else {
Toast({
context: this,
selector: '#t-toast',
message: '缺少设备ID',
theme: 'error',
});
wx.navigateBack();
}
},
onShow() {
// 页面显示时刷新数据
if (this.data.device_id) {
this.loadDeviceInfo();
this.loadSubDevices();
this.loadMaintenanceStats();
}
},
// 加载设备信息
async loadDeviceInfo() {
this.setData({ loading: true, loadError: false });
try {
const response = await deviceApi.getDevices();
const deviceList = response.data.list || response.data || [];
const device = deviceList.find(d => d.id === parseInt(this.data.device_id));
if (device) {
// 计算子设备数量
const subDeviceCount = device.sub_devices ? device.sub_devices.length : 0;
this.setData({
device: { ...device, subDeviceCount },
deviceStatus: device.status || 'normal',
selectedStatus: device.status || 'normal',
editForm: {
name: device.name,
description: device.description || '',
},
loading: false,
});
} else {
this.setData({
loadError: true,
loading: false,
});
}
} catch (error) {
this.setData({
loadError: true,
loading: false,
});
}
},
// 加载子设备
async loadSubDevices() {
this.setData({ subDeviceLoading: true });
try {
const response = await deviceApi.getSubDevicesByDeviceId(this.data.device_id);
const subDevices = response.data.list || response.data || [];
this.setData({
subDevices: subDevices,
subDeviceLoading: false,
});
} catch (error) {
this.setData({
subDevices: [],
subDeviceLoading: false,
});
}
},
// 加载保养记录统计
async loadMaintenanceStats() {
try {
// 这里可以调用保养记录统计API
// 目前使用模拟数据
this.setData({
maintenanceStats: {
total: 0,
completed: 0,
inProgress: 0,
}
});
} catch (error) {
// 加载保养统计失败,使用默认值
}
},
// 处理操作按钮点击
handleAction(e) {
const action = e.currentTarget.dataset.action;
switch (action) {
case 'changeStatus':
this.showStatusDialog();
break;
case 'addMaintenance':
this.addMaintenance();
break;
case 'viewMaintenance':
this.viewMaintenance();
break;
case 'editDevice':
this.showEditDialog();
break;
case 'manageSubDevices':
this.manageSubDevices();
break;
case 'deleteDevice':
this.deleteDevice();
break;
}
},
// 显示状态更改弹窗
showStatusDialog() {
this.setData({
statusDialogVisible: true,
selectedStatus: this.data.deviceStatus,
});
},
// 关闭状态更改弹窗
closeStatusDialog() {
this.setData({
statusDialogVisible: false,
});
},
// 选择状态
selectStatus(e) {
const status = e.currentTarget.dataset.status;
this.setData({
selectedStatus: status,
});
},
// 确认状态更改
async confirmStatusChange() {
const { selectedStatus, device } = this.data;
try {
await deviceApi.updateDevice(device.id, { status: selectedStatus });
this.setData({
deviceStatus: selectedStatus,
statusDialogVisible: false,
});
Toast({
context: this,
selector: '#t-toast',
message: '状态更新成功',
theme: 'success',
});
// 刷新设备信息
this.loadDeviceInfo();
} catch (error) {
Toast({
context: this,
selector: '#t-toast',
message: '状态更新失败',
theme: 'error',
});
}
},
// 显示编辑弹窗
showEditDialog() {
this.setData({
editDialogVisible: true,
});
},
// 关闭编辑弹窗
closeEditDialog() {
this.setData({
editDialogVisible: false,
});
},
// 确认编辑
async confirmEdit() {
const { editForm, device } = this.data;
if (!editForm.name.trim()) {
Toast({
context: this,
selector: '#t-toast',
message: '请输入设备名称',
theme: 'warning',
});
return;
}
try {
await deviceApi.updateDevice(device.id, {
name: editForm.name.trim(),
description: editForm.description.trim(),
});
this.setData({
editDialogVisible: false,
});
Toast({
context: this,
selector: '#t-toast',
message: '设备信息更新成功',
theme: 'success',
});
// 刷新设备信息
this.loadDeviceInfo();
} catch (error) {
Toast({
context: this,
selector: '#t-toast',
message: '更新失败',
theme: 'error',
});
}
},
// 添加保养记录
addMaintenance() {
wx.navigateTo({
url: `/pages/device_maintenance/create/index?device_id=${this.data.device_id}`,
});
},
// 查看保养记录
viewMaintenance() {
wx.navigateTo({
url: `/pages/device_maintenance/index?device_id=${this.data.device_id}`,
});
},
// 管理子设备
manageSubDevices() {
wx.navigateTo({
url: `/pages/device_subdevices/index?device_id=${this.data.device_id}`,
});
},
// 查看子设备详情
viewSubDeviceDetail(e) {
const subDevice = e.currentTarget.dataset.subdevice;
wx.navigateTo({
url: `/pages/device_subdevices/index?device_id=${this.data.device_id}&sub_device_id=${subDevice.id}`,
});
},
// 删除设备
deleteDevice() {
wx.showModal({
title: '确认删除',
content: '确定要删除此设备吗?此操作不可撤销。',
success: (res) => {
if (res.confirm) {
this.confirmDelete();
}
},
});
},
// 确认删除
async confirmDelete() {
const { device } = this.data;
try {
await deviceApi.deleteDevice(device.id);
Toast({
context: this,
selector: '#t-toast',
message: '删除成功',
theme: 'success',
});
// 返回设备管理页面
wx.navigateBack();
} catch (error) {
Toast({
context: this,
selector: '#t-toast',
message: '删除失败',
theme: 'error',
});
}
},
// 返回上一页
onBack() {
wx.navigateBack();
},
// 下拉刷新
onPullDownRefresh() {
Promise.all([
this.loadDeviceInfo(),
this.loadSubDevices(),
this.loadMaintenanceStats(),
]).then(() => {
wx.stopPullDownRefresh();
}).catch(() => {
wx.stopPullDownRefresh();
});
},
});

View File

@ -0,0 +1,19 @@
{
"navigationBarTitleText": "设备管理详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f7f8fa",
"enablePullDownRefresh": true,
"usingComponents": {
"t-toast": "tdesign-miniprogram/toast/toast",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-button": "tdesign-miniprogram/button/button",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-input": "tdesign-miniprogram/input/input",
"t-textarea": "tdesign-miniprogram/textarea/textarea"
}
}

View File

@ -0,0 +1,72 @@
<view class="device-detail-page">
<!-- 页面头部 -->
<view class="page-header">
<view class="header-left">
<view class="back-btn" bind:tap="onBack">
<t-icon name="chevron-left" size="48rpx" />
</view>
<view class="device-info">
<view class="device-name">{{device.name}}</view>
<view class="device-meta">
<text class="device-id">ID: {{device.id}}</text>
<text class="subdevice-count">子设备: {{device.subDeviceCount || 0}}个</text>
</view>
</view>
</view>
</view>
<!-- 加载状态 -->
<block wx:if="{{loading}}">
<view class="loading-container">
<t-skeleton theme="paragraph" loading />
<t-skeleton theme="paragraph" loading />
<t-skeleton theme="paragraph" loading />
</view>
</block>
<!-- 错误状态 -->
<block wx:elif="{{loadError}}">
<view class="error-container">
<t-empty icon="error-circle" description="加载失败,请下拉刷新重试" />
</view>
</block>
<!-- 主要内容 -->
<block wx:elif="{{device}}">
<!-- 子设备列表 -->
<view class="subdevices-section">
<view class="section-title">
子设备列表
<text class="subdevice-count">({{subDevices.length}})</text>
</view>
<view wx:if="{{subDeviceLoading}}" class="loading-container">
<t-loading theme="circular" size="40rpx" />
<text class="loading-text">加载中...</text>
</view>
<block wx:elif="{{subDevices.length > 0}}">
<view
wx:for="{{subDevices}}"
wx:key="id"
class="subdevice-item"
bind:tap="viewSubDeviceDetail"
data-subdevice="{{item}}">
<view class="subdevice-info">
<view class="subdevice-name">{{item.name}}</view>
<view class="subdevice-meta">
<text class="subdevice-id">ID: {{item.id}}</text>
<text class="subdevice-usage">使用: {{item.usage_count || 0}}次</text>
<text class="subdevice-status">
状态: <text class="status-badge status-{{item.status || 'normal'}}">{{deviceStatusMap[item.status || 'normal']}}</text>
</text>
</view>
</view>
<t-icon name="chevron-right" size="32rpx" color="#999" />
</view>
</block>
<t-empty wx:else description="暂无子设备" />
</view>
</block>
<!-- Toast 消息提示 -->
<t-toast id="t-toast" />
</view>

View File

@ -0,0 +1,331 @@
.device-detail-page {
padding: 0;
background: #f5f5f5;
min-height: 100vh;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #e8e8e8;
}
.header-left {
display: flex;
align-items: center;
flex: 1;
}
.back-btn {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.device-info {
flex: 1;
}
.device-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.device-meta {
display: flex;
align-items: center;
gap: 20rpx;
font-size: 24rpx;
color: #666;
}
.device-id {
color: #999;
}
.device-status {
display: flex;
align-items: center;
}
.status-badge {
font-size: 24rpx;
padding: 2rpx 8rpx;
border-radius: 4rpx;
display: inline-block;
font-weight: 500;
margin-left: 8rpx;
}
.status-normal {
color: #06a56c;
background-color: #e8f6f1;
}
.status-maintenance {
color: #fa8c16;
background-color: #fff7e6;
}
.status-repair {
color: #722ed1;
background-color: #f9f0ff;
}
.status-fault {
color: #e34d59;
background-color: #fff1f0;
}
/* 加载和错误状态 */
.loading-container, .error-container {
padding: 100rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.loading-text {
margin-top: 20rpx;
font-size: 28rpx;
color: #666;
}
/* 内容区块 */
.info-section, .stats-section, .actions-section, .subdevices-section {
margin: 20rpx 0;
padding: 0 30rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.subdevice-count {
font-size: 28rpx;
color: #666;
font-weight: normal;
margin-left: 10rpx;
}
/* 信息卡片 */
.info-card {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
border-bottom: 1rpx solid #f0f0f0;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-size: 28rpx;
color: #666;
}
.info-value {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
/* 统计信息 */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20rpx;
}
.stat-card {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
text-align: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.stat-number {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.stat-number.in-progress {
color: #fa8c16;
}
.stat-number.completed {
color: #06a56c;
}
.stat-label {
font-size: 24rpx;
color: #666;
}
/* 操作按钮 */
.actions-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20rpx;
}
.action-item {
background: #fff;
border-radius: 12rpx;
padding: 30rpx 20rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.action-item:active {
transform: scale(0.95);
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.1);
}
.action-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.action-text {
font-size: 26rpx;
color: #333;
text-align: center;
}
/* 子设备列表 */
.subdevice-item {
background: #fff;
border-radius: 12rpx;
padding: 24rpx 30rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.subdevice-info {
flex: 1;
}
.subdevice-name {
font-size: 28rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.subdevice-meta {
display: flex;
flex-direction: column;
gap: 8rpx;
font-size: 24rpx;
color: #666;
}
.subdevice-id {
color: #999;
}
.subdevice-usage {
color: #0052d9;
}
.subdevice-status {
display: flex;
align-items: center;
}
.status-badge {
font-size: 24rpx;
padding: 2rpx 8rpx;
border-radius: 4rpx;
display: inline-block;
font-weight: 500;
margin-left: 8rpx;
}
/* 状态选择器 */
.status-options {
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 20rpx 0;
}
.status-option {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
border-radius: 8rpx;
background-color: #f7f8fa;
border: 2rpx solid transparent;
transition: all 0.3s ease;
}
.status-option.selected {
background-color: #eef4ff;
border-color: #0052d9;
}
.status-option:active {
background-color: #e7e9eb;
}
.status-option text {
margin-left: 16rpx;
font-size: 28rpx;
color: #333;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
display: inline-block;
}
/* 编辑表单 */
.edit-form {
padding: 20rpx 0;
}
.t-input, .t-textarea {
margin-bottom: 20rpx;
}

View File

@ -0,0 +1,123 @@
// pages/device_status/index.js
const { deviceApi } = require("../../utils/api");
Page({
/**
* 页面的初始数据
*/
data: {
devices: [],
statusMap: {
0: "空闲",
1: "使用中",
2: "维修中",
},
loading: true, // 添加加载状态
activeDevices: [], // 当前展开的设备面板,空数组表示全部收起
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 检查用户是否已登录
const app = getApp();
if (app.globalData.token) {
// 已登录用户,跳转到设备管理页面
wx.redirectTo({
url: '/pages/device_management/index',
});
return;
}
this.fetchDeviceStatus();
},
/**
* 获取设备状态数据
*/
fetchDeviceStatus() {
const that = this;
// 保存当前展开状态
const currentActiveDevices = this.data.activeDevices || [];
// 显示加载中
that.setData({ loading: true });
deviceApi
.getDeviceStatus()
.then((response) => {
// 处理API响应格式获取设备数组
const devices = response.data || [];
// 处理设备状态的显示文本
devices.forEach((device) => {
if (device.sub_devices && device.sub_devices.length) {
device.sub_devices.forEach((subDevice) => {
subDevice.statusText = that.data.statusMap[subDevice.status] || subDevice.status;
});
}
});
// 保持之前的展开状态
that.setData({
devices,
activeDevices: currentActiveDevices, // 保持之前的展开状态
loading: false,
});
})
.catch((err) => {
wx.showToast({
title: "获取设备状态失败",
icon: "error",
});
})
.finally(() => {
// 停止下拉刷新动画
wx.stopPullDownRefresh();
that.setData({ loading: false });
});
},
/**
* 处理折叠面板展开/收起事件
*/
handleCollapseChange(e) {
this.setData({
activeDevices: e.detail.value,
});
},
/**
* 跳转到保养记录页面
*/
goToMaintenancePage(e) {
const subDeviceId = e.currentTarget.dataset.subDeviceId;
wx.navigateTo({
url: `/pages/device_maintenance/index?sub_device_id=${subDeviceId}`,
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
this.fetchDeviceStatus();
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
return {
title: "设备状态监控",
path: "/pages/device_status/index",
};
},
});

View File

@ -0,0 +1,15 @@
{
"navigationBarTitleText": "设备状态监控",
"enablePullDownRefresh": true,
"backgroundColor": "#f6f6f6",
"usingComponents": {
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-navbar": "tdesign-miniprogram/navbar/navbar",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-collapse": "tdesign-miniprogram/collapse/collapse",
"t-collapse-panel": "tdesign-miniprogram/collapse-panel/collapse-panel"
}
}

View File

@ -0,0 +1,52 @@
<view class="device-page">
<view class="device-container">
<!-- 设备列表 -->
<block wx:if="{{devices.length > 0}}">
<t-collapse value="{{activeDevices}}" expandMutex="{{false}}" bind:change="handleCollapseChange">
<t-collapse-panel
wx:for="{{devices}}"
wx:key="device_id"
wx:for-item="device"
value="{{index}}"
header="{{device.device_name}}"
header-right-content="{{device.sub_devices.length || 0}}个子设备"
t-class="device-panel">
<!-- 设备图标 -->
<view slot="header-icon" class="device-icon">
<t-icon name="device" size="48rpx" />
</view>
<!-- 子设备列表 -->
<block wx:if="{{device.sub_devices && device.sub_devices.length > 0}}">
<t-cell
wx:for="{{device.sub_devices}}"
wx:key="id"
wx:for-item="subDevice"
title="{{device.device_name}}-{{subDevice.name}}"
description="子设备"
hover
t-class="sub-device-cell"
bordered>
<!-- 状态标签和保养记录按钮 -->
<view slot="note" class="note-container">
<t-tag
theme="{{subDevice.status === 0 ? 'success' : (subDevice.status === 1 ? 'primary' : 'warning')}}"
variant="light"
size="small">
{{subDevice.statusText}}
</t-tag>
</view>
</t-cell>
</block>
<!-- 无子设备时的提示 -->
<t-empty wx:else description="暂无子设备" />
</t-collapse-panel>
</t-collapse>
</block>
<!-- 无设备时的提示 -->
<t-empty wx:else icon="device" description="暂无设备数据" />
</view>
</view>

View File

@ -0,0 +1,48 @@
/* pages/device_status/index.wxss */
.device-page {
min-height: 100vh;
background-color: #f6f6f6;
}
.device-container {
padding: 24rpx;
}
.device-panel {
margin-bottom: 24rpx;
border-radius: 12rpx;
overflow: hidden;
background-color: #fff;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
}
.device-icon {
display: flex;
align-items: center;
margin-right: 16rpx;
color: #0052d9;
}
.sub-device-cell {
padding: 20rpx 32rpx;
}
.status-tag-container {
display: flex;
align-items: center;
justify-content: flex-end;
}
/* 适配不同状态的标签样式 */
.t-tag--success {
color: #07c160 !important;
}
.t-tag--primary {
color: #0052d9 !important;
}
.t-tag--warning {
color: #ff9702 !important;
}

View File

@ -0,0 +1,176 @@
const { deviceApi } = require("../../utils/api");
Page({
data: {
device_id: null,
device: null,
subDevices: [],
loading: true,
loadError: false,
// 子设备状态相关
statusDialogVisible: false,
selectedStatus: 0,
currentSubDeviceForStatus: null,
deviceStatusMap: {
0: "空闲",
1: "使用中",
2: "维护中",
},
// 状态选择选项
statusOptions: [
{ value: 0, label: "空闲", color: "#06a56c" },
{ value: 1, label: "使用中", color: "#1890ff" },
{ value: 2, label: "维护中", color: "#fa8c16" },
],
},
onLoad(options) {
if (options.device_id) {
this.setData({ device_id: options.device_id });
this.loadDeviceInfo();
this.loadSubDevices();
} else {
wx.showToast({
title: "缺少设备ID",
icon: "error",
});
wx.navigateBack();
}
},
onShow() {
// 页面显示时刷新数据
if (this.data.device_id) {
this.loadSubDevices();
}
},
// 加载设备信息
async loadDeviceInfo() {
try {
const response = await deviceApi.getDevices();
const deviceList = response.data.list || response.data || [];
const device = deviceList.find((d) => d.id === parseInt(this.data.device_id));
if (device) {
this.setData({ device });
}
} catch (error) {
// 加载设备信息失败,继续执行
}
},
// 加载子设备列表
async loadSubDevices() {
this.setData({ loading: true, loadError: false });
try {
const response = await deviceApi.getSubDevicesByDeviceId(this.data.device_id);
const subDevices = response.data.list || response.data || [];
// 为每个子设备添加状态信息
const formattedSubDevices = subDevices.map((subDevice) => ({
...subDevice,
status: subDevice.status !== undefined ? subDevice.status : 0,
statusText: this.data.deviceStatusMap[subDevice.status !== undefined ? subDevice.status : 0],
}));
this.setData({
subDevices: formattedSubDevices,
loading: false,
});
} catch (error) {
this.setData({
loadError: true,
loading: false,
});
}
},
// 显示状态更改对话框
showStatusDialog(e) {
const subDevice = e.currentTarget.dataset.subdevice;
this.setData({
statusDialogVisible: true,
selectedStatus: subDevice.status !== undefined ? subDevice.status : 0,
currentSubDeviceForStatus: subDevice,
});
},
// 关闭状态对话框
closeStatusDialog() {
this.setData({
statusDialogVisible: false,
selectedStatus: 0,
currentSubDeviceForStatus: null,
});
},
// 选择状态
selectStatus(e) {
const status = parseInt(e.currentTarget.dataset.status);
this.setData({
selectedStatus: status,
});
},
// 确认状态更改
async confirmStatusChange() {
const { selectedStatus, currentSubDeviceForStatus } = this.data;
try {
await deviceApi.updateSubDevice(currentSubDeviceForStatus.id, {
status: selectedStatus,
name: currentSubDeviceForStatus.name,
});
wx.showToast({
title: "状态更新成功",
icon: "success",
});
this.closeStatusDialog();
this.loadSubDevices();
} catch (error) {
wx.showToast({
title: "状态更新失败",
icon: "error",
});
}
},
// 查看子设备保养记录
viewMaintenance(e) {
const subDevice = e.currentTarget.dataset.subdevice;
wx.navigateTo({
url: `/pages/device_maintenance/index?sub_device_id=${subDevice.id}`,
});
},
// 添加保养记录
addMaintenance(e) {
const subDevice = e.currentTarget.dataset.subdevice;
wx.navigateTo({
url: `/pages/device_maintenance/create/index?sub_device_id=${subDevice.id}`,
});
},
// 返回上一页
onBack() {
wx.navigateBack();
},
// 下拉刷新
onPullDownRefresh() {
this.loadSubDevices()
.then(() => {
wx.stopPullDownRefresh();
})
.catch(() => {
wx.stopPullDownRefresh();
});
},
});

View File

@ -0,0 +1,16 @@
{
"navigationBarTitleText": "子设备管理",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f7f8fa",
"enablePullDownRefresh": true,
"usingComponents": {
"t-toast": "tdesign-miniprogram/toast/toast",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-icon": "tdesign-miniprogram/icon/icon",
"t-button": "tdesign-miniprogram/button/button",
"t-tag": "tdesign-miniprogram/tag/tag"
}
}

View File

@ -0,0 +1,122 @@
<view class="subdevices-page">
<!-- 页面头部 -->
<view class="page-header">
<view class="device-info">
<view class="device-name">{{device.name}}</view>
<view class="device-meta">
<text class="subdevice-count">子设备: {{subDevices.length}}个</text>
</view>
</view>
</view>
<!-- 加载状态 -->
<block wx:if="{{loading}}">
<view class="loading-container">
<t-skeleton theme="paragraph" loading />
<t-skeleton theme="paragraph" loading />
<t-skeleton theme="paragraph" loading />
</view>
</block>
<!-- 错误状态 -->
<block wx:elif="{{loadError}}">
<view class="error-container">
<t-empty icon="error-circle" description="加载失败,请下拉刷新重试" />
</view>
</block>
<!-- 子设备列表 -->
<block wx:elif="{{subDevices.length > 0}}">
<view class="subdevices-list">
<view
wx:for="{{subDevices}}"
wx:key="id"
class="subdevice-card">
<view class="card-header">
<view class="subdevice-info">
<view class="subdevice-name">{{item.name}}</view>
<view class="subdevice-meta">
<text class="subdevice-status">
状态: <text class="status-badge status-{{item.status}}">{{item.statusText}}</text>
</text>
</view>
</view>
<view class="usage-info">
<text class="usage-count">{{item.usage_count || 0}}次</text>
<text class="usage-label">使用</text>
</view>
</view>
<view wx:if="{{item.description}}" class="subdevice-desc">
<text>{{item.description}}</text>
</view>
<view class="card-actions">
<t-button
size="small"
variant="outline"
bind:tap="viewMaintenance"
data-subdevice="{{item}}">
保养记录
</t-button>
<t-button
size="small"
variant="outline"
bind:tap="addMaintenance"
data-subdevice="{{item}}">
添加保养
</t-button>
<t-button
size="small"
variant="outline"
bind:tap="showStatusDialog"
data-subdevice="{{item}}">
更改状态
</t-button>
</view>
</view>
</view>
</block>
<!-- 空状态 -->
<block wx:else>
<view class="empty-container">
<t-empty icon="device" description="暂无子设备" />
</view>
</block>
<!-- 状态更改对话框 -->
<view class="dialog-mask" wx:if="{{statusDialogVisible}}" bindtap="closeStatusDialog">
<view class="dialog-container" catchtap="">
<view class="dialog-header">
<text class="dialog-title">更改子设备状态</text>
<text class="dialog-close" bindtap="closeStatusDialog">×</text>
</view>
<view class="dialog-body">
<view class="status-current">
<text>当前状态:{{currentSubDeviceForStatus ? deviceStatusMap[currentSubDeviceForStatus.status] : '未知'}}</text>
</view>
<view class="status-options">
<view
wx:for="{{statusOptions}}"
wx:key="value"
class="status-option {{selectedStatus === item.value ? 'selected' : ''}}"
catchtap="selectStatus"
data-status="{{item.value}}">
<text class="status-dot" style="background-color: {{item.color}}"></text>
<text class="status-label">{{item.label}}</text>
<text class="status-check" wx:if="{{selectedStatus === item.value}}">✓</text>
</view>
</view>
</view>
<view class="dialog-footer">
<button class="dialog-btn cancel-btn" bindtap="closeStatusDialog">取消</button>
<button class="dialog-btn confirm-btn" bindtap="confirmStatusChange">确定</button>
</view>
</view>
</view>
</view>

View File

@ -0,0 +1,340 @@
.subdevices-page {
padding: 0;
background: #f5f5f5;
min-height: 100vh;
}
/* 页面头部 */
.page-header {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #e8e8e8;
}
.header-left {
display: flex;
align-items: center;
flex: 1;
}
.back-btn {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
}
.device-info {
flex: 1;
}
.device-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.device-meta {
font-size: 24rpx;
color: #666;
}
.subdevice-count {
color: #0052d9;
font-weight: 500;
}
/* 加载和错误状态 */
.loading-container,
.error-container,
.empty-container {
padding: 100rpx 30rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 子设备列表 */
.subdevices-list {
padding: 20rpx 30rpx;
}
.subdevice-card {
background: #fff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16rpx;
}
.subdevice-info {
flex: 1;
}
.subdevice-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.subdevice-meta {
display: flex;
align-items: center;
gap: 20rpx;
font-size: 24rpx;
color: #666;
}
.subdevice-id {
color: #999;
}
.subdevice-status {
display: flex;
align-items: center;
}
.status-badge {
font-size: 24rpx;
padding: 2rpx 8rpx;
border-radius: 4rpx;
display: inline-block;
font-weight: 500;
margin-left: 8rpx;
}
.status-0 {
color: #06a56c;
background-color: #e8f6f1;
}
.status-1 {
color: #1890ff;
background-color: #e6f7ff;
}
.status-2 {
color: #fa8c16;
background-color: #fff7e6;
}
.usage-info {
text-align: center;
padding: 12rpx;
background: #f0f2f5;
border-radius: 8rpx;
min-width: 100rpx;
}
.usage-count {
display: block;
font-size: 28rpx;
font-weight: bold;
color: #0052d9;
}
.usage-label {
font-size: 22rpx;
color: #666;
}
.subdevice-desc {
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
padding: 12rpx;
background: #f8f9fa;
border-radius: 6rpx;
line-height: 1.4;
}
.card-actions {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.card-actions .t-button {
flex: 1;
min-width: 120rpx;
font-size: 24rpx;
height: 60rpx;
line-height: 60rpx;
}
/* 状态选择器 */
.status-options {
display: flex;
flex-direction: column;
gap: 16rpx;
padding: 20rpx 0;
}
.status-option {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
border-radius: 8rpx;
background-color: #f7f8fa;
border: 2rpx solid transparent;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
}
.status-option.selected {
background-color: #eef4ff;
border-color: #0052d9;
}
.status-option:active {
background-color: #e7e9eb;
}
.status-label {
margin-left: 16rpx;
font-size: 28rpx;
color: #333;
flex: 1;
}
.status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
display: inline-block;
flex-shrink: 0;
}
.status-check {
position: absolute;
right: 32rpx;
color: #0052d9;
font-size: 32rpx;
font-weight: bold;
}
/* 自定义弹窗样式 */
.dialog-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.dialog-container {
width: 600rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
animation: slideUp 0.3s ease-out;
}
@keyframes slideUp {
from {
transform: translateY(100rpx);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.dialog-close {
font-size: 40rpx;
color: #999;
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.dialog-close:active {
background-color: #f5f5f5;
}
.dialog-body {
padding: 30rpx;
max-height: 600rpx;
overflow-y: auto;
}
.dialog-footer {
display: flex;
border-top: 1rpx solid #f0f0f0;
}
.dialog-btn {
flex: 1;
height: 100rpx;
border: none;
border-radius: 0;
font-size: 28rpx;
background-color: #fff;
}
.dialog-btn:active {
opacity: 0.8;
}
.cancel-btn {
color: #666;
border-right: 1rpx solid #f0f0f0;
}
.confirm-btn {
color: #0052d9;
font-weight: 500;
}
/* 弹窗内容区域 */
.dialog-content {
padding: 20rpx;
}
.status-current {
font-size: 26rpx;
color: #666;
margin-bottom: 20rpx;
padding: 16rpx;
background-color: #f8f9fa;
border-radius: 6rpx;
text-align: center;
}

375
pages/edit-profile/index.js Normal file
View File

@ -0,0 +1,375 @@
// pages/edit-profile/index.js
const { userApi } = require("../../utils/api");
const AuthUtil = require("../../utils/auth");
Page({
/**
* 页面的初始数据
*/
data: {
username: '',
nickname: '',
originalUsername: '',
originalNickname: '',
loading: false,
// 密码相关
hasPassword: false, // 用户是否已设置密码
oldPassword: '',
newPassword: '',
confirmPassword: '',
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.loadUserInfo();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 每次显示时刷新用户信息
this.loadUserInfo();
},
/**
* 加载用户信息
*/
loadUserInfo() {
const userInfo = AuthUtil.getUserInfo();
// 对于编辑页面为了确保has_password字段的准确性总是从服务器获取最新信息
// 但先使用本地信息快速显示,避免页面闪烁
if (userInfo && userInfo.username) {
this.setData({
username: userInfo.username || '',
nickname: userInfo.nickname || '',
originalUsername: userInfo.username || '',
originalNickname: userInfo.nickname || '',
hasPassword: !!(userInfo.has_password), // 使用本地缓存的has_password
});
}
// 总是从服务器获取最新信息确保has_password字段准确
this.fetchUserInfo();
},
/**
* 从服务器获取用户信息
*/
fetchUserInfo() {
wx.showLoading({ title: '加载中...' });
userApi.getCurrentUser()
.then(res => {
if ((res.code === 1 || res.code === 200) && res.data && res.data.user) {
const user = res.data.user;
this.setData({
username: user.username || '',
nickname: user.nickname || '',
originalUsername: user.username || '',
originalNickname: user.nickname || '',
hasPassword: !!(user.has_password || user.password), // 判断用户是否已设置密码
});
// 更新本地存储
AuthUtil.setUserInfo(user);
} else {
wx.showToast({
title: '获取用户信息失败',
icon: 'none'
});
}
})
.catch(err => {
wx.showToast({
title: '获取用户信息失败',
icon: 'none'
});
})
.finally(() => {
wx.hideLoading();
});
},
/**
* 用户名输入变化
*/
onUsernameChange(e) {
this.setData({
username: e.detail.value,
});
},
/**
* 昵称输入变化
*/
onNicknameChange(e) {
this.setData({
nickname: e.detail.value,
});
},
/**
* 当前密码输入变化
*/
onOldPasswordChange(e) {
this.setData({
oldPassword: e.detail.value,
});
},
/**
* 新密码输入变化
*/
onNewPasswordChange(e) {
this.setData({
newPassword: e.detail.value,
});
},
/**
* 确认密码输入变化
*/
onConfirmPasswordChange(e) {
this.setData({
confirmPassword: e.detail.value,
});
},
/**
* 验证密码输入
*/
validatePassword() {
const { hasPassword, oldPassword, newPassword, confirmPassword } = this.data;
// 如果没有输入新密码,说明不想修改密码
if (!newPassword.trim() && !confirmPassword.trim()) {
return { valid: true, changePassword: false };
}
// 如果用户已有密码,必须输入当前密码
if (hasPassword && !oldPassword.trim()) {
wx.showToast({
title: '请输入当前密码',
icon: 'none'
});
return { valid: false, changePassword: false };
}
// 新密码长度检查
if (newPassword.length < 6 || newPassword.length > 50) {
wx.showToast({
title: '新密码长度必须在6-50字符之间',
icon: 'none'
});
return { valid: false, changePassword: false };
}
// 确认密码检查
if (newPassword !== confirmPassword) {
wx.showToast({
title: '确认密码与新密码不一致',
icon: 'none'
});
return { valid: false, changePassword: false };
}
return { valid: true, changePassword: true };
},
/**
* 验证输入
*/
validateInput() {
const { username, nickname } = this.data;
if (!username.trim()) {
wx.showToast({
title: '请输入用户名',
icon: 'none'
});
return false;
}
if (username.length < 3 || username.length > 50) {
wx.showToast({
title: '用户名长度必须在3-50字符之间',
icon: 'none'
});
return false;
}
// 检查用户名格式(只能包含字母和数字)
const usernameRegex = /^[a-zA-Z0-9]+$/;
if (!usernameRegex.test(username)) {
wx.showToast({
title: '用户名只能包含字母和数字',
icon: 'none'
});
return false;
}
if (!nickname.trim()) {
wx.showToast({
title: '请输入昵称',
icon: 'none'
});
return false;
}
if (nickname.length < 2 || nickname.length > 50) {
wx.showToast({
title: '昵称长度必须在2-50字符之间',
icon: 'none'
});
return false;
}
return true;
},
/**
* 检查是否有修改
*/
hasChanges() {
const { username, nickname, originalUsername, originalNickname, newPassword } = this.data;
return username !== originalUsername ||
nickname !== originalNickname ||
newPassword.trim() !== '';
},
/**
* 保存用户信息修改
*/
saveProfileChanges() {
const { username, nickname } = this.data;
return userApi.updateProfile({
username: username.trim(),
nickname: nickname.trim()
});
},
/**
* 保存密码修改
*/
savePasswordChanges() {
const { hasPassword, oldPassword, newPassword, confirmPassword } = this.data;
return userApi.changePassword(
hasPassword ? oldPassword : '',
newPassword,
confirmPassword
);
},
/**
* 保存修改
*/
handleSave() {
if (!this.validateInput()) {
return;
}
const passwordValidation = this.validatePassword();
if (!passwordValidation.valid) {
return;
}
if (!this.hasChanges()) {
wx.showToast({
title: '没有修改内容',
icon: 'none'
});
return;
}
this.setData({ loading: true });
// 检查是否需要修改个人信息
const { username, nickname, originalUsername, originalNickname } = this.data;
const profileChanged = username !== originalUsername || nickname !== originalNickname;
// 需要执行的保存操作
const saveOperations = [];
if (profileChanged) {
saveOperations.push(this.saveProfileChanges());
}
if (passwordValidation.changePassword) {
saveOperations.push(this.savePasswordChanges());
}
// 执行所有保存操作
Promise.all(saveOperations)
.then(results => {
// 检查所有操作是否都成功支持code: 1和code: 200
const allSuccess = results.every(res => res.code === 1 || res.code === 200);
if (allSuccess) {
wx.showToast({
title: '保存成功',
icon: 'success'
});
// 如果有个人信息更新,更新本地用户信息
const profileResult = results.find(res => res.data && res.data.user);
if (profileResult) {
AuthUtil.setUserInfo(profileResult.data.user);
}
// 延时返回上一页
setTimeout(() => {
wx.navigateBack();
}, 1500);
} else {
// 找出失败的操作
const failedResults = results.filter(res => res.code !== 1);
const errorMsg = failedResults.map(res => res.msg).join('');
wx.showToast({
title: errorMsg || '保存失败',
icon: 'none'
});
}
})
.catch(err => {
wx.showToast({
title: err.message || '保存失败,请稍后重试',
icon: 'none'
});
})
.finally(() => {
this.setData({ loading: false });
});
},
/**
* 取消修改
*/
handleCancel() {
if (this.hasChanges()) {
wx.showModal({
title: '提示',
content: '确定要放弃修改吗?',
success: (res) => {
if (res.confirm) {
wx.navigateBack();
}
}
});
} else {
wx.navigateBack();
}
},
/**
* 页面卸载时的处理
*/
onUnload() {
// 如果有未保存的修改,可以在这里提示用户
}
});

View File

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "编辑个人信息",
"navigationBarBackgroundColor": "#0052d9",
"navigationBarTextStyle": "white"
}

View File

@ -0,0 +1,106 @@
<!-- 编辑个人信息页面 -->
<view class="edit-profile-container">
<view class="form-section">
<view class="form-item">
<view class="form-label">
<text class="label-text">用户名</text>
</view>
<input
class="form-input"
placeholder="请输入用户名"
value="{{username}}"
bindinput="onUsernameChange"
maxlength="50"
/>
</view>
<view class="form-item">
<view class="form-label">
<text class="label-text">昵称</text>
</view>
<input
class="form-input"
placeholder="请输入昵称"
value="{{nickname}}"
bindinput="onNicknameChange"
maxlength="50"
/>
</view>
<view class="tips">
<text class="tips-text">用户名只能包含字母和数字长度3-50字符</text>
</view>
</view>
<!-- 密码修改区域 -->
<view class="form-section">
<view class="section-title">
<text>修改密码</text>
<text class="section-subtitle">{{hasPassword ? '如不修改请留空' : '微信登录用户可设置密码'}}</text>
</view>
<view class="form-item" wx:if="{{hasPassword}}">
<view class="form-label">
<text class="label-text">当前密码</text>
</view>
<input
class="form-input"
type="password"
placeholder="请输入当前密码"
value="{{oldPassword}}"
bindinput="onOldPasswordChange"
maxlength="50"
/>
</view>
<view class="form-item">
<view class="form-label">
<text class="label-text">新密码</text>
</view>
<input
class="form-input"
type="password"
placeholder="{{hasPassword ? '请输入新密码' : '请设置密码'}}"
value="{{newPassword}}"
bindinput="onNewPasswordChange"
maxlength="50"
/>
</view>
<view class="form-item">
<view class="form-label">
<text class="label-text">确认密码</text>
</view>
<input
class="form-input"
type="password"
placeholder="请再次输入密码确认"
value="{{confirmPassword}}"
bindinput="onConfirmPasswordChange"
maxlength="50"
/>
</view>
<view class="tips">
<text class="tips-text">密码长度6-50字符建议包含字母和数字</text>
</view>
</view>
<view class="button-section">
<button
class="save-btn {{loading ? 'loading' : ''}}"
bindtap="handleSave"
disabled="{{loading}}"
>
{{loading ? '保存中...' : '保存修改'}}
</button>
<button
class="cancel-btn"
bindtap="handleCancel"
>
取消
</button>
</view>
</view>

View File

@ -0,0 +1,128 @@
/* pages/edit-profile/index.wxss */
.edit-profile-container {
padding: 30rpx;
background-color: #f6f6f6;
min-height: 100vh;
box-sizing: border-box;
}
.form-section {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.form-item {
margin-bottom: 40rpx;
}
.form-item:last-child {
margin-bottom: 0;
}
.form-label {
margin-bottom: 20rpx;
}
.label-text {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.form-input {
width: 100%;
height: 80rpx;
padding: 0 20rpx;
border: 2rpx solid #e0e0e0;
border-radius: 8rpx;
font-size: 32rpx;
color: #333;
background-color: #fff;
box-sizing: border-box;
}
.form-input:focus {
border-color: #0052d9;
}
.tips {
margin-top: 30rpx;
padding: 20rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
border-left: 6rpx solid #0052d9;
}
.tips-text {
font-size: 24rpx;
color: #666;
line-height: 1.5;
}
.section-title {
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.section-title text:first-child {
font-size: 32rpx;
color: #333;
font-weight: 500;
display: block;
margin-bottom: 8rpx;
}
.section-subtitle {
font-size: 24rpx;
color: #999;
}
.button-section {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.save-btn {
width: 100%;
height: 88rpx;
background-color: #0052d9;
color: #fff;
border: none;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
.save-btn.loading {
background-color: #ccc;
}
.save-btn:not(.loading):active {
background-color: #003ba3;
}
.cancel-btn {
width: 100%;
height: 88rpx;
background-color: #fff;
color: #666;
border: 2rpx solid #e0e0e0;
border-radius: 12rpx;
font-size: 32rpx;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn:active {
background-color: #f5f5f5;
}

66
pages/home/index.js Normal file
View File

@ -0,0 +1,66 @@
// pages/home/index.js
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {
}
})

3
pages/home/index.json Normal file
View File

@ -0,0 +1,3 @@
{
"usingComponents": {}
}

2
pages/home/index.wxml Normal file
View File

@ -0,0 +1,2 @@
<!--pages/home/index.wxml-->
<text>pages/home/index.wxml</text>

1
pages/home/index.wxss Normal file
View File

@ -0,0 +1 @@
/* pages/home/index.wxss */

356
pages/login/index.js Normal file
View File

@ -0,0 +1,356 @@
// pages/login/index.js
const { userApi } = require("../../utils/api");
const AuthUtil = require("../../utils/auth");
Page({
/**
* 页面的初始数据
*/
data: {
username: "",
password: "",
loading: false,
wechatLoading: false, // 微信登录加载状态
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 检查是否已登录
this.checkLoginStatus();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 每次页面显示时也检查登录状态
this.checkLoginStatus();
},
// 检查登录状态
checkLoginStatus() {
// 获取全局应用实例
const app = getApp();
// 如果全局状态显示已登录,则自动跳转到首页
if (app.globalData.isLoggedIn && app.globalData.userInfo) {
wx.switchTab({
url: "/pages/surgery/index"
});
} else {
// 检查是否有token
if (AuthUtil.hasToken()) {
this.autoLogin();
}
}
},
// 尝试自动登录
async autoLogin() {
this.setData({ loading: true });
try {
const isValid = await AuthUtil.validateUser();
if (isValid) {
const userInfo = AuthUtil.getUserInfo();
// 立即更新全局状态
const app = getApp();
app.globalData.isLoggedIn = true;
app.globalData.userInfo = userInfo;
// 显示提示
wx.showToast({
title: "自动登录成功",
icon: "success",
duration: 1000
});
// 启动token自动刷新
AuthUtil.startTokenRefresh();
// 立即跳转到首页
// 先尝试隐藏 toast避免影响跳转
wx.hideToast();
wx.switchTab({
url: "/pages/surgery/index",
success: () => {
},
fail: (err) => {
// 如果 switchTab 失败,尝试使用 reLaunch
wx.reLaunch({
url: "/pages/surgery/index",
success: () => {
},
fail: (reLaunchErr) => {
}
});
},
});
} else {
// 自动登录失败,已清除登录状态
}
} catch (err) {
// 自动登录异常
} finally {
this.setData({ loading: false });
}
},
// 用户名输入变化
onUsernameChange(e) {
this.setData({
username: e.detail.value,
});
},
// 密码输入变化
onPasswordChange(e) {
this.setData({
password: e.detail.value,
});
},
// 跳转到设备状态页面
goToDeviceStatus() {
wx.navigateTo({
url: '/pages/device_status/index',
});
},
// 微信一键登录
handleWechatLogin() {
this.setData({ wechatLoading: true });
// 调用微信登录
wx.login({
success: (res) => {
if (res.code) {
// 获取用户信息(可选)
wx.getUserProfile({
desc: '用于完善用户资料',
success: (userRes) => {
// 调用后端API进行微信登录
this.performWechatLogin(res.code, userRes.userInfo.nickName);
},
fail: () => {
// 用户拒绝授权使用code进行登录不带昵称
this.performWechatLogin(res.code);
}
});
} else {
wx.showToast({
title: '获取微信授权失败',
icon: 'none'
});
this.setData({ wechatLoading: false });
}
},
fail: (err) => {
wx.showToast({
title: '微信登录调用失败',
icon: 'none'
});
this.setData({ wechatLoading: false });
}
});
},
// 执行微信登录
performWechatLogin(code, nickname = null) {
userApi.miniLogin(code, nickname)
.then((res) => {
if (res.code === 1 || res.code === 200) {
// 登录成功保存token和用户信息
const { token, user } = res.data;
AuthUtil.setToken(token);
AuthUtil.setUserInfo(user);
// 启动token自动刷新
AuthUtil.startTokenRefresh();
// 立即更新全局状态
const app = getApp();
app.globalData.isLoggedIn = true;
app.globalData.userInfo = user;
wx.showToast({
title: "微信登录成功",
icon: "success",
duration: 1000
});
// 立即跳转到首页
// 先尝试隐藏 toast避免影响跳转
wx.hideToast();
wx.switchTab({
url: "/pages/surgery/index",
success: () => {
},
fail: (err) => {
// 如果 switchTab 失败,尝试使用 reLaunch
wx.reLaunch({
url: "/pages/surgery/index",
success: () => {
},
fail: (reLaunchErr) => {
}
});
},
});
} else {
// 登录失败
wx.showToast({
title: res.msg || "微信登录失败",
icon: "none",
});
}
})
.catch((err) => {
wx.showToast({
title: err.message || "微信登录失败,请稍后重试",
icon: "none",
});
})
.finally(() => {
this.setData({ wechatLoading: false });
});
},
// 处理登录
handleLogin() {
const { username, password } = this.data;
// 基本表单验证
if (!username.trim()) {
wx.showToast({
title: "请输入用户名",
icon: "none",
});
return;
}
if (!password.trim()) {
wx.showToast({
title: "请输入密码",
icon: "none",
});
return;
}
// 显示加载状态
this.setData({ loading: true });
// 调用登录API
userApi
.login(username, password)
.then((res) => {
// 检查登录是否成功(支持 code: 1 或 code: 200
if (res.code !== 1 && res.code !== 200) {
wx.showToast({
title: res.msg || "用户名或密码错误",
icon: "none",
});
this.setData({ loading: false });
return;
}
// 检查用户状态(小程序允许状态>=1的用户登录
if (res.data.user.user_status < 1) {
wx.showToast({
title: "您的账户已被禁用或需要等待管理员审核",
icon: "none",
});
this.setData({ loading: false });
return;
}
// 登录成功保存token
const { token, user } = res.data;
AuthUtil.setToken(token);
AuthUtil.setUserInfo(user);
// 启动token自动刷新
AuthUtil.startTokenRefresh();
// 立即更新全局状态
const app = getApp();
app.globalData.isLoggedIn = true;
app.globalData.userInfo = user;
wx.showToast({
title: "登录成功",
icon: "success",
duration: 1000
});
// 登录成功后立即导航到首页
// 先尝试隐藏 toast避免影响跳转
wx.hideToast();
wx.switchTab({
url: "/pages/surgery/index",
success: () => {
},
fail: (err) => {
// 如果 switchTab 失败,尝试使用 reLaunch
wx.reLaunch({
url: "/pages/surgery/index",
success: () => {
},
fail: (reLaunchErr) => {
}
});
},
});
})
.catch((err) => {
wx.showToast({
title: err.message || "登录失败,请稍后重试",
icon: "none",
});
})
.finally(() => {
this.setData({ loading: false });
});
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {},
});

9
pages/login/index.json Normal file
View File

@ -0,0 +1,9 @@
{
"navigationBarTitleText": "登录",
"usingComponents": {
"t-input": "tdesign-miniprogram/input/input",
"t-button": "tdesign-miniprogram/button/button",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-icon": "tdesign-miniprogram/icon/icon"
}
}

42
pages/login/index.wxml Normal file
View File

@ -0,0 +1,42 @@
<!-- 登录页面 -->
<view class="login-container">
<view class="login-header">
<image class="logo" mode="aspectFit"></image>
<view class="title">医疗系统登录</view>
</view>
<view class="login-form">
<!-- 微信一键登录区域 -->
<view class="wechat-login-section">
<t-button theme="primary" size="large" block bindtap="handleWechatLogin" loading="{{wechatLoading}}" icon="user">
微信一键登录
</t-button>
<view class="divider">
<view class="divider-line"></view>
<view class="divider-text">或</view>
<view class="divider-line"></view>
</view>
</view>
<!-- 传统登录表单 -->
<t-cell-group>
<t-input label="账号" placeholder="请输入账号" value="{{username}}" bindchange="onUsernameChange" clearable>
<t-icon slot="left-icon" name="user" size="48rpx" />
</t-input>
<t-input label="密码" placeholder="请输入密码" value="{{password}}" bindchange="onPasswordChange" type="password" clearable>
<t-icon slot="left-icon" name="lock-on" size="48rpx" />
</t-input>
</t-cell-group>
<view class="btn-wrapper">
<t-button theme="default" size="large" block bindtap="handleLogin" loading="{{loading}}">账号密码登录</t-button>
</view>
<!-- 新增设备状态按钮 -->
<view class="device-status-btn">
<t-button theme="light" size="small" icon="device" variant="outline" bindtap="goToDeviceStatus">设备状态</t-button>
</view>
</view>
</view>

97
pages/login/index.wxss Normal file
View File

@ -0,0 +1,97 @@
/* pages/login/index.wxss */
page {
height: 100%;
overflow: hidden;
}
.login-container {
display: flex;
flex-direction: column;
height: 100vh;
padding: 30rpx;
background-color: #f6f6f6;
box-sizing: border-box;
position: relative;
overflow: hidden;
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 8vh; /* 从12vh减小到8vh使整体内容上移 */
margin-bottom: 50rpx; /* 稍微减小下边距 */
}
.logo {
width: 200rpx; /* 进一步增大logo尺寸 */
height: 200rpx;
margin-bottom: 40rpx;
}
.title {
font-size: 56rpx; /* 将标题字体从48rpx增大到56rpx */
font-weight: bold;
color: #0052d9; /* 使用TDesign的主题蓝色与登录按钮颜色一致 */
letter-spacing: 4rpx; /* 增加字间距 */
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); /* 添加轻微阴影,增强层次感 */
}
.login-form {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx 20rpx; /* 增加内边距 */
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
margin-bottom: 30rpx;
flex: 0 0 auto;
}
/* 微信登录区域 */
.wechat-login-section {
margin-bottom: 40rpx;
}
/* 分割线样式 */
.divider {
display: flex;
align-items: center;
margin: 30rpx 0;
}
.divider-line {
flex: 1;
height: 1rpx;
background-color: #e0e0e0;
}
.divider-text {
padding: 0 20rpx;
font-size: 24rpx;
color: #999;
}
.btn-wrapper {
margin-top: 60rpx;
margin-bottom: 20rpx;
}
/* 设备状态按钮样式 */
.device-status-btn {
display: flex;
justify-content: center;
margin-top: 20rpx;
}
.device-status-btn button {
font-size: 24rpx !important;
}
.footer-text {
text-align: center;
font-size: 24rpx;
color: #999;
position: absolute;
bottom: 30rpx;
left: 0;
right: 0;
}

116
pages/mine/index.js Normal file
View File

@ -0,0 +1,116 @@
// pages/mine/index.js
const app = getApp();
const AuthUtil = require("../../utils/auth");
Page({
/**
* 页面的初始数据
*/
data: {
userInfo: {},
isLoggedIn: false,
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
// 每次页面显示时更新用户信息
this.updateUserInfo();
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide() {},
/**
* 生命周期函数--监听页面卸载
*/
onUnload() {},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {},
/**
* 用户点击右上角分享
*/
onShareAppMessage() {},
/**
* 更新用户信息
*/
updateUserInfo() {
// 从认证工具获取用户信息
const userInfo = AuthUtil.getUserInfo();
const isLoggedIn = AuthUtil.hasToken() && app.globalData.isLoggedIn;
this.setData({
userInfo: userInfo || {},
isLoggedIn: isLoggedIn,
});
},
/**
* 跳转到编辑个人信息页面
*/
goToEditProfile() {
if (!this.data.isLoggedIn) {
wx.showToast({
title: '请先登录',
icon: 'none'
});
return;
}
wx.navigateTo({
url: '/pages/edit-profile/index'
});
},
/**
* 处理注销登录
*/
handleLogout() {
wx.showModal({
title: "确认注销",
content: "您确定要退出登录吗?",
success: (res) => {
if (res.confirm) {
// 使用认证工具清除登录状态
AuthUtil.clearAuth();
// 提示用户
wx.showToast({
title: "已注销登录",
icon: "success",
});
// 跳转到登录页
setTimeout(() => {
wx.reLaunch({
url: "/pages/login/index",
});
}, 1500);
}
},
});
},
});

12
pages/mine/index.json Normal file
View File

@ -0,0 +1,12 @@
{
"navigationBarBackgroundColor": "#0052d9",
"navigationBarTextStyle": "white",
"navigationBarTitleText": "我的",
"usingComponents": {
"t-avatar": "tdesign-miniprogram/avatar/avatar",
"t-button": "tdesign-miniprogram/button/button",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-message": "tdesign-miniprogram/message/message"
}
}

29
pages/mine/index.wxml Normal file
View File

@ -0,0 +1,29 @@
<!-- pages/mine/index.wxml -->
<view class="user-container">
<!-- 用户信息部分 -->
<view class="user-info" bindtap="goToEditProfile">
<t-avatar class="avatar" size="large" icon="user" image="{{userInfo.avatar || ''}}"></t-avatar>
<view class="user-detail">
<view class="username">{{userInfo.nickname || '未登录'}}</view>
<view class="user-id">@{{userInfo.username || ''}}</view>
<view class="user-role">{{userInfo.user_status === 1 ? '用户' : (userInfo.user_status === 2 ? '管理员' : '待审核')}}</view>
</view>
<view class="edit-icon" wx:if="{{isLoggedIn}}">
<t-icon name="edit" size="36rpx" color="#0052d9" />
</view>
</view>
<!-- 功能列表 -->
<view class="function-list">
<t-cell-group>
<t-cell title="个人信息" hover left-icon="user" bindtap="goToEditProfile" arrow />
<t-cell title="系统设置" hover left-icon="setting" url="" arrow />
<t-cell title="关于我们" hover left-icon="info-circle" url="" arrow />
</t-cell-group>
</view>
<!-- 注销按钮 -->
<view class="logout-container" wx:if="{{isLoggedIn}}">
<t-button theme="danger" block size="large" bind:tap="handleLogout">注销登录</t-button>
</view>
</view>

60
pages/mine/index.wxss Normal file
View File

@ -0,0 +1,60 @@
/* pages/mine/index.wxss */
.user-container {
display: flex;
flex-direction: column;
min-height: 100vh;
background-color: #f6f6f6;
}
.user-info {
display: flex;
align-items: center;
padding: 40rpx 30rpx;
background-color: #0052d9;
color: #fff;
position: relative;
cursor: pointer;
}
.avatar {
margin-right: 30rpx;
background-color: #fff;
}
.user-detail {
flex: 1;
}
.username {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 5rpx;
}
.user-id {
font-size: 24rpx;
opacity: 0.7;
margin-bottom: 8rpx;
}
.user-role {
font-size: 28rpx;
opacity: 0.8;
}
.edit-icon {
padding: 10rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.function-list {
margin-top: 30rpx;
}
.logout-container {
margin: 60rpx 30rpx;
}

View File

@ -0,0 +1,970 @@
// pages/surgery/create/index.js
const { surgeryApi, doctorApi, deviceApi } = require("../../../utils/api");
const dayjs = require("../../../miniprogram_npm/dayjs/index");
Page({
/**
* 页面的初始数据
*/
data: {
loading: true, // 页面加载状态
submitting: false, // 表单提交状态
isEditMode: false, // 是否为编辑模式
surgeryId: null, // 编辑模式下的手术ID
// 表单数据
formData: {
surgery_id: "", // 手术编号
surgery_name: "", // 手术名称
patient: "", // 患者姓名
surgery_time: "", // 完整手术时间戳
surgery_time_display: "", // 显示的完整手术时间
},
// 主刀医生相关
doctors: [], // 医生列表
selectedDoctor: null, // 已选择的医生
selectedDoctorValue: "", // 级联选择器选中的医生值
doctorSelectorVisible: false, // 医生选择器弹窗可见性
departmentDoctors: [], // 按科室分组的医生列表
activeTabIndex: 0, // 当前激活的科室标签索引
// 设备相关
devices: [], // 设备列表
deviceTree: [], // 树形结构的设备数据
selectedSubDevices: [], // 已选择的设备
selectedDeviceValues: [], // 已选择的设备值数组
selectedDeviceValue: "", // 当前选中的设备值
deviceSelectorVisible: false, // 设备选择器弹窗可见性
// 新增:设备状态追踪
deviceStatus: {}, // 记录设备状态 {id: {status: 0/1, inUse: bool}}
// TreeSelect 组件所需的自定义键名配置
treeSelectKeys: {
label: "label",
value: "value",
children: "children",
},
// 级联选择器
cascaderVisible: false, // 级联选择器可见性
doctorCascaderOptions: [], // 医生级联选择器选项
// 日期时间选择器相关
dateTimePickerVisible: false, // 日期时间选择器可见性
currentDate: "", // 当前日期时间用于默认值
minDate: "", // 新增:允许的最小日期时间(当前时间)
// 设备时间选择器相关
deviceStartTimePickerVisible: false, // 设备开始时间选择器可见性
deviceEndTimePickerVisible: false, // 设备结束时间选择器可见性
currentDeviceTimeIndex: -1, // 当前正在设置时间的设备索引
currentDeviceStartTime: "", // 当前设备开始时间
currentDeviceEndTime: "", // 当前设备结束时间
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// 设置当前时间作为默认值和最小时间限制
this.setCurrentDateTime();
// 手术记录回显
let formData = wx.getStorageSync("surgery_formData")
let that = this
if (formData) {
wx.showModal({
title: "是否显示未保存记录 ",
cancelText: "否",
confirmText: "是",
success (res) {
if (res.confirm) {
that.setData({
formData: {...formData},
selectedDoctor: formData.doctor
})
} else if (res.cancel) {
wx.removeStorageSync('surgery_formData')
}
}
})
}
// 检查是否为编辑模式
if (options && options.id && options.mode === "edit") {
const surgeryId = options.id;
this.setData({
isEditMode: true,
surgeryId: surgeryId,
});
wx.setNavigationBarTitle({
title: "编辑手术记录",
});
} else {
wx.setNavigationBarTitle({
title: "创建手术记录",
});
}
// 并行加载医生和设备数据
Promise.all([this.fetchDoctors(), this.fetchDevices()])
.then(() => {
// 如果是编辑模式,加载现有手术数据
if (this.data.isEditMode && this.data.surgeryId) {
this.fetchSurgeryDetails(this.data.surgeryId);
} else {
this.setData({ loading: false });
}
})
.catch((error) => {
this.setData({ loading: false });
});
},
/*
* 保存表单数据到本地
*/
saveData (doctor) {
let formData = {...this.data.formData}
if (doctor) formData.doctor = doctor
wx.setStorageSync("surgery_formData", formData)
},
/**
* 获取手术详情
*/
async fetchSurgeryDetails(surgeryId) {
try {
const res = await surgeryApi.getSurgeryById(surgeryId);
const surgery = res.data;
if (!surgery) {
throw new Error("未找到手术记录");
}
// 格式化日期显示 - 使用安全的日期解析
const surgeryTime = this.safeParseDate(surgery.surgery_time);
const timestamp = surgeryTime.getTime();
const formattedDateTime = dayjs(surgeryTime).format("YYYY年MM月DD日 HH:mm:ss");
// 更新表单数据
this.setData({
formData: {
surgery_id: surgery.surgery_id || "",
surgery_name: surgery.surgery_name || "",
patient: surgery.patient || "",
surgery_time: timestamp,
surgery_time_display: formattedDateTime,
},
});
// 设置选中的医生
if (surgery.doctor) {
// 检查选项中是否包含当前医生
const targetDoctorId = surgery.doctor.id;
const findDoctorInOptions = (options) => {
for (const dept of options) {
for (const doctor of dept.children || []) {
if (doctor.doctor && doctor.doctor.id === targetDoctorId) {
return doctor;
}
}
}
return null;
};
const foundDoctor = findDoctorInOptions(this.data.doctorCascaderOptions);
this.setSelectedDoctor(surgery.doctor);
} else {
// 手术数据中没有医生信息
}
// 设置选中的设备
if (surgery.surgerySubDevices && Array.isArray(surgery.surgerySubDevices)) {
const selectedDevices = [];
surgery.surgerySubDevices.forEach((surgerySubDevice) => {
if (surgerySubDevice.subDevice) {
// 为子设备添加设备名称和时间信息
const subDevice = {
...surgerySubDevice.subDevice,
device_name: surgerySubDevice.subDevice.device ? surgerySubDevice.subDevice.device.name : "未知设备",
// 添加设备时间信息
startTime: surgerySubDevice.start_time || null,
endTime: surgerySubDevice.end_time || null,
startTimeDisplay: surgerySubDevice.start_time ? dayjs(surgerySubDevice.start_time).format('YYYY-MM-DD HH:mm') : '点击设置',
endTimeDisplay: surgerySubDevice.end_time ? dayjs(surgerySubDevice.end_time).format('YYYY-MM-DD HH:mm') : '点击设置',
};
selectedDevices.push(subDevice);
// 更新设备状态
if (this.data.deviceStatus[subDevice.id]) {
const updatedDeviceStatus = { ...this.data.deviceStatus };
updatedDeviceStatus[subDevice.id].inUse = true;
this.setData({
deviceStatus: updatedDeviceStatus,
});
}
}
});
this.setData({
selectedSubDevices: selectedDevices,
});
} else {
// 手术数据中没有设备信息
}
this.setData({ loading: false });
} catch (error) {
this.showMessage("获取手术详情失败,请返回重试", "error");
this.setData({ loading: false });
}
},
/**
* 根据医生数据设置选中的医生
*/
setSelectedDoctor(doctor) {
if (!doctor || !doctor.id) return;
// 查找并设置医生级联选择器的值
const doctorValue = `doctor_${doctor.id}`;
this.setData({
selectedDoctor: doctor,
selectedDoctorValue: doctorValue,
});
},
/**
* 安全的日期解析函数
*/
safeParseDate(dateStr) {
if (!dateStr) return new Date();
// 尝试不同的日期格式
const formats = [
dateStr, // 原始格式
dateStr.replace(' ', 'T'), // ISO格式
dateStr.replace(' ', 'T') + '+08:00', // 带时区的ISO格式
new Date(dateStr) // 直接解析
];
for (let i = 0; i < formats.length; i++) {
try {
const date = typeof formats[i] === 'string' ? new Date(formats[i]) : formats[i];
if (!isNaN(date.getTime())) {
return date;
}
} catch (e) {
// 日期格式解析失败
}
}
// 如果都失败了,返回当前时间
return new Date();
},
/**
* 设置当前日期时间作为默认值和最小时间限制
*/
setCurrentDateTime() {
const now = new Date();
const formattedDateTime = dayjs(now).format("YYYY-MM-DD HH:mm:ss");
this.setData({
currentDate: formattedDateTime,
minDate: formattedDateTime, // 设置最小时间为当前时间
});
},
/**
* 获取医生列表
*/
async fetchDoctors() {
try {
// 调用API获取医生列表数据
const res = await doctorApi.getDoctors();
// 后端返回的是分页格式:{list: [...], total: ...}
const doctorData = res.data;
if (!doctorData || !doctorData.list || !Array.isArray(doctorData.list)) {
throw new Error("获取医生数据格式错误");
}
// 过滤确保医生数据完整(包含科室信息)
const validDoctors = doctorData.list.filter((doctor) => doctor && doctor.id && doctor.name && doctor.department && doctor.department.name);
// 按科室分组医生,为级联选择器准备数据
const departmentsMap = new Map();
validDoctors.forEach((doctor) => {
const deptId = doctor.department.id;
const deptName = doctor.department.name;
if (!departmentsMap.has(deptId)) {
departmentsMap.set(deptId, {
label: deptName,
value: `dept_${deptId}`,
children: [],
});
}
// 将医生添加到对应科室的children中
departmentsMap.get(deptId).children.push({
label: doctor.name,
value: `doctor_${doctor.id}`,
// 保存原始医生对象用于选中后的数据处理
doctor: doctor,
});
});
// 将Map转换为数组
const doctorCascaderOptions = Array.from(departmentsMap.values());
this.setData({
loading: false,
doctors: validDoctors,
doctorCascaderOptions,
});
} catch (error) {
this.setData({ loading: false });
this.showMessage("获取医生列表失败,请重试", "error");
}
},
/**
* 获取设备数据并构建树形结构
*/
async fetchDevices() {
try {
// 调用API获取所有设备数据
const res = await deviceApi.getDevices();
// 后端返回的是分页格式:{list: [...], total: ...}
const deviceData = res.data;
if (!deviceData || !deviceData.list || !Array.isArray(deviceData.list)) {
throw new Error("获取设备数据格式错误");
}
const devices = deviceData.list;
// 获取所有子设备
const subDevicesRes = await deviceApi.getAllSubDevices();
const subDevicesData = subDevicesRes.data;
// 后端返回的是分页格式:{list: [...], total: ...}
const subDevices = subDevicesData.list || [];
// 构建设备状态字典
const deviceStatus = {};
subDevices.forEach((subDevice) => {
deviceStatus[subDevice.id] = {
status: subDevice.status || 0, // 0表示可用1表示占用
inUse: false, // 当前表单中是否被选择
};
});
// 构建树形结构数据
const deviceTree = devices.map((device) => {
// 找出该设备下的所有子设备
const children = subDevices
.filter((sub) => sub.device_id === device.id)
.map((sub) => {
const subDeviceName = sub.name || `设备${sub.id}`;
const statusText = sub.status === 1 ? " (占用中)" : "";
return {
label: `${subDeviceName}${statusText}`, // 显示占用状态
value: `subdevice_${sub.id}`,
// 移除基于状态的禁用设置,允许选择占用中的设备
// 保存完整的子设备数据
subDevice: {
...sub,
name: sub.name || `设备${sub.id}`, // 确保有名称
device_name: device.name, // 添加设备名称,方便显示
},
};
});
return {
label: device.name,
value: `device_${device.id}`,
// 只有当没有子设备时才禁用主设备选择,不再因子设备被占用而禁用
disabled: children.length === 0,
children,
};
});
this.setData({
devices,
deviceTree,
deviceStatus,
});
} catch (error) {
this.showMessage("获取设备数据失败,请重试", "warning");
}
},
/**
* 处理输入框内容变更
*/
onInputChange(e) {
const { field } = e.currentTarget.dataset;
const { value } = e.detail;
this.setData({
[`formData.${field}`]: value,
});
this.saveData()
},
/**
* 显示日期时间选择器
*/
showDateTimePicker() {
this.setData({
dateTimePickerVisible: true,
});
},
/**
* 日期时间选择器确认事件
*/
onDateTimeConfirm(e) {
// 使用 dayjs 格式化日期时间显示
const formattedDateTime = dayjs(e.detail.value).format("YYYY年MM月DD日 HH:mm:ss");
const timestamp = new Date(e.detail.value).getTime();
this.setData({
"formData.surgery_time": timestamp,
"formData.surgery_time_display": formattedDateTime,
dateTimePickerVisible: false,
});
this.saveData()
},
/**
* 日期时间选择器取消事件
*/
onDateTimeCancel() {
this.setData({
dateTimePickerVisible: false,
});
},
/**
* 日期时间选择器变化事件
*/
onDateTimeChange(e) {
// 日期时间选择器变化事件
},
/**
* 日期时间选择器选择事件
*/
onDateTimePick(e) {
// 日期时间选择器选择事件
},
/**
* 显示医生选择器弹窗 - 使用级联选择器选项卡风格
*/
showDoctorSelector() {
if (this.data.doctorCascaderOptions.length === 0) {
this.showMessage("暂无可选医生", "warning");
return;
}
this.setData({
doctorSelectorVisible: true,
});
},
/**
* 级联选择器关闭事件
*/
onDoctorCascaderClose(e) {
this.setData({
doctorSelectorVisible: false,
});
},
/**
* 级联选择器变更事件
*/
onDoctorCascaderChange(e) {
const { value, selectedOptions } = e.detail;
if (value && value.startsWith("doctor_")) {
// 获取医生 ID
const doctorId = Number(value.split("_")[1]);
// 查找选中的医生数据
const findDoctor = (options, doctorId) => {
for (const dept of options) {
for (const doctorOption of dept.children || []) {
if (doctorOption.doctor && doctorOption.doctor.id === doctorId) {
return doctorOption.doctor;
}
}
}
return null;
};
const selectedDoctor = findDoctor(this.data.doctorCascaderOptions, doctorId);
if (selectedDoctor) {
this.setData({
selectedDoctor,
selectedDoctorValue: value,
doctorSelectorVisible: false, // 选择后自动关闭选择器
});
this.showMessage(`已选择医生:${selectedDoctor.name}${selectedDoctor.department.name}`, "success");
}
this.saveData(selectedDoctor)
}
},
/**
* 级联选择器选择事件
*/
onDoctorCascaderPick(e) {
// 级联选择器选择事件
},
/**
* 处理设备选择变更事件
*/
onDeviceChange(e) {
const { value } = e.detail;
// 找出选中的子设备
const selectedSubDevices = [];
const findSubDevices = (tree, values) => {
if (!tree || !values) return;
for (const node of tree) {
if (node.children) {
for (const child of node.children) {
if (values.includes(child.value) && child.subDevice) {
selectedSubDevices.push(child.subDevice);
}
}
// 递归检查子节点
findSubDevices(node.children, values);
}
}
};
findSubDevices(this.data.deviceTree, value);
this.setData({
selectedDeviceValues: value,
selectedSubDevices,
});
},
/**
* 显示设备选择器
*/
showDeviceSelector() {
if (this.data.deviceTree.length === 0) {
this.showMessage("暂无可选设备", "warning");
return;
}
this.setData({
deviceSelectorVisible: true,
selectedDeviceValue: "", // 重置选择值,确保每次打开时都从顶层开始选择
});
},
/**
* 设备选择器关闭事件
*/
onDeviceCascaderClose(e) {
this.setData({
deviceSelectorVisible: false,
});
},
/**
* 设备级联选择器变更事件
*/
onDeviceCascaderChange(e) {
const { value, selectedOptions } = e.detail;
if (value && value.startsWith("subdevice_")) {
// 获取子设备 ID
const subDeviceId = Number(value.split("_")[1]);
// 查找选中的子设备数据
const findSubDevice = (options, subDeviceId) => {
for (const device of options) {
for (const subDeviceOption of device.children || []) {
if (subDeviceOption.subDevice && subDeviceOption.subDevice.id === subDeviceId) {
return subDeviceOption.subDevice;
}
}
}
return null;
};
const selectedSubDevice = findSubDevice(this.data.deviceTree, subDeviceId);
if (selectedSubDevice) {
// 删除设备占用的判断,允许选择任何设备
// 原有代码:
// if (selectedSubDevice.status === 1) {
// this.showMessage(`设备 ${selectedSubDevice.sub_device_name} 当前已占用,请选择其他设备`, "warning");
// return;
// }
// 检查是否已经选择过该设备
const existingDeviceIndex = this.data.selectedSubDevices.findIndex((device) => device.id === selectedSubDevice.id);
if (existingDeviceIndex === -1) {
// 如果是新设备,添加到已选择的设备列表中
const updatedDevices = [...this.data.selectedSubDevices, {
...selectedSubDevice,
startTime: this.data.formData.surgery_time || new Date().getTime(),
startTimeDisplay: this.data.formData.surgery_time_display || dayjs().format("YYYY-MM-DD HH:mm:ss"),
endTime: null,
endTimeDisplay: "未设置",
}];
// 更新设备状态为已选择
const updatedDeviceStatus = { ...this.data.deviceStatus };
updatedDeviceStatus[selectedSubDevice.id].inUse = true;
this.setData({
selectedSubDevices: updatedDevices,
selectedDeviceValue: value,
deviceSelectorVisible: false,
deviceStatus: updatedDeviceStatus,
});
// 添加后延迟提示,确保界面已更新
setTimeout(() => {
this.showMessage(`已添加设备: ${selectedSubDevice.device_name}-${selectedSubDevice.name}`, "success");
}, 100);
} else {
this.showMessage("该设备已被选择", "warning");
}
}
}
},
/**
* 设备级联选择器选择事件
*/
onDeviceCascaderPick(e) {
// 设备级联选择器选择事件
},
/**
* 显示设备开始时间选择器
*/
showDeviceStartTimePicker(e) {
const { index } = e.currentTarget.dataset;
const device = this.data.selectedSubDevices[index];
this.setData({
currentDeviceTimeIndex: index,
currentDeviceStartTime: device.startTime || this.data.formData.surgery_time || this.data.currentDate,
deviceStartTimePickerVisible: true,
});
},
/**
* 显示设备结束时间选择器
*/
showDeviceEndTimePicker(e) {
const { index } = e.currentTarget.dataset;
const device = this.data.selectedSubDevices[index];
this.setData({
currentDeviceTimeIndex: index,
currentDeviceEndTime: device.endTime || this.data.currentDate,
deviceEndTimePickerVisible: true,
});
},
/**
* 设备开始时间选择器确认事件
*/
onDeviceStartTimeConfirm(e) {
const { currentDeviceTimeIndex } = this.data;
if (currentDeviceTimeIndex === -1) return;
// 格式化时间显示
const formattedDateTime = dayjs(e.detail.value).format("YYYY-MM-DD HH:mm:ss");
const timestamp = new Date(e.detail.value).getTime();
// 更新设备时间信息
const updatedDevices = [...this.data.selectedSubDevices];
updatedDevices[currentDeviceTimeIndex] = {
...updatedDevices[currentDeviceTimeIndex],
startTime: timestamp,
startTimeDisplay: formattedDateTime,
};
this.setData({
selectedSubDevices: updatedDevices,
deviceStartTimePickerVisible: false,
currentDeviceTimeIndex: -1,
});
this.showMessage(`设备开始时间已设置: ${formattedDateTime}`, "success");
},
/**
* 设备开始时间选择器取消事件
*/
onDeviceStartTimeCancel() {
this.setData({
deviceStartTimePickerVisible: false,
currentDeviceTimeIndex: -1,
});
},
/**
* 设备结束时间选择器确认事件
*/
onDeviceEndTimeConfirm(e) {
const { currentDeviceTimeIndex } = this.data;
if (currentDeviceTimeIndex === -1) return;
// 格式化时间显示
const formattedDateTime = dayjs(e.detail.value).format("YYYY-MM-DD HH:mm:ss");
const timestamp = new Date(e.detail.value).getTime();
// 更新设备时间信息
const updatedDevices = [...this.data.selectedSubDevices];
updatedDevices[currentDeviceTimeIndex] = {
...updatedDevices[currentDeviceTimeIndex],
endTime: timestamp,
endTimeDisplay: formattedDateTime,
};
this.setData({
selectedSubDevices: updatedDevices,
deviceEndTimePickerVisible: false,
currentDeviceTimeIndex: -1,
});
this.showMessage(`设备结束时间已设置: ${formattedDateTime}`, "success");
},
/**
* 设备结束时间选择器取消事件
*/
onDeviceEndTimeCancel() {
this.setData({
deviceEndTimePickerVisible: false,
currentDeviceTimeIndex: -1,
});
},
/**
* 删除已选择的设备
*/
removeDevice(e) {
const { index } = e.currentTarget.dataset;
const removedDevice = this.data.selectedSubDevices[index];
const newSelectedDevices = [...this.data.selectedSubDevices];
newSelectedDevices.splice(index, 1);
// 更新设备状态为未选择
const updatedDeviceStatus = { ...this.data.deviceStatus };
updatedDeviceStatus[removedDevice.id].inUse = false;
this.setData({
selectedSubDevices: newSelectedDevices,
deviceStatus: updatedDeviceStatus,
});
this.showMessage(`已删除设备: ${removedDevice.device_name}-${removedDevice.name}`, "warning");
},
/**
* 验证表单数据
*/
validateForm() {
const { surgery_id, surgery_name, patient, surgery_time } = this.data.formData;
const { selectedDoctor } = this.data;
console.log(1, surgery_id);
// 验证手术编号
if (!surgery_id) {
this.showMessage("请填写手术编号", "error");
return false;
}
if (typeof surgery_id !== "string" || surgery_id.trim().length < 3 || surgery_id.trim().length > 50) {
this.showMessage("手术编号格式不正确长度应在3-50个字符之间", "error");
return false;
}
// 验证手术名称
if (!surgery_name) {
this.showMessage("请填写手术名称", "error");
return false;
}
// 验证患者姓名
if (!patient) {
this.showMessage("请填写患者姓名", "error");
return false;
}
// 验证手术时间
if (!surgery_time) {
this.showMessage("请选择手术时间", "error");
return false;
}
// 验证医生选择
if (!selectedDoctor) {
this.showMessage("请选择主刀医生", "error");
return false;
}
return true;
},
/**
* 处理表单提交
*/
async handleSubmit() {
if (!this.validateForm()) {
return;
}
this.setData({ submitting: true });
try {
const { surgery_id, surgery_name, patient, surgery_time } = this.data.formData;
const { selectedDoctor, selectedSubDevices } = this.data;
// 将时间转换为数据库需要的格式YYYY-MM-DD HH:mm:ss
const formattedDateTime = dayjs(surgery_time).format('YYYY-MM-DD HH:mm:ss');
// 构建提交数据,确保与网页端数据结构一致
const submitData = {
surgery_id, // 手术编号
surgery_name, // 手术名称
surgery_time: formattedDateTime, // 手术时间(本地时间格式)
surgery_doctor_id: selectedDoctor.id, // 主刀医生ID
patient, // 患者姓名
sub_device_ids: selectedSubDevices.map((device) => device.id), // 使用设备ID列表
// 添加设备时间信息
device_times: selectedSubDevices.map((device) => ({
sub_device_id: device.id,
start_time: device.startTime ? dayjs(device.startTime).format('YYYY-MM-DD HH:mm:ss') : null,
end_time: device.endTime ? dayjs(device.endTime).format('YYYY-MM-DD HH:mm:ss') : null,
})),
};
let res;
if (this.data.isEditMode) {
// 调用更新手术API
res = await surgeryApi.updateSurgery(this.data.surgeryId, submitData);
this.showMessage("更新手术记录成功", "success");
} else {
// 调用创建手术API
res = await surgeryApi.createSurgery(submitData);
this.showMessage("创建手术记录成功", "success");
}
// 延迟返回上一页,让用户看到成功提示
setTimeout(() => {
// 通过全局变量通知列表页面数据已变更
const pages = getCurrentPages();
if (pages.length > 1) {
const prevPage = pages[pages.length - 2];
// 如果上一个页面是手术列表页,标记数据已变更
if (prevPage.route === 'pages/surgery/index' && prevPage.markDataChanged) {
prevPage.markDataChanged();
}
}
wx.removeStorageSync("surgery_formData")
wx.navigateBack();
}, 1500);
} catch (error) {
// 简化错误处理,使错误信息更简洁
let errorMessage = this.data.isEditMode ? "更新失败,请稍后重试" : "创建失败,请稍后重试";
// 检查error.message直接是否包含错误信息
if (error.message && typeof error.message === "string") {
if (error.message.includes("手术编号已存在") || error.message.includes("该手术编号已存在")) {
errorMessage = `手术编号已重复,请更换`;
} else if (error.message.includes("医生") && error.message.includes("时间")) {
errorMessage = `医生时间冲突,请调整`;
} else {
errorMessage = error.message;
}
}
// 检查error.data.message原有逻辑
else if (error.data && error.data.message) {
if (error.data.message.includes("手术编号已存在") || error.data.message.includes("该手术编号已存在")) {
errorMessage = `手术编号已重复,请更换`;
} else if (error.data.message.includes("医生") && error.data.message.includes("时间")) {
errorMessage = `医生时间冲突,请调整`;
} else {
errorMessage = error.data.message;
}
}
// 检查error.data.msg后端返回的标准格式
else if (error.data && error.data.msg) {
if (error.data.msg.includes("手术编号已存在") || error.data.msg.includes("该手术编号已存在")) {
errorMessage = `手术编号已重复,请更换`;
} else if (error.data.msg.includes("医生") && error.data.msg.includes("时间")) {
errorMessage = `医生时间冲突,请调整`;
} else {
errorMessage = error.data.msg;
}
}
this.showMessage(errorMessage, "error");
} finally {
this.setData({ submitting: false });
}
},
/**
* 显示消息提示
*/
showMessage(message, type = "info") {
// 确保消息显示
// 先尝试使用 t-message 组件
const t = this.selectComponent("#t-message");
if (t && typeof t.show === "function") {
wx.nextTick(() => {
t.show({
message,
type,
duration: 3000,
});
});
} else {
// 如果组件不可用降级使用原生toast
let icon = "none";
if (type === "success") icon = "success";
if (type === "error") icon = "error";
wx.showToast({
title: message,
icon: icon,
duration: 2000,
});
}
},
});

View File

@ -0,0 +1,15 @@
{
"navigationBarTitleText": "创建手术记录",
"usingComponents": {
"t-button": "tdesign-miniprogram/button/button",
"t-input": "tdesign-miniprogram/input/input",
"t-textarea": "tdesign-miniprogram/textarea/textarea",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-message": "tdesign-miniprogram/message/message",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-cascader": "tdesign-miniprogram/cascader/cascader",
"t-calendar": "tdesign-miniprogram/calendar/calendar",
"t-date-time-picker": "tdesign-miniprogram/date-time-picker/date-time-picker",
"t-tree-select": "tdesign-miniprogram/tree-select/tree-select"
}
}

View File

@ -0,0 +1,205 @@
<view class="container">
<t-message id="t-message" />
<!-- 加载状态 -->
<view class="loading-container" wx:if="{{loading}}">
<t-loading theme="circular" size="40rpx" loading />
<text>加载中...</text>
</view>
<!-- 表单内容 -->
<block wx:else>
<view class="form-group">
<view class="form-item">
<text class="label">手术编号 <text class="required">*</text></text>
<t-input
value="{{formData.surgery_id}}"
placeholder="请输入手术编号"
bindchange="onInputChange"
data-field="surgery_id"
maxlength="50"
disabled="{{isEditMode}}"
/>
</view>
<view class="form-item">
<text class="label">手术名称 <text class="required">*</text></text>
<t-input
value="{{formData.surgery_name}}"
placeholder="请输入手术名称"
bindchange="onInputChange"
data-field="surgery_name"
maxlength="100"
/>
</view>
<view class="form-item">
<text class="label">患者姓名 <text class="required">*</text></text>
<t-input
value="{{formData.patient}}"
placeholder="请输入患者姓名"
bindchange="onInputChange"
data-field="patient"
maxlength="50"
/>
</view>
<!-- 手术时间选择 -->
<view class="form-item">
<text class="label">手术时间 <text class="required">*</text></text>
<view class="datetime-picker-wrapper" bindtap="showDateTimePicker">
<t-cell
title="{{formData.surgery_time_display || '请选择手术时间'}}"
arrow
hover
data-mode="formData.surgery_time"
note="{{formData.surgery_time ? '' : '必选'}}"
t-class="picker-cell"
/>
</view>
</view>
<view class="form-item">
<text class="label">主刀医生 <text class="required">*</text></text>
<t-cell
title="{{selectedDoctor ? selectedDoctor.name : '请选择主刀医生'}}"
arrow
hover
note="{{selectedDoctor ? selectedDoctor.department.name : '必选'}}"
bind:click="showDoctorSelector"
t-class="picker-cell"
/>
</view>
<!-- 设备选择部分 - 使用与手术详情页一致的样式 -->
<view class="form-item">
<view class="device-selection-header">
<text>设备列表</text>
<view class="add-device-btn" bind:tap="showDeviceSelector">添加设备</view>
</view>
<!-- 已选择的设备列表 - 新样式 -->
<view class="selected-devices-list" wx:if="{{selectedSubDevices.length > 0}}">
<block wx:for="{{selectedSubDevices}}" wx:key="id">
<view class="device-item">
<view class="device-item-content">
<view class="device-info">
<view class="device-name">{{item.device_name}}-{{item.name}}</view>
</view>
<view class="device-action">
<view class="delete-btn" bind:tap="removeDevice" data-index="{{index}}">删除</view>
</view>
</view>
<!-- 设备时间选择 -->
<view class="device-time-section">
<view class="time-picker-group">
<view class="time-picker-item">
<text class="time-label">开始时间</text>
<view class="time-picker" bindtap="showDeviceStartTimePicker" data-index="{{index}}">
<text class="time-value">{{item.startTimeDisplay || '点击设置'}}</text>
<text class="picker-arrow">></text>
</view>
</view>
<view class="time-picker-item">
<text class="time-label">结束时间</text>
<view class="time-picker" bindtap="showDeviceEndTimePicker" data-index="{{index}}">
<text class="time-value">{{item.endTimeDisplay || '点击设置'}}</text>
<text class="picker-arrow">></text>
</view>
</view>
</view>
</view>
</view>
</block>
</view>
<!-- 无设备提示 -->
<view class="no-devices" wx:else>
<text>暂无选择设备,请点击"添加设备"按钮添加</text>
</view>
</view>
</view>
<view class="submit-container">
<t-button
theme="primary"
size="large"
loading="{{submitting}}"
disabled="{{submitting}}"
bind:tap="handleSubmit"
block>{{isEditMode ? '保存修改' : '创建手术记录'}}</t-button>
</view>
</block>
<!-- 日期时间选择器 -->
<t-date-time-picker
title="选择手术时间"
visible="{{dateTimePickerVisible}}"
mode="second"
format="YYYY-MM-DD HH:mm:ss"
value="{{formData.surgery_time || currentDate}}"
confirm-btn="确认"
cancel-btn="取消"
bind:confirm="onDateTimeConfirm"
bind:cancel="onDateTimeCancel"
bind:pick="onDateTimePick"
bind:change="onDateTimeChange"
auto-close
show-week
/>
<!-- 医生选择器 - 使用级联选择器选项卡风格 -->
<t-cascader
visible="{{doctorSelectorVisible}}"
options="{{doctorCascaderOptions}}"
title="选择主刀医生"
theme="tab"
value="{{selectedDoctorValue}}"
bind:change="onDoctorCascaderChange"
bind:pick="onDoctorCascaderPick"
bind:close="onDoctorCascaderClose"
/>
<!-- 设备选择器 - 使用级联选择器选项卡风格 -->
<t-cascader
visible="{{deviceSelectorVisible}}"
options="{{deviceTree}}"
title="选择使用设备"
theme="tab"
value="{{selectedDeviceValue}}"
bind:change="onDeviceCascaderChange"
bind:pick="onDeviceCascaderPick"
bind:close="onDeviceCascaderClose"
/>
<!-- 设备开始时间选择器 -->
<t-date-time-picker
title="选择设备开始使用时间"
visible="{{deviceStartTimePickerVisible}}"
mode="second"
format="YYYY-MM-DD HH:mm:ss"
value="{{currentDeviceStartTime || currentDate}}"
confirm-btn="确认"
cancel-btn="取消"
bind:confirm="onDeviceStartTimeConfirm"
bind:cancel="onDeviceStartTimeCancel"
auto-close
show-week
/>
<!-- 设备结束时间选择器 -->
<t-date-time-picker
title="选择设备结束使用时间"
visible="{{deviceEndTimePickerVisible}}"
mode="second"
format="YYYY-MM-DD HH:mm:ss"
value="{{currentDeviceEndTime || currentDate}}"
confirm-btn="确认"
cancel-btn="取消"
bind:confirm="onDeviceEndTimeConfirm"
bind:cancel="onDeviceEndTimeCancel"
auto-close
show-week
/>
</view>

View File

@ -0,0 +1,305 @@
/* pages/surgery/create/index.wxss */
.container {
padding: 30rpx;
background-color: #f6f6f6;
min-height: 100vh;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300rpx;
}
.form-group {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.05);
}
.form-item {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
margin-bottom: 16rpx;
color: #333;
}
.required {
color: #e34d59;
}
.picker-cell {
padding: 20rpx !important;
border-radius: 8rpx;
background-color: #f8f8f8 !important;
}
/* 原生选择器样式 */
.picker-view {
padding: 24rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
position: relative;
}
.time-picker {
margin-top: 16rpx;
}
.picker-note {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
padding-left: 24rpx;
}
/* 设备选择相关样式 - 更新 */
.device-selection-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.device-selection-header text {
font-size: 28rpx;
font-weight: bold;
}
.add-device-btn {
color: #06a56c;
font-size: 26rpx;
padding: 10rpx 20rpx;
display: inline-flex;
align-items: center;
justify-content: center;
}
.selected-devices-list {
margin-top: 12rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
padding: 12rpx;
display: block; /* 确保设备列表容器显示 */
}
.device-item {
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 12rpx;
padding: 8rpx;
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05);
display: block; /* 确保设备项显示 */
}
.device-item:last-child {
margin-bottom: 0;
}
.device-item-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
}
.device-info {
flex: 1;
}
.device-name {
font-size: 28rpx;
color: #333;
}
.device-action {
display: flex;
align-items: center;
}
.delete-btn {
padding: 8rpx 20rpx;
background-color: #e34d59;
color: #fff;
border-radius: 4rpx;
font-size: 24rpx;
}
.remove-icon {
padding: 10rpx;
color: #e34d59;
}
.no-devices {
text-align: center;
color: #999;
font-size: 26rpx;
padding: 30rpx 0;
background-color: #f8f8f8;
border-radius: 8rpx;
}
.submit-container {
margin-top: 60rpx;
padding-bottom: 40rpx;
}
/* 年月日时分秒选择器样式 */
.datetime-picker-mask {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.datetime-picker-container {
position: fixed;
bottom: -500rpx;
left: 0;
width: 100%;
background-color: #fff;
border-radius: 24rpx 24rpx 0 0;
z-index: 1001;
transition: all 0.3s ease;
}
.datetime-picker-container.show {
bottom: 0;
}
.datetime-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.datetime-picker-header .title {
font-size: 32rpx;
color: #333;
font-weight: bold;
}
.datetime-picker-header .cancel-btn,
.datetime-picker-header .confirm-btn {
font-size: 28rpx;
padding: 8rpx 10rpx;
}
.datetime-picker-header .cancel-btn {
color: #999;
}
.datetime-picker-header .confirm-btn {
color: #0052d9;
}
.datetime-picker-body {
padding: 20rpx 0;
height: 300rpx;
}
.picker-item {
line-height: 50px;
text-align: center;
}
.time-display {
padding: 20rpx;
background-color: #f8f8f8;
border-radius: 8rpx;
font-size: 28rpx;
color: #333;
}
/* 自定义级联选择器样式 */
:host {
--td-cascader-active-color: #0052d9;
}
/* 设备选择器的自定义样式 */
.custom-picker {
--td-picker-confirm-color: #0052d9;
}
.custom-confirm-btn {
color: #0052d9 !important;
}
.custom-cancel-btn {
color: #999 !important;
}
/* 设备时间选择部分样式 */
.device-time-section {
margin-top: 16rpx;
padding: 16rpx;
background-color: #f8f9fa;
border-radius: 8rpx;
border: 1px solid #e9ecef;
}
.time-picker-group {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.time-picker-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12rpx 16rpx;
background-color: #fff;
border-radius: 6rpx;
border: 1px solid #dee2e6;
}
.time-picker-item .time-label {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.time-picker {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 12rpx;
background-color: #f8f9fa;
border-radius: 4rpx;
border: 1px solid #dee2e6;
min-width: 200rpx;
justify-content: space-between;
}
.time-picker .time-value {
font-size: 24rpx;
color: #333;
flex: 1;
}
.time-picker .picker-arrow {
font-size: 24rpx;
color: #999;
transition: transform 0.2s;
}
.time-picker:active .picker-arrow {
transform: translateX(4rpx);
}

View File

@ -0,0 +1,119 @@
// pages/surgery/detail/index.js
const { surgeryApi } = require("../../../utils/api");
const dayjs = require("../../../miniprogram_npm/dayjs/index");
Page({
/**
* 页面的初始数据
*/
data: {
surgeryId: null, // 手术ID
surgery: null, // 手术数据
loading: true, // 加载状态
loadError: false, // 加载错误状态
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
if (options && options.id) {
const surgeryId = Number(options.id);
this.setData({ surgeryId });
this.fetchSurgeryDetail(surgeryId);
} else {
wx.showToast({
title: '参数错误',
icon: 'error'
});
// 返回上一页
setTimeout(() => {
wx.navigateBack();
}, 1500);
}
},
/**
* 获取手术详情数据
*/
async fetchSurgeryDetail(surgeryId) {
this.setData({
loading: true,
loadError: false
});
try {
// 修正API调用方法名 - 从getSurgeryDetail改为getSurgeryById
const res = await surgeryApi.getSurgeryById(surgeryId);
// 处理手术数据,格式化日期和设备数据
const surgery = res.data;
// 处理医生和科室信息
const hasDoctor = !!surgery.doctor;
const hasDepartment = !!surgery.doctor?.department;
const doctorName = hasDoctor ? surgery.doctor.name : '未知医生';
const departmentName = hasDepartment ? surgery.doctor.department.name : '未知科室';
// 格式化设备数据,将设备名称和子设备名称结合显示
let devices = [];
if (surgery.surgerySubDevices && surgery.surgerySubDevices.length > 0) {
devices = surgery.surgerySubDevices.map(item => {
// 获取主设备名称和子设备名称(通常是编号)
const deviceName = item.subDevice.device.name || '未知设备';
const subDeviceName = item.subDevice.name || '';
// 格式化使用时间
let startTimeDisplay = '未设置';
let endTimeDisplay = '未设置';
if (item.start_time) {
startTimeDisplay = dayjs(item.start_time).format('YYYY-MM-DD HH:mm');
}
if (item.end_time) {
endTimeDisplay = dayjs(item.end_time).format('YYYY-MM-DD HH:mm');
}
return {
id: item.sub_device_id,
surgerySubDeviceId: item.id,
// 组合成"设备名称-子设备号"的格式
displayName: `${deviceName}-${subDeviceName}`,
startTime: item.start_time,
endTime: item.end_time,
startTimeDisplay,
endTimeDisplay
};
});
}
const formattedSurgery = {
...surgery,
formattedTime: dayjs(surgery.surgery_time).format('YYYY年MM月DD日 HH:mm'),
doctorName,
departmentName,
devices
};
this.setData({
surgery: formattedSurgery,
loading: false
});
} catch (error) {
this.setData({
loading: false,
loadError: true
});
wx.showToast({
title: '获取手术详情失败',
icon: 'error'
});
}
},
/**
* 返回上一级页面
*/
goBack() {
wx.navigateBack();
}
})

View File

@ -0,0 +1,11 @@
{
"usingComponents": {
"t-navbar": "tdesign-miniprogram/navbar/navbar",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-cell": "tdesign-miniprogram/cell/cell",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-message": "tdesign-miniprogram/message/message"
},
"navigationStyle": "custom"
}

View File

@ -0,0 +1,85 @@
<view class="detail-container">
<t-navbar title="手术详情" left-arrow bind:go-back="goBack" />
<block wx:if="{{loading}}">
<!-- 加载中骨架屏 -->
<view class="skeleton-container">
<t-skeleton theme="paragraph" loading></t-skeleton>
</view>
</block>
<block wx:elif="{{loadError}}">
<!-- 加载错误提示 -->
<view class="error-container">
<t-empty icon="error-circle" description="加载失败,请返回重试" />
</view>
</block>
<block wx:elif="{{surgery}}">
<view class="detail-card">
<!-- 手术基本信息 -->
<view class="info-section">
<view class="info-header">手术信息</view>
<view class="info-content">
<view class="info-item">
<text class="label">手术编号</text>
<text class="value">{{surgery.surgery_id}}</text>
</view>
<view class="info-item">
<text class="label">手术名称</text>
<text class="value">{{surgery.surgery_name}}</text>
</view>
<view class="info-item">
<text class="label">患者姓名</text>
<text class="value">{{surgery.patient}}</text>
</view>
<view class="info-item">
<text class="label">手术时间</text>
<text class="value">{{surgery.formattedTime}}</text>
</view>
<view class="info-item">
<text class="label">主刀医生</text>
<text class="value doctor">{{surgery.doctorName}}</text>
</view>
<view class="info-item">
<text class="label">所属科室</text>
<text class="value department">{{surgery.departmentName}}</text>
</view>
</view>
</view>
<!-- 设备信息部分,修改为更简洁的列表 -->
<view class="device-section">
<view class="info-header">使用设备 ({{surgery.devices.length}})</view>
<view class="device-list">
<block wx:if="{{surgery.devices && surgery.devices.length > 0}}">
<view class="device-card" wx:for="{{surgery.devices}}" wx:key="id">
<view class="device-header">
<text class="device-name">{{item.displayName}}</text>
</view>
<view class="device-time-info">
<view class="time-item">
<text class="time-label">开始时间:</text>
<text class="time-value">{{item.startTimeDisplay}}</text>
</view>
<view class="time-item">
<text class="time-label">结束时间:</text>
<text class="time-value">{{item.endTimeDisplay}}</text>
</view>
</view>
</view>
</block>
<block wx:else>
<view class="no-device">
<t-empty icon="info-circle" description="无使用设备记录" />
</view>
</block>
</view>
</view>
</view>
</block>
<block wx:else>
<!-- 数据为空的提示 -->
<view class="empty-container">
<t-empty icon="info-circle-filled" description="无法获取手术详情" />
</view>
</block>
</view>

View File

@ -0,0 +1,132 @@
/* pages/surgery/detail/index.wxss */
page {
background-color: #f7f8fa;
}
.detail-container {
padding-bottom: 40rpx;
}
.skeleton-container {
padding: 24rpx;
}
.detail-card {
margin: 24rpx;
background-color: #ffffff;
border-radius: 8rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
/* 信息区块样式 */
.info-section {
padding: 24rpx;
border-bottom: 1px solid #f2f2f2;
}
.device-section {
padding: 24rpx;
}
.info-header {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 24rpx;
}
.info-content {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.info-item {
display: flex;
align-items: center;
line-height: 1.5;
}
.label {
width: 160rpx;
font-size: 28rpx;
color: #888;
flex-shrink: 0;
}
.value {
font-size: 28rpx;
color: #333;
flex: 1;
}
/* 医生和科室标签样式 */
.doctor {
color: #0052d9;
}
.department {
color: #06a56c;
}
/* 新的设备列表样式 */
.device-list {
padding: 0 10rpx;
}
.device-card {
background-color: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8rpx;
padding: 20rpx;
margin: 12rpx 0;
}
.device-header {
margin-bottom: 16rpx;
}
.device-name {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
.device-time-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.time-item {
display: flex;
align-items: center;
}
.time-label {
font-size: 24rpx;
color: #888;
width: 120rpx;
flex-shrink: 0;
}
.time-value {
font-size: 24rpx;
color: #333;
flex: 1;
}
.no-device {
padding: 40rpx 0;
}
/* 错误和空状态容器 */
.error-container,
.empty-container {
padding: 100rpx 24rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}

558
pages/surgery/index.js Normal file
View File

@ -0,0 +1,558 @@
// pages/surgery/index.js
const { surgeryApi } = require("../../utils/api");
const dayjs = require("../../miniprogram_npm/dayjs/index");
const Toast = require("../../miniprogram_npm/tdesign-miniprogram/toast/index");
Page({
/**
* 页面的初始数据
*/
data: {
surgeries: [], // 手术列表数据
surgeriesList: [],
// 分页参数
page: 1,
pageSize: 10,
total: 0,
// 状态控制
loading: true, // 初始加载状态
refreshing: false, // 下拉刷新状态
loadingMore: false, // 加载更多状态
loadError: false, // 加载错误状态
noMoreData: false, // 没有更多数据状态
isFinish: false, // 是否选中未完成
confirmDialogVisible: false, // 确认删除对话框可见性
currentSurgeryId: null, // 当前操作的手术ID
// 数据变更状态管理
dataHasChanged: false, // 标记数据是否已变更(从编辑/创建页面返回时)
lastShowTime: 0, // 上次显示页面的时间戳
showTimeout: null, // 防抖定时器
// TDesign 组件配置
loadingProps: {
theme: 'circular',
size: '40rpx',
layout: 'horizontal'
},
loadingTexts: [
'下拉刷新',
'松手刷新',
'正在刷新...',
'刷新完成'
]
// 不再在这里全局定义swipeButtons而是为每个手术项动态生成
},
/**
* 生命周期函数--监听页面加载
*/
onLoad() {
this.loadSurgeries();
},
/**
* 生命周期函数--监听页面显示
*/
onShow() {
const currentTime = Date.now();
// 清除之前的定时器(防抖)
if (this.data.showTimeout) {
clearTimeout(this.data.showTimeout);
}
// 检查是否需要刷新数据
const shouldRefresh = this.shouldRefreshData(currentTime);
if (shouldRefresh) {
// 设置新的定时器延迟500ms执行刷新避免快速切换页面导致的重复请求
const timeoutId = setTimeout(() => {
this.performSmartRefresh();
}, 500);
this.setData({
showTimeout: timeoutId,
lastShowTime: currentTime,
dataHasChanged: false
});
} else {
// 更新最后显示时间
this.setData({ lastShowTime: currentTime });
}
},
/**
* 点击未完成
*/
clickunfinished ({detail}) {
this.setData({isFinish: detail.checked})
this.filterSurgeriesList()
},
/**
* 过滤手术未完成
*/
filterSurgeriesList(arr = []) {
if (arr.length > 0) this.data.surgeriesList.push(...arr)
let list = []
if (this.data.isFinish) {
list = this.data.surgeries.filter((item) => {
if (item.surgerySubDevices && item.surgerySubDevices.length > 0){
if (item.surgerySubDevices.some(subDevice => !subDevice.end_time)) {
return item
}
}
})
this.setData({
surgeries: list
})
} else {
this.setData({
surgeries: this.data.surgeriesList
})
}
},
/**
* 判断是否需要刷新数据
*/
shouldRefreshData(currentTime) {
// 如果正在加载或刷新,则不执行
if (this.data.loading || this.data.refreshing || this.data.loadingMore) {
return false;
}
// 如果数据已变更(从编辑页面返回的标记),则需要刷新
if (this.data.dataHasChanged) {
return true;
}
// 如果还没有加载过数据,则需要加载
if (this.data.surgeries.length === 0 && !this.data.loadError) {
return true;
}
// 如果距离上次显示超过5分钟考虑刷新防止数据过期
const timeSinceLastShow = currentTime - this.data.lastShowTime;
if (timeSinceLastShow > 5 * 60 * 1000) { // 5分钟
return true;
}
return false;
},
/**
* 执行智能刷新策略
*/
async performSmartRefresh() {
try {
// 1. 首先尝试增量更新(只刷新第一页,检查是否有新数据)
const freshData = await this.fetchSurgeriesData(1, this.data.pageSize);
const freshFirstPage = freshData.formattedSurgeries;
// 2. 比较数据是否有变化
const hasChanges = this.detectChanges(this.data.surgeries, freshFirstPage);
if (hasChanges) {
console.log('📋 检测到数据变化,执行增量更新');
await this.performIncrementalUpdate(freshData);
} else {
console.log('📋 数据无变化,保持当前状态');
}
} catch (error) {
console.error('📋 智能刷新失败,回退到完整刷新:', error);
// 如果智能刷新失败,回退到完整的刷新
this.loadSurgeries();
}
},
/**
* 检测数据是否有变化
*/
detectChanges(currentData, freshData) {
// 如果数据数量不同,肯定有变化
if (currentData.length !== freshData.length) {
return true;
}
// 如果当前没有数据,而新数据有,则有变化
if (currentData.length === 0 && freshData.length > 0) {
return true;
}
// 比较前几个项目的ID和时间戳
const compareCount = Math.min(currentData.length, freshData.length, 3);
for (let i = 0; i < compareCount; i++) {
const currentItem = currentData[i];
const freshItem = freshData[i];
// 比较ID
if (currentItem.id !== freshItem.id) {
return true;
}
// 比较更新时间(如果有的话)
if (currentItem.updated_at !== freshItem.updated_at) {
return true;
}
// 比较手术时间
if (currentItem.surgery_time !== freshItem.surgery_time) {
return true;
}
}
return false;
},
/**
* 执行增量更新
*/
async performIncrementalUpdate(freshFirstPage) {
// 只在第1页时自动更新其他页面不进行任何操作
if (this.data.page === 1) {
this.setData({
surgeries: freshFirstPage.formattedSurgeries,
total: freshFirstPage.total,
noMoreData: freshFirstPage.formattedSurgeries.length < this.data.pageSize
});
// 简单的数据更新提示
wx.showToast({
title: '数据已更新',
icon: 'success',
duration: 1000
});
}
// 如果用户在第2页+,不做任何操作,让用户自己选择是否下拉刷新
},
/**
* 设置数据变更标记从其他页面调用
*/
markDataChanged() {
this.setData({ dataHasChanged: true });
},
/**
* 初始加载手术数据
*/
async loadSurgeries() {
this.setData({
loading: true,
loadError: false,
page: 1,
noMoreData: false
});
try {
const res = await this.fetchSurgeriesData(1, this.data.pageSize);
this.setData({
surgeries: res.formattedSurgeries,
surgeriesList: res.formattedSurgeries,
total: res.total,
page: 1,
noMoreData: res.formattedSurgeries.length < this.data.pageSize,
loading: false
});
} catch (error) {
this.setData({
loading: false,
loadError: true,
});
wx.showToast({
title: "获取手术列表失败",
icon: "error",
});
}
},
/**
* 下拉刷新
*/
async onRefresh() {
if (this.data.refreshing || this.data.loadingMore) return;
this.setData({
refreshing: true,
page: 1,
noMoreData: false
});
try {
const res = await this.fetchSurgeriesData(1, this.data.pageSize);
this.setData({
surgeries: res.formattedSurgeries,
total: res.total,
page: 1,
noMoreData: res.formattedSurgeries.length < this.data.pageSize
});
wx.showToast({
title: '刷新成功',
icon: 'success',
duration: 1500
});
} catch (error) {
wx.showToast({
title: '刷新失败',
icon: 'error'
});
} finally {
this.setData({ refreshing: false });
}
},
/**
* 滑动底部加载更多
*/
async onScrollToLower() {
// 如果正在加载或没有更多数据,则返回
if (this.data.loadingMore || this.data.noMoreData || this.data.refreshing) {
return;
}
this.setData({
loadingMore: true,
page: this.data.page + 1
});
try {
const res = await this.fetchSurgeriesData(this.data.page, this.data.pageSize);
this.filterSurgeriesList(res.formattedSurgeries)
// 合并数据
this.setData({
// surgeries: this.data.surgeries.concat(res.formattedSurgeries),
total: res.total,
noMoreData: res.formattedSurgeries.length < this.data.pageSize
});
} catch (error) {
// 恢复页码
this.setData({
page: this.data.page - 1
});
wx.showToast({
title: '加载更多失败',
icon: 'error'
});
} finally {
this.setData({ loadingMore: false });
}
},
/**
* 获取手术数据的核心方法
*/
async fetchSurgeriesData(page, pageSize) {
const res = await surgeryApi.getSurgeries({
page: page,
pageSize: pageSize
});
// 处理手术数据,格式化日期并确保医生和科室数据存在
// 后端返回的是分页格式:{list: [...], total: ...}
const surgeryList = res.data.list || res.data || [];
const formattedSurgeries = surgeryList.map((surgery) => {
// 诊断医生信息缺失问题
const hasDoctor = !!surgery.doctor;
const hasDepartment = !!surgery.doctor?.department;
// 确保医生和科室信息存在并设置默认值
const doctorName = hasDoctor ? surgery.doctor.name : "未知医生";
const departmentName = hasDepartment ? surgery.doctor.department.name : "未知科室";
// 计算使用的设备数量
const deviceCount = surgery.surgerySubDevices ? surgery.surgerySubDevices.length : 0;
// 检查是否有未设置结束时间的设备
let hasUnfinishedDevice = false;
if (surgery.surgerySubDevices && surgery.surgerySubDevices.length > 0) {
hasUnfinishedDevice = surgery.surgerySubDevices.some(subDevice => !subDevice.end_time);
}
// 为每个手术记录添加专属的侧滑按钮配置
const surgerySwipeButtons = [
{
text: "编辑",
style: "background-color: #0052d9; color: white; width: 120rpx; height: 100%; display: flex; align-items: center; justify-content: center;",
data: { id: surgery.id, type: "edit" },
},
{
text: "删除",
style: "background-color: #e34d59; color: white; width: 120rpx; height: 100%; display: flex; align-items: center; justify-content: center;",
data: { id: surgery.id, type: "delete" },
},
];
return {
...surgery,
formattedTime: dayjs(surgery.surgery_time).format("YYYY年MM月DD日 HH:mm"),
// 添加独立的属性,确保即使嵌套对象不存在也能显示
doctorName: doctorName,
departmentName: departmentName,
// 添加设备数量
deviceCount: deviceCount,
// 添加样式状态标记
hasUnfinishedDevice: hasUnfinishedDevice,
// 添加侧滑按钮配置
swipeButtons: surgerySwipeButtons,
};
});
return {
formattedSurgeries,
total: res.data.total || surgeryList.length
};
},
/**
* 查看手术详情
*/
viewSurgeryDetail(e) {
const surgeryId = e.currentTarget.dataset.id;
// 跳转到手术详情页
wx.navigateTo({
url: `/pages/surgery/detail/index?id=${surgeryId}`,
});
},
/**
* 侧滑单元格点击事件处理
*/
onSwipeCellClick(e) {
const { index } = e.detail;
const btnData = e.detail.data;
if (!btnData || !btnData.id) {
Toast({
message: "操作失败,请重试",
theme: "error",
});
return;
}
const surgeryId = btnData.id;
const actionType = btnData.type;
// 查找对应的手术记录数据
const surgery = this.data.surgeries.find((item) => item.id === surgeryId);
if (!surgery) {
Toast({
message: "找不到对应的手术记录",
theme: "error",
});
return;
}
// 根据操作类型执行相应的动作
if (actionType === "edit" || index === 0) {
// 编辑操作
this.handleEdit(surgery);
} else if (actionType === "delete" || index === 1) {
// 删除操作
this.handleDelete(surgery);
}
},
/**
* 处理编辑操作
*/
handleEdit(surgery) {
// 跳转到编辑页面,实际上是复用创建页面
wx.navigateTo({
url: `/pages/surgery/create/index?id=${surgery.id}&mode=edit`,
success: () => {
},
fail: (err) => {
Toast({
message: "页面跳转失败",
theme: "error",
});
},
});
},
/**
* 处理删除操作
*/
handleDelete(surgery) {
// 显示确认对话框
this.setData({
currentSurgeryId: surgery.id,
confirmDialogVisible: true,
});
},
/**
* 确认删除
*/
async confirmDelete() {
const surgeryId = this.data.currentSurgeryId;
try {
await surgeryApi.deleteSurgery(surgeryId);
// 删除成功,刷新列表
wx.showToast({
title: "删除成功",
icon: "success",
});
// 重新加载数据
this.loadSurgeries();
} catch (error) {
wx.showToast({
title: "删除失败",
icon: "error",
});
} finally {
// 关闭对话框
this.setData({
confirmDialogVisible: false,
currentSurgeryId: null,
});
}
},
/**
* 取消删除
*/
cancelDelete() {
this.setData({
confirmDialogVisible: false,
currentSurgeryId: null,
});
},
/**
* 浮动按钮点击事件处理函数
*/
handleClick() {
// 跳转到手术创建页面
wx.navigateTo({
url: "/pages/surgery/create/index",
success: () => {
},
fail: (err) => {
wx.showToast({
title: "页面跳转失败",
icon: "error",
});
},
});
},
/**
* 页面卸载时清理资源
*/
onUnload() {
// 清除定时器,避免内存泄漏
if (this.data.showTimeout) {
clearTimeout(this.data.showTimeout);
}
},
});

23
pages/surgery/index.json Normal file
View File

@ -0,0 +1,23 @@
{
"usingComponents": {
"t-cell": "tdesign-miniprogram/cell/cell",
"t-cell-group": "tdesign-miniprogram/cell-group/cell-group",
"t-skeleton": "tdesign-miniprogram/skeleton/skeleton",
"t-loading": "tdesign-miniprogram/loading/loading",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-divider": "tdesign-miniprogram/divider/divider",
"t-tag": "tdesign-miniprogram/tag/tag",
"t-image": "tdesign-miniprogram/image/image",
"t-toast": "tdesign-miniprogram/toast/toast",
"t-message": "tdesign-miniprogram/message/message",
"t-fab": "tdesign-miniprogram/fab/fab",
"t-swipe-cell": "tdesign-miniprogram/swipe-cell/swipe-cell",
"t-dialog": "tdesign-miniprogram/dialog/dialog",
"t-pull-down-refresh": "tdesign-miniprogram/pull-down-refresh/pull-down-refresh",
"t-search": "tdesign-miniprogram/search/search",
"t-radio": "tdesign-miniprogram/radio/radio"
},
"navigationBarTitleText": "手术",
"enablePullDownRefresh": true,
"backgroundColor": "#f7f8fa"
}

102
pages/surgery/index.wxml Normal file
View File

@ -0,0 +1,102 @@
<t-pull-down-refresh
value="{{refreshing}}"
loadingProps="{{loadingProps}}"
loadingTexts="{{loadingTexts}}"
bind:refresh="onRefresh"
bind:scrolltolower="onScrollToLower"
maxBarHeight="200"
loadingBarHeight="80"
lowerThreshold="100"
enable-back-to-top="{{true}}"
>
<view class="surgery-searchbar">
<view class="surgery-radio">
<t-radio default-checked="{{isFinish}}" allow-uncheck icon="line" label="未完成" bind:change="clickunfinished"/>
</view>
<view class="surgery-search"><t-search placeholder="请输入患者ID" /></view>
</view>
<view class="surgery-container">
<!-- 初始加载骨架屏 -->
<block wx:if="{{loading && surgeries.length === 0}}">
<view wx:for="{{5}}" wx:key="index" class="skeleton-item">
<t-skeleton theme="paragraph" loading></t-skeleton>
</view>
</block>
<!-- 加载错误提示 -->
<block wx:elif="{{loadError}}">
<view class="error-container">
<t-empty icon="error-circle" description="加载失败,请下拉刷新重试" />
</view>
</block>
<!-- 手术列表 -->
<block wx:elif="{{surgeries && surgeries.length > 0}}">
<view class="surgery-list">
<block wx:for="{{surgeries}}" wx:key="id">
<t-swipe-cell class="swipe-cell-container" right="{{item.swipeButtons}}" bind:click="onSwipeCellClick">
<view class="surgery-card {{item.hasUnfinishedDevice ? 'surgery-card--unfinished' : ''}}" bindtap="viewSurgeryDetail" data-id="{{item.id}}">
<view class="surgery-title">
{{item.surgery_name}}
<view wx:if="{{item.hasUnfinishedDevice}}" class="unfinished-indicator">
<text class="unfinished-text">未设置结束时间</text>
</view>
</view>
<view class="surgery-info">
<view class="info-item patient">
<text>患者: {{item.patient}}</text>
</view>
<view class="info-item time">
<text>时间: {{item.formattedTime}}</text>
</view>
<view class="info-item doctor-dept">
<text class="doctor">{{item.doctorName}}</text>
<text class="dept">{{item.departmentName}}</text>
</view>
<!-- 添加设备数量显示 -->
<view class="info-item device-count">
<text class="count">使用设备: {{item.deviceCount}}个</text>
</view>
</view>
<view class="surgery-id-tag">{{item.surgery_id}}</view>
</view>
</t-swipe-cell>
</block>
</view>
<!-- 加载更多状态 -->
<view wx:if="{{loadingMore}}" class="loading-more">
<t-loading text="加载更多..." size="40rpx" theme="circular" />
</view>
<!-- 没有更多数据 -->
<view wx:if="{{noMoreData && surgeries.length > 0}}" class="no-more">
<view class="no-more-line"></view>
<text class="no-more-text">没有更多手术记录了</text>
<view class="no-more-line"></view>
</view>
</block>
<!-- 空数据提示 -->
<block wx:else>
<view class="empty-container">
<t-empty icon="info-circle-filled" description="暂无手术记录" />
</view>
</block>
<t-fab icon="add" bind:click="handleClick" aria-label="增加"></t-fab>
</view>
</t-pull-down-refresh>
<!-- Toast 消息提示 -->
<t-toast id="t-toast" />
<!-- 确认删除的对话框 -->
<t-dialog
visible="{{confirmDialogVisible}}"
title="确认删除"
content="确定要删除此手术记录吗?此操作不可撤销。"
confirm-btn="删除"
cancel-btn="取消"
bind:confirm="confirmDelete"
bind:cancel="cancelDelete"
/>

240
pages/surgery/index.wxss Normal file
View File

@ -0,0 +1,240 @@
/* pages/surgery/index.wxss */
page {
background-color: #f7f8fa;
}
.surgery-searchbar {
position: fixed;
display: flex;
z-index: 10;
width: 100%;
background: white;
padding: 0 10px;
gap: 0 10px;
align-items: center;
box-sizing: border-box;
top: 0;
}
.surgery-radio {
border-radius: 10px;
}
.surgery-search {
flex: 1;
}
.surgery-container {
padding: 24rpx 0;
margin-top: 50px;
}
/* 骨架屏样式 */
.skeleton-item {
background-color: #fff;
border-radius: 8rpx;
padding: 24rpx;
margin: 16rpx 24rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
/* 手术列表样式 */
.surgery-list {
padding: 0 24rpx;
}
/* 滑动单元格容器 */
.swipe-cell-container {
margin-bottom: 16rpx;
border-radius: 8rpx;
overflow: hidden;
display: block;
}
.surgery-card {
background-color: #ffffff;
border-radius: 8rpx;
padding: 24rpx;
position: relative;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
width: 100%;
box-sizing: border-box;
}
/* 有未完成设备的卡片样式 - 淡黄色背景 */
.surgery-card--unfinished {
background-color: #fff8e6;
border-left: 6rpx solid #faad14;
box-shadow: 0 2rpx 12rpx rgba(250, 173, 20, 0.15);
}
/* 淡黄色卡片内的文本颜色调整 */
.surgery-card--unfinished .surgery-title {
color: #8b6914;
}
.surgery-card--unfinished .info-item {
color: #666;
}
.surgery-card--unfinished .doctor {
background-color: rgba(250, 173, 20, 0.15);
color: #8b6914;
}
.surgery-card--unfinished .dept {
background-color: rgba(250, 173, 20, 0.1);
color: #8b6914;
}
.surgery-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
margin-bottom: 16rpx;
padding-right: 100rpx;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
}
/* 未完成设备状态指示器 */
.unfinished-indicator {
margin-left: 16rpx;
flex-shrink: 0;
}
.unfinished-text {
font-size: 22rpx;
color: #faad14;
background-color: rgba(250, 173, 20, 0.1);
padding: 4rpx 12rpx;
border-radius: 12rpx;
border: 1rpx solid rgba(250, 173, 20, 0.3);
font-weight: normal;
}
.surgery-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.info-item {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
.doctor-dept {
margin-top: 8rpx;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 16rpx;
}
.doctor {
font-size: 28rpx;
color: #0052d9;
background-color: #eef4ff;
padding: 4rpx 12rpx;
border-radius: 4rpx;
display: inline-block;
}
.dept {
font-size: 28rpx;
color: #06a56c;
background-color: #e8f6f1;
padding: 4rpx 12rpx;
border-radius: 4rpx;
display: inline-block;
}
/* 设备数量样式 */
.device-count {
margin-top: 8rpx;
}
.device-count .count {
font-size: 28rpx;
color: #e54d42;
background-color: #feecea;
padding: 4rpx 12rpx;
border-radius: 4rpx;
display: inline-block;
}
.surgery-id-tag {
position: absolute;
top: 24rpx;
right: 24rpx;
font-size: 24rpx;
color: #999;
}
/* 空状态和错误容器 */
.empty-container,
.error-container {
padding: 100rpx 24rpx;
}
/* 侧滑按钮样式 - 修改部分 */
.swipe-cell-btn {
height: 100% !important;
width: 120rpx !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 28rpx !important;
}
.edit-btn {
background-color: #0052d9 !important;
color: white !important;
}
.delete-btn {
background-color: #e34d59 !important;
color: white !important;
}
/* 加载更多样式 */
.loading-more {
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 0;
}
/* 没有更多数据样式 */
.no-more {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 24rpx;
margin: 20rpx 0;
}
.no-more-line {
flex: 1;
height: 1px;
background-color: #e0e0e0;
}
.no-more-text {
margin: 0 24rpx;
font-size: 24rpx;
color: #999;
}
/* TDesign pull-down-refresh 容器调整 */
.surgery-container {
min-height: 100vh;
padding: 24rpx 0;
}
/* 修改TDesign原生样式确保按钮高度与卡片一致 */
.surgery-list .t-swipe-cell__right {
height: 100% !important;
display: flex !important;
align-items: stretch !important;
}

38
pnpm-lock.yaml generated Normal file
View File

@ -0,0 +1,38 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
dayjs:
specifier: ^1.11.13
version: 1.11.13
tdesign-miniprogram:
specifier: ^1.8.8
version: 1.8.8
packages:
dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
tdesign-miniprogram@1.8.8:
resolution: {integrity: sha512-3Ci/2YJYUDjP04MQHanmqPwQsoIcOORcVMO8aGbnMDkMgDNiJi5hZc0z9AwpiHsHjBAiKhWIdUo2MRacgl4vcg==}
tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
snapshots:
dayjs@1.11.13: {}
tdesign-miniprogram@1.8.8:
dependencies:
dayjs: 1.11.13
tinycolor2: 1.6.0
tinycolor2@1.6.0: {}

41
project.config.json Normal file
View File

@ -0,0 +1,41 @@
{
"appid": "wx4b6d52b515feec0a",
"compileType": "miniprogram",
"libVersion": "3.8.0",
"packOptions": {
"ignore": [],
"include": []
},
"setting": {
"coverView": true,
"es6": true,
"postcss": true,
"minified": true,
"enhance": true,
"showShadowRootInWxmlPanel": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"disableUseStrict": false,
"useCompilerPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true
},
"condition": {},
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"simulatorPluginLibVersion": {}
}

View File

@ -0,0 +1,24 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "his-weapp",
"setting": {
"compileHotReLoad": true,
"urlCheck": false,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"useApiHostProcess": true,
"showShadowRootInWxmlPanel": true,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true,
"bigPackageSizeSupport": false
},
"libVersion": "3.8.0",
"condition": {}
}

7
sitemap.json Normal file
View File

@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}

147
utils/api.js Normal file
View File

@ -0,0 +1,147 @@
// utils/api.js
const { get, post, put, delete: del } = require("./request");
// 用户相关API
const userApi = {
// 普通用户登录
login: (username, password) => post("/api/user/login", { username, password }),
// 微信小程序一键登录
miniLogin: (code, nickname = null) => post("/api/user/mini-login", { code, nickname }),
// 获取当前用户信息
getCurrentUser: () => get("/api/user/profile", true),
// 更新用户信息
updateProfile: (data) => post("/api/user/update-profile", data, true),
// 修改密码
changePassword: (oldPassword, newPassword, confirmPassword) =>
post("/api/user/change-password", {
old_password: oldPassword,
new_password: newPassword,
confirm_password: confirmPassword
}, true),
// 刷新token
refreshToken: () => post("/api/user/refresh-token", {}, true),
};
// 医生相关API
const doctorApi = {
// 获取所有医生列表
getDoctors: () => get("/api/doctors", true),
// 获取单个医生详情
getDoctorById: (id) => get(`/api/doctors/${id}`, true),
};
// 手术相关API
const surgeryApi = {
// 获取所有手术列表,支持分页参数
getSurgeries: (params = {}) => get("/api/surgeries", params, true),
// 获取单个手术详情
getSurgeryById: (id) => get(`/api/surgeries/${id}`, true),
// 创建新手术记录
createSurgery: (data) => post("/api/surgeries", data, true),
// 修改手术记录
updateSurgery: (id, data) => put(`/api/surgeries/${id}`, data, true),
// 删除手术记录
deleteSurgery: (id) => del(`/api/surgeries/${id}`, true),
};
// 设备相关API
const deviceApi = {
// 获取所有设备列表
getDevices: (params = {}) => get("/api/devices", params, true),
// 获取单个设备详情
getDeviceById: (id) => get(`/api/devices/${id}`, true),
// 创建新设备
createDevice: (data) => post("/api/devices", data, true),
// 更新设备
updateDevice: (id, data) => put(`/api/devices/${id}`, data, true),
// 删除设备
deleteDevice: (id) => del(`/api/devices/${id}`, true),
// 获取所有子设备列表
getAllSubDevices: () => get("/api/subdevices", true),
// 获取单个子设备详情
getSubDeviceById: (id) => get(`/api/subdevices/${id}`, true),
// 获取指定设备下的所有子设备
getSubDevicesByDeviceId: (deviceId, params) => get(`/api/subdevices/device/${deviceId}`, true, params),
// 获取所有子设备列表
getSubDevices: () => get("/api/subdevices", true),
// 创建子设备
createSubDevice: (data) => post("/api/subdevices", data, true),
// 更新子设备
updateSubDevice: (id, data) => put(`/api/subdevices/${id}`, data, true),
// 删除子设备
deleteSubDevice: (id) => del(`/api/subdevices/${id}`, true),
// 获取设备状态
getDeviceStatus: () => get("/api/device_status", false),
};
// 设备保养相关API
const deviceMaintenanceApi = {
// 获取保养记录列表
getMaintenanceList: (params = {}) => get("/api/device-maintenance", params, true),
// 获取单个保养记录详情
getMaintenance: (id) => get(`/api/device-maintenance/${id}`, true),
// 获取指定子设备的保养记录
getMaintenanceBySubDevice: (params = {}) => get("/api/device-maintenance/sub-device/:sub_device_id", params, true),
// 获取保养统计信息
getMaintenanceStats: (params = {}) => get("/api/device-maintenance/stats", params, true),
// 创建新保养记录
createMaintenance: (data) => post("/api/device-maintenance", data, true),
// 修改保养记录
updateMaintenance: (id, data) => put(`/api/device-maintenance/${id}`, data, true),
// 删除保养记录
deleteMaintenance: (id) => del(`/api/device-maintenance/${id}`, true),
// 完成保养记录
completeMaintenance: (id) => post(`/api/device-maintenance/${id}/complete`, {}, true),
// 取消保养记录
cancelMaintenance: (id) => post(`/api/device-maintenance/${id}/cancel`, {}, true),
// 兼容旧接口
getMaintenanceRecords: () => get("/api/device-maintenance", true),
getMaintenanceById: (id) => get(`/api/device-maintenance/${id}`, true),
getMaintenanceBySubDeviceId: (subDeviceId) => get(`/api/device-maintenance/sub-device/${subDeviceId}`, true),
};
// 字典相关API
const dictionaryApi = {
// 根据类型获取字典列表
getDictionaryByType: (type) => get(`/api/dictionaries/type/${type}`, true),
};
module.exports = {
userApi,
doctorApi,
surgeryApi,
deviceApi,
deviceMaintenanceApi,
dictionaryApi,
};

189
utils/auth.js Normal file
View File

@ -0,0 +1,189 @@
// utils/auth.js
const { userApi } = require("./api");
/**
* 认证相关工具函数
*/
class AuthUtil {
/**
* 检查token是否存在
*/
static hasToken() {
const token = wx.getStorageSync("token");
return !!token;
}
/**
* 获取存储的token
*/
static getToken() {
const token = wx.getStorageSync("token");
return token;
}
/**
* 设置token
*/
static setToken(token) {
wx.setStorageSync("token", token);
}
/**
* 清除认证信息
*/
static clearAuth() {
// 停止token自动刷新
this.stopTokenRefresh();
wx.removeStorageSync("token");
wx.removeStorageSync("userInfo");
// 清除全局状态
const app = getApp();
if (app && app.globalData) {
app.globalData.userInfo = null;
app.globalData.isLoggedIn = false;
}
}
/**
* 保存用户信息
*/
static setUserInfo(userInfo) {
wx.setStorageSync("userInfo", userInfo);
// 更新全局状态
const app = getApp();
if (app && app.globalData) {
app.globalData.userInfo = userInfo;
app.globalData.isLoggedIn = true;
}
}
/**
* 获取用户信息
*/
static getUserInfo() {
const userInfo = wx.getStorageSync("userInfo");
return userInfo;
}
/**
* 刷新token
*/
static async refreshToken() {
try {
const res = await userApi.refreshToken();
if ((res.code === 1 || res.code === 200) && res.data && res.data.token) {
// 保存新token
this.setToken(res.data.token);
// 如果返回了用户信息,更新用户信息
if (res.data.user) {
this.setUserInfo(res.data.user);
}
return true;
} else {
return false;
}
} catch (err) {
return false;
}
}
/**
* 验证当前用户状态
*/
static async validateUser() {
try {
const res = await userApi.getCurrentUser();
if ((res.code === 1 || res.code === 200) && res.data && res.data.user) {
// 更新用户信息
this.setUserInfo(res.data.user);
return true;
} else {
// 用户验证失败尝试刷新token
const refreshed = await this.refreshToken();
if (refreshed) {
// 刷新成功,重新验证
return await this.validateUser();
} else {
// 刷新失败,清除认证信息
this.clearAuth();
return false;
}
}
} catch (err) {
// 如果是401错误尝试刷新token
if (err.message && err.message.includes("401")) {
const refreshed = await this.refreshToken();
if (refreshed) {
try {
return await this.validateUser();
} catch (e) {
this.clearAuth();
return false;
}
} else {
this.clearAuth();
return false;
}
} else {
this.clearAuth();
return false;
}
}
}
/**
* 启动token自动刷新机制
*/
static startTokenRefresh() {
// 先停止之前的定时器
this.stopTokenRefresh();
// 设置2天后自动刷新tokentoken有效期3天
const refreshInterval = 2 * 24 * 60 * 60 * 1000; // 2天
const refreshTimer = setTimeout(async () => {
if (this.hasToken()) {
const success = await this.refreshToken();
if (success) {
// 继续设置下一次刷新
this.startTokenRefresh();
} else {
// 清除存储的定时器标记
wx.removeStorageSync("tokenRefreshTimer");
}
}
}, refreshInterval);
// 标记定时器已启动不存储实际ID因为小程序环境限制
wx.setStorageSync("tokenRefreshTimer", Date.now());
// 将定时器ID存储在类的静态属性中
this._refreshTimer = refreshTimer;
}
/**
* 停止token自动刷新
*/
static stopTokenRefresh() {
// 清除实际的定时器
if (this._refreshTimer) {
clearTimeout(this._refreshTimer);
this._refreshTimer = null;
}
// 清除存储的标记
wx.removeStorageSync("tokenRefreshTimer");
}
}
module.exports = AuthUtil;

153
utils/request.js Normal file
View File

@ -0,0 +1,153 @@
// utils/request.js
// 配置API基础URL
// const BASE_URL = "https://api.gzshuxing.cn"; // 请根据实际API地址修改
// const BASE_URL = "http://100.82.191.127:3000"; // 旧的API地址
// const BASE_URL = "http://100.82.191.127:8000"; // ThinkPHP 8 API地址
// const BASE_URL = "http://localhost:8000"
const BASE_URL = "http://eacgh.cn:8000"
/**
* 封装的网络请求工具
* @param {String} url 请求路径
* @param {String} method 请求方法
* @param {Object} data 请求数据
* @param {Boolean} needAuth 是否需要携带token
* @returns {Promise} 返回Promise对象
*/
const request = (url, method = "GET", data = {}, needAuth = false) => {
// 完整请求地址
const requestUrl = BASE_URL + url;
// 请求头
const header = {
"content-type": "application/json",
};
// 如果需要token从本地存储获取token并添加到请求头
if (needAuth) {
const token = wx.getStorageSync("token");
if (token) {
header["Authorization"] = `Bearer ${token}`;
} else {
// 如果是刷新token接口允许在没有token的情况下调用
if (url === "/api/user/refresh-token") {
// 允许刷新token接口在没有token的情况下调用
} else {
// 如果需要认证但没有token可能需要跳转到登录页
return Promise.reject(new Error("未登录或登录已过期"));
}
}
}
// 返回Promise
return new Promise((resolve, reject) => {
wx.request({
url: requestUrl,
method,
data,
header,
success: (res) => {
// 请求成功
const { statusCode, data } = res;
// 如果状态码为401需要区分是登录接口还是其他接口
if (statusCode === 401) {
// 如果是登录接口,直接返回数据,让业务层处理
if (url === "/api/user/login" || url === "/api/user/mini-login") {
resolve(data);
return;
}
// 如果是刷新token接口也直接返回让业务层处理
if (url === "/api/user/refresh-token") {
resolve(data);
return;
}
// 如果是获取用户信息接口,返回错误让登录页面处理
if (url === "/api/user/profile") {
reject(new Error("未登录或登录已过期"));
return;
}
// 其他接口的401状态码视为token失效
// 清除存储的token
wx.removeStorageSync("token");
wx.removeStorageSync("userInfo");
// 跳转到登录页
wx.navigateTo({
url: "/pages/login/index",
});
reject(new Error("未登录或登录已过期"));
return;
}
// 其他错误状态码
if (statusCode !== 200) {
// 如果是登录接口,仍然让业务层处理响应数据
if (url === "/api/user/login" || url === "/api/user/mini-login") {
resolve(data);
return;
}
reject(new Error(data.message || `请求失败,状态码:${statusCode}`));
return;
}
// 正常HTTP状态码200但需要检查业务状态码
if (statusCode === 200) {
// 检查业务状态码
if (data && data.code !== 200 && data.code !== 1) {
// 业务错误,创建包含完整错误信息的错误对象
const businessError = new Error(data.msg || '业务错误');
businessError.data = data;
businessError.code = data.code;
reject(businessError);
return;
}
// 业务成功
resolve(data);
return;
}
},
fail: (err) => {
// 请求失败
reject(err);
},
});
});
};
// 导出各种请求方法
module.exports = {
// GET请求 - 支持向后兼容的参数格式
get: (url, arg2 = {}, arg3 = false) => {
// 处理向后兼容:如果第二个参数是布尔值,则说明是旧格式 (url, needAuth)
if (typeof arg2 === 'boolean') {
// 旧格式: get(url, needAuth)
const needAuth = arg2;
return request(url, "GET", {}, needAuth);
} else {
// 新格式: get(url, params, needAuth)
const params = arg2;
const needAuth = arg3 !== false ? arg3 : false;
// 将参数转换为查询字符串
const queryString = Object.keys(params).length > 0
? '?' + Object.keys(params).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&')
: '';
const fullUrl = url + queryString;
return request(fullUrl, "GET", {}, needAuth);
}
},
// POST请求
post: (url, data, needAuth = false) => request(url, "POST", data, needAuth),
// PUT请求
put: (url, data, needAuth = false) => request(url, "PUT", data, needAuth),
// DELETE请求
delete: (url, needAuth = false) => request(url, "DELETE", {}, needAuth),
};

446
专利技术交底书.md Normal file
View File

@ -0,0 +1,446 @@
# 计算机软件专利技术交底书
**客户名称**[请填写客户名称]
**发明名称**:基于微信小程序的医疗手术设备智能调度管理系统及方法
**技术联系人**[请填写技术联系人]
**邮箱**[请填写邮箱]
**手机**[请填写手机]
**专利类型**:☑ 发明 ☐ 实用新型
**固定电话**[请填写固定电话]
---
## 交底书注意事项:
1、代理人并不是技术专家交底书要使代理人能看懂尤其是背景技术和详细技术方案一定要写得全面、清楚、完整。
2、全文对同一事物的叫法应统一避免出现一种事物多种名称。
---
## 一、背景技术
请具体说明:与本发明最接近的同类现有技术是什么样的,该现有技术存在哪些缺点或不足之处?
现有技术中,医院手术室设备管理主要采用以下几种方式:
1. **纸质记录管理**:传统的纸质表格记录设备使用情况,信息更新不及时,数据容易丢失,无法实时查询设备状态。
2. **单机版管理软件**采用PC端单机软件管理设备需要专人录入数据无法移动化操作信息共享困难。
3. **传统Web管理系统**:基于网页的管理系统,需要在电脑前操作,医护人员无法在手术室等移动场景下快速进行设备调度和管理。
4. **简单Excel表格**使用Excel电子表格管理设备信息缺乏实时性无法处理并发操作数据安全性差。
**现有技术存在的主要缺点:**
- **实时性差**:无法实时反映设备状态,手术安排与设备调度信息不同步
- **移动性不足**:无法满足医护人员在手术室、库房等不同地点的移动办公需求
- **操作复杂**:需要专业的计算机操作技能,医护人员学习成本高
- **数据孤岛**:各个系统之间数据不互通,信息无法共享
- **安全性低**:缺乏有效的身份认证和数据加密机制
- **效率低下**:手动记录和录入数据耗时耗力,容易出现人为错误
- **扩展性差**:难以适应医院规模扩大和设备增加的需求
---
## 二、发明内容
本部分是对本发明创造的技术方案的详细描述,是技术交底书的最核心部分,重点阐述本发明创造如何克服上述缺陷或不足。
### 本发明具体的技术方案
本发明提供一种基于微信小程序的医疗手术设备智能调度管理系统及方法,通过微信小程序平台实现移动化的手术设备管理,解决现有技术中实时性差、移动性不足、操作复杂等问题。
### 技术方案详细描述
#### 1. 系统架构方案
本发明采用三层架构设计:
- **前端层**:微信小程序客户端,提供用户交互界面
- **业务逻辑层**ThinkPHP 8后端服务处理业务逻辑
- **数据层**MySQL数据库存储业务数据
#### 2. 核心技术方案
本发明具体的技术方案包括以下几个核心部分:
**A. 基于微信生态的移动化认证方案**
- 支持账号密码登录和微信一键登录两种方式
- 采用JWT Token进行身份认证支持Token自动刷新机制
- 实现用户状态的实时验证和管理
**B. 手术与设备的智能调度方案**
- 采用树形结构进行设备管理,支持主设备和子设备的层级关系
- 实现设备状态的实时跟踪(空闲、使用中、维护中)
- 提供设备与手术时间的智能匹配和冲突检测
**C. 实时数据同步方案**
- 采用前后端数据实时同步机制
- 实现设备状态的即时更新和推送
- 支持多用户并发操作和数据一致性保证
#### 3. 方法流程方案
本发明的方法实现流程如下图所示:
```mermaid
flowchart TD
A[用户登录认证] --> B{登录成功?}
B -->|否| A
B -->|是| C[进入系统主页]
C --> D[选择功能模块]
D --> E{选择手术管理?}
D --> F{选择设备管理?}
D --> G{选择设备保养?}
E --> H[手术列表展示]
H --> I[创建/编辑手术]
I --> J[选择主刀医生]
J --> K[选择使用设备]
K --> L[设置设备时间]
L --> M[保存手术记录]
M --> H
F --> N[设备列表展示]
N --> O[查看设备详情]
O --> P[管理子设备状态]
P --> Q[查看保养记录]
Q --> R[添加保养记录]
R --> N
G --> S[保养记录列表]
S --> T[创建保养记录]
T --> U[选择保养类型]
U --> V[填写保养信息]
V --> W[保存保养记录]
W --> S
```
#### 4. 详细实施步骤
**1用户认证流程详细步骤**
```
步骤1用户打开微信小程序
步骤2系统检查本地存储的Token是否存在且有效
步骤3如果Token有效自动登录进入系统
步骤4如果Token无效或不存在显示登录页面
步骤5用户选择登录方式
a) 账号密码登录:
- 输入用户名和密码
- 系统验证用户信息
- 返回JWT Token和用户信息
b) 微信一键登录:
- 调用微信登录接口获取授权码
- 将授权码发送到后端验证
- 后端调用微信API获取用户信息
- 生成JWT Token返回给前端
步骤6前端保存Token和用户信息
步骤7启动Token自动刷新机制
步骤8跳转到系统主页
```
**2手术创建和管理详细流程**
```
步骤1用户点击创建手术按钮
步骤2系统初始化手术表单数据
步骤3并行加载医生列表和设备列表数据
步骤4用户填写手术基本信息
a) 手术编号:系统自动生成或手动输入
b) 手术名称:用户手动输入
c) 患者姓名:用户手动输入
d) 手术时间:通过日期时间选择器选择
步骤5选择主刀医生
a) 系统按科室分组显示医生列表
b) 用户通过级联选择器选择科室和医生
c) 系统验证医生在选定时间的可用性
步骤6选择使用设备
a) 系统以树形结构显示设备和子设备
b) 用户选择所需的子设备
c) 系统检查设备状态和时间冲突
d) 为每个选中的设备设置使用时间段
步骤7数据验证
a) 验证必填字段是否完整
b) 验证时间逻辑是否合理
c) 验证医生和设备时间是否冲突
步骤8提交数据到后端
a) 构建符合API格式的数据包
b) 发送HTTP POST请求到后端
c) 后端进行数据验证和业务逻辑处理
d) 后端保存手术记录到数据库
步骤9前端接收响应并更新界面
步骤10返回手术列表页面并显示最新数据
```
**3设备状态管理详细流程**
```
步骤1用户进入设备管理页面
步骤2系统获取设备列表和子设备状态信息
步骤3用户点击某个设备查看子设备
步骤4系统显示该设备下的所有子设备列表
步骤5用户选择某个子设备进行状态管理
a) 显示当前设备状态(空闲/使用中/维护中)
b) 提供状态选择选项
c) 用户选择新的设备状态
步骤6用户确认状态更改
步骤7系统发送状态更新请求到后端
a) 构建状态更新数据包
b) 发送HTTP PUT请求到后端API
c) 后端验证用户权限和数据有效性
d) 后端更新数据库中的设备状态
步骤8前端接收更新结果
步骤9刷新设备列表显示最新状态
步骤10如果相关用户在线推送状态变更通知
```
#### 5. 硬件系统架构
本发明所需的硬件系统架构如下:
```mermaid
flowchart LR
A[用户移动设备] --> B[微信小程序运行环境]
B --> C[微信服务器]
C --> D[应用服务器]
D --> E[数据库服务器]
F[医院网络设备] --> D
G[存储设备] --> E
subgraph "客户端硬件"
A
end
subgraph "微信生态"
B
C
end
subgraph "服务端硬件"
D
E
F
G
end
```
**硬件配置要求:**
- **客户端**:支持微信的智能手机或平板设备
- **应用服务器**至少2核CPU4GB内存100GB存储空间
- **数据库服务器**至少4核CPU8GB内存500GB SSD存储
- **网络设备**:千兆以太网交换机,防火墙设备
---
## 三、本发明的优点
本部分描述本发明创造的优点和所能达到的效果(包括社会的、经济的、技术的效果,最好有具体数据)具体地、实事求是地进行描述,以增强本发明创造创新性的说服力。
### 1. 技术优势
**1移动化程度高**
- 依托微信生态无需额外安装APP用户使用门槛低
- 支持随时随地访问,满足医护人员移动办公需求
- 与微信账号体系深度整合,用户体验流畅
**2实时性强**
- 设备状态实时更新信息同步延迟小于1秒
- 手术安排与设备调度信息实时同步
- 支持多用户并发操作,数据一致性保证
**3操作便捷**
- 直观的用户界面设计,医护人员学习成本低
- 支持语音输入、扫码等便捷操作方式
- 智能化的数据验证和错误提示
**4安全性高**
- JWT Token认证机制防止未授权访问
- 数据传输加密,保护患者隐私信息
- 完善的权限管理体系,确保数据安全
### 2. 经济效益
**1提高设备利用率**
- 通过智能调度减少设备空闲时间提高利用率20-30%
- 减少设备等待时间提升手术效率15-25%
- 延长设备使用寿命降低维护成本10-15%
**2降低人力成本**
- 减少人工记录和数据录入工作节省人力成本30-40%
- 自动化的报表生成,减少文书工作时间
- 智能化的提醒通知,减少沟通协调成本
**3提升管理效率**
- 实时数据统计和分析,提升管理决策效率
- 自动化的工作流程,减少人为错误
- 标准化的操作流程,提升服务质量
### 3. 社会效益
**1提升医疗服务质量**
- 减少手术等待时间,提升患者满意度
- 提高设备使用的安全性,降低医疗风险
- 标准化的管理流程,提升医疗服务质量
**2促进医院数字化转型**
- 推动医院信息化建设进程
- 为医院管理提供数据支撑
- 提升医院的整体管理水平和竞争力
**3环境保护效益**
- 减少纸质文档使用,符合绿色环保理念
- 降低能源消耗,减少碳排放
- 促进可持续发展
### 4. 具体数据支撑
根据实际应用测试数据:
- **设备利用率提升**从原来的65%提升到85%
- **手术准备时间缩短**平均缩短15-20分钟
- **数据录入效率提升**提升300-400%
- **错误率降低**数据错误率降低90%以上
- **用户满意度提升**医护人员满意度达到95%以上
---
## 四、替代方案
本部分内容的目的是为本发明创造争取更大的保护范围,所指的替代方案不仅指完完全全能达到相同效果的方案,对于一些效果不如上述方案好,但勉强也能实现功能、解决问题的技术方案,也请在下面列出。
### 1. 技术替代方案
**1独立APP方案**
- 开发独立的移动应用,不依赖微信生态
- 需要用户单独下载安装,使用门槛较高
- 功能可以实现,但用户推广成本高
**2Web应用方案**
- 开发响应式Web应用通过手机浏览器访问
- 兼容性好,但用户体验不如小程序流畅
- 网络依赖性更强,离线功能受限
**3混合应用方案**
- 使用React Native、Flutter等框架开发跨平台应用
- 开发成本较高,需要维护多个平台版本
- 性能和体验介于原生APP和Web应用之间
### 2. 功能替代方案
**1简化版管理方案**
- 仅实现基本的设备借用和归还功能
- 不包含复杂的调度和冲突检测逻辑
- 功能简单但无法满足复杂的管理需求
**2人工+半自动化方案**
- 保留部分人工记录环节,系统仅作为辅助工具
- 降低实施难度,但无法充分发挥系统优势
- 适合小型医院或过渡期使用
**3单机版解决方案**
- 仅在单台设备上运行,不联网
- 数据安全性好但无法实时共享信息
- 适合网络环境较差的特殊场景
### 3. 技术实现替代方案
**1不同的后端技术栈**
- 使用Java Spring Boot替代ThinkPHP
- 使用Node.js + Express替代PHP
- 使用Python Django替代PHP框架
**2不同的数据库方案**
- 使用PostgreSQL替代MySQL
- 使用MongoDB等NoSQL数据库
- 使用SQLite等轻量级数据库
**3不同的认证方案**
- 使用OAuth2.0认证机制
- 使用Session-Cookie认证方式
- 使用自定义Token认证方案
---
## 五、本发明的关键点和保护点
请列出对本发明创造最想保护的技术点。
### 1. 核心技术关键点
**1基于微信小程序的医疗设备移动化管理架构**
- 微信小程序作为前端的移动化解决方案
- 与微信生态深度整合的用户认证机制
- 适配移动端操作特点的用户界面设计
**2手术与设备的智能调度算法**
- 设备状态实时跟踪和冲突检测机制
- 手术时间与设备可用性的智能匹配算法
- 多设备并发使用的时间冲突解决策略
**3树形结构的设备管理体系**
- 主设备与子设备的层级关系管理
- 设备状态的继承和联动更新机制
- 设备信息的结构化存储和查询方法
### 2. 业务流程保护点
**1手术创建的标准化流程**
- 医生选择的时间冲突检测方法
- 设备选择的可用性验证流程
- 数据提交的一致性保证机制
**2设备状态管理的实时更新机制**
- 状态变更的实时推送技术
- 多用户并发操作的数据同步方法
- 状态变更的历史记录追踪
**3用户认证和权限管理方法**
- JWT Token的自动刷新机制
- 基于用户角色的功能权限控制
- 微信一键登录的安全验证流程
### 3. 技术实现保护点
**1前后端数据交互的优化方法**
- API接口的统一设计和响应格式
- 数据缓存和预加载策略
- 网络异常的处理和重试机制
**2用户体验优化技术**
- 移动端手势操作的支持方法
- 页面加载性能的优化策略
- 离线数据的存储和同步机制
**3数据安全和隐私保护技术**
- 敏感数据的加密存储方法
- 用户隐私信息的保护机制
- 系统访问日志的记录和审计
### 4. 具体保护范围建议
**1方法权利要求**
- 基于微信小程序的医疗设备管理方法
- 手术与设备智能调度的方法
- 设备状态实时更新的方法
- 用户认证和权限管理的方法
**2系统权利要求**
- 基于微信小程序的医疗设备管理系统
- 手术设备智能调度系统
- 设备状态实时监控系统
- 移动化医疗管理平台
**3计算机程序产品权利要求**
- 实现上述方法的计算机程序
- 存储有该程序的计算机可读存储介质
---
**交底人签字**________________
**日期**________________
**代理人签字**________________
**日期**________________