p3l6f9c7s 6 months ago
parent 48e9d01494
commit bf03cf2d86

@ -0,0 +1,39 @@
{
"pages": [
"pages/index/index",
"pages/logs/logs",
"pages/report/form",
"pages/repairs/list",
"pages/repairs/detail",
"pages/tech/list",
"pages/tech/auth",
"pages/admin/login",
"pages/admin/index",
"pages/admin/repairs-list",
"pages/admin/tech-management",
"pages/admin/add-tech",
"pages/admin/edit-tech",
"pages/admin/user-management",
"pages/admin/tech-orders",
"pages/admin/repair-detail"
],
"window": {
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
},
"style": "v2",
"renderer": "skyline",
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true,
"defaultContentBox": true,
"tagNameStyleIsolation": "legacy",
"disableABTest": true,
"sdkVersionBegin": "3.0.0",
"sdkVersionEnd": "15.255.255"
}
},
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

@ -0,0 +1,18 @@
// app.ts
App<IAppOption>({
globalData: {},
onLaunch() {
// 展示本地存储能力
const logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)
// 登录
wx.login({
success: res => {
console.log(res.code)
// 发送 res.code 到后台换取 openId, sessionKey, unionId
},
})
},
})

@ -0,0 +1,10 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}

@ -0,0 +1,5 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {}
}

@ -0,0 +1,105 @@
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
*
*/
properties: {
extClass: {
type: String,
value: ''
},
title: {
type: String,
value: ''
},
background: {
type: String,
value: ''
},
color: {
type: String,
value: ''
},
back: {
type: Boolean,
value: true
},
loading: {
type: Boolean,
value: false
},
homeButton: {
type: Boolean,
value: false,
},
animated: {
// 显示隐藏的时候opacity动画效果
type: Boolean,
value: true
},
show: {
// 显示隐藏导航隐藏的时候navigation-bar的高度占位还在
type: Boolean,
value: true,
observer: '_showChange'
},
// back为true的时候返回的页面深度
delta: {
type: Number,
value: 1
},
},
/**
*
*/
data: {
displayStyle: ''
},
lifetimes: {
attached() {
const rect = wx.getMenuButtonBoundingClientRect()
wx.getSystemInfo({
success: (res) => {
const isAndroid = res.platform === 'android'
const isDevtools = res.platform === 'devtools'
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${res.windowWidth - rect.left}px`,
leftWidth: `width: ${res.windowWidth - rect.left }px`,
safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${res.safeArea.top}px); padding-top: ${res.safeArea.top}px` : ``
})
}
})
},
},
/**
*
*/
methods: {
_showChange(show: boolean) {
const animated = this.data.animated
let displayStyle = ''
if (animated) {
displayStyle = `opacity: ${
show ? '1' : '0'
};transition:opacity 0.5s;`
} else {
displayStyle = `display: ${show ? '' : 'none'}`
}
this.setData({
displayStyle
})
},
back() {
const data = this.data
if (data.delta) {
wx.navigateBack({
delta: data.delta
})
}
this.triggerEvent('back', { delta: data.delta }, {})
}
},
})

@ -0,0 +1,64 @@
<view class="weui-navigation-bar {{extClass}}">
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}};">
<!-- 左侧按钮 -->
<view class='weui-navigation-bar__left' style="{{leftWidth}};">
<block wx:if="{{back || homeButton}}">
<!-- 返回上一页 -->
<block wx:if="{{back}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
<view
bindtap="back"
class="weui-navigation-bar__btn_goback_wrapper"
hover-class="weui-active"
hover-stay-time="100"
aria-role="button"
aria-label="返回"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
</view>
</view>
</block>
<!-- 返回首页 -->
<block wx:if="{{homeButton}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
<view
bindtap="home"
class="weui-navigation-bar__btn_home_wrapper"
hover-class="weui-active"
aria-role="button"
aria-label="首页"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
</view>
</view>
</block>
</block>
<block wx:else>
<slot name="left"></slot>
</block>
</view>
<!-- 标题 -->
<view class='weui-navigation-bar__center'>
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
<view
class="weui-loading"
aria-role="img"
aria-label="加载中"
></view>
</view>
<block wx:if="{{title}}">
<text>{{title}}</text>
</block>
<block wx:else>
<slot name="center"></slot>
</block>
</view>
<!-- 右侧留空 -->
<view class='weui-navigation-bar__right'>
<slot name="right"></slot>
</view>
</view>
</view>

@ -0,0 +1,96 @@
.weui-navigation-bar {
--weui-FG-0:rgba(0,0,0,.9);
--height: 44px;
--left: 16px;
}
.weui-navigation-bar .android {
--height: 48px;
}
.weui-navigation-bar {
overflow: hidden;
color: var(--weui-FG-0);
flex: none;
}
.weui-navigation-bar__inner {
position: relative;
top: 0;
left: 0;
height: calc(var(--height) + env(safe-area-inset-top));
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: env(safe-area-inset-top);
width: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__left {
position: relative;
padding-left: var(--left);
display: flex;
flex-direction: row;
align-items: flex-start;
height: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__btn_goback_wrapper {
padding: 11px 18px 11px 16px;
margin: -11px -18px -11px -16px;
}
.weui-navigation-bar__btn_goback_wrapper.weui-active {
opacity: 0.5;
}
.weui-navigation-bar__btn_goback {
font-size: 12px;
width: 12px;
height: 24px;
-webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--weui-FG-0);
}
.weui-navigation-bar__center {
font-size: 17px;
text-align: center;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-weight: bold;
flex: 1;
height: 100%;
}
.weui-navigation-bar__loading {
margin-right: 4px;
align-items: center;
}
.weui-loading {
font-size: 16px;
width: 16px;
height: 16px;
display: block;
background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat;
background-size: 100%;
margin-left: 0;
animation: loading linear infinite 1s;
}
@keyframes loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "添加维修人员",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,110 @@
// admin/add-tech.ts
Page({
data: {
name: '',
account: '',
password: '',
phone: '',
area: '',
loading: false,
errorMsg: ''
},
onLoad() {
// 检查管理员登录状态
this.checkAdminLogin()
},
// 检查管理员登录状态
checkAdminLogin() {
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
}
},
// 输入处理函数
onNameInput(e: WechatMiniprogram.Input) {
this.setData({
name: e.detail.value
})
},
onAccountInput(e: WechatMiniprogram.Input) {
this.setData({
account: e.detail.value
})
},
onPasswordInput(e: WechatMiniprogram.Input) {
this.setData({
password: e.detail.value
})
},
onPhoneInput(e: WechatMiniprogram.Input) {
this.setData({
phone: e.detail.value
})
},
onAreaInput(e: WechatMiniprogram.Input) {
this.setData({
area: e.detail.value
})
},
// 提交表单
submitForm(e: WechatMiniprogram.FormSubmit) {
const { name, account, password, phone } = this.data
// 表单验证
if (!name) {
this.setData({ errorMsg: '请输入维修人员姓名' })
return
}
if (!account) {
this.setData({ errorMsg: '请输入登录账号' })
return
}
if (!password) {
this.setData({ errorMsg: '请输入登录密码' })
return
}
if (!phone) {
this.setData({ errorMsg: '请输入联系电话' })
return
}
// 简单的手机号验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(phone)) {
this.setData({ errorMsg: '请输入正确的手机号码' })
return
}
this.setData({ loading: true, errorMsg: '' })
// 模拟添加维修人员实际项目中应该调用后端API
setTimeout(() => {
// 假设添加成功
this.setData({ loading: false })
wx.showToast({
title: '添加成功',
icon: 'success',
success: () => {
// 延迟返回上一页,让用户看到成功提示
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
})
}, 1000)
}
})

@ -0,0 +1,81 @@
<!-- admin/add-tech.wxml -->
<navigation-bar title="添加维修人员" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<form bindsubmit="submitForm">
<view class="form-group">
<text class="label">姓名 <text class="required">*</text></text>
<input
class="input"
placeholder="请输入维修人员姓名"
name="name"
value="{{name}}"
bindinput="onNameInput"
required
/>
</view>
<view class="form-group">
<text class="label">账号 <text class="required">*</text></text>
<input
class="input"
placeholder="请输入登录账号"
name="account"
value="{{account}}"
bindinput="onAccountInput"
required
/>
</view>
<view class="form-group">
<text class="label">密码 <text class="required">*</text></text>
<input
class="input"
placeholder="请输入登录密码"
password
name="password"
value="{{password}}"
bindinput="onPasswordInput"
required
/>
</view>
<view class="form-group">
<text class="label">联系电话 <text class="required">*</text></text>
<input
class="input"
placeholder="请输入联系电话"
type="number"
name="phone"
value="{{phone}}"
bindinput="onPhoneInput"
required
/>
</view>
<view class="form-group">
<text class="label">负责区域</text>
<input
class="input"
placeholder="例如A栋、B栋"
name="area"
value="{{area}}"
bindinput="onAreaInput"
/>
</view>
<button
class="submit-btn"
type="primary"
form-type="submit"
loading="{{loading}}"
disabled="{{loading}}"
>
添加
</button>
<view class="tips" wx:if="{{errorMsg}}">
{{errorMsg}}
</view>
</form>
</view>

@ -0,0 +1,49 @@
/* admin/add-tech.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
.form-group {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.required {
color: #e64340;
}
.input {
width: 100%;
height: 88rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 0 30rpx;
box-sizing: border-box;
font-size: 32rpx;
border: 1rpx solid #e0e0e0;
}
.submit-btn {
margin-top: 60rpx;
width: 100%;
height: 96rpx;
font-size: 32rpx;
border-radius: 8rpx;
line-height: 96rpx;
}
.tips {
margin-top: 20rpx;
text-align: center;
font-size: 28rpx;
color: #e64340;
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "编辑维修人员",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,170 @@
// admin/edit-tech.ts
Page({
data: {
techId: '',
technician: {
id: '',
name: '',
account: '',
phone: '',
area: '',
status: '',
registerTime: 0
},
password: '', // 新密码,留空表示不修改
loading: false,
errorMsg: ''
},
onLoad(options: any) {
// 检查管理员登录状态
this.checkAdminLogin()
// 获取维修人员ID
if (options.id) {
this.setData({
techId: options.id
})
// 加载维修人员详情
this.loadTechnicianDetail()
}
},
// 检查管理员登录状态
checkAdminLogin() {
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
}
},
// 加载维修人员详情
loadTechnicianDetail() {
this.setData({ loading: true })
// 模拟获取维修人员详情实际项目中应该调用后端API
setTimeout(() => {
// 模拟数据根据ID返回不同的维修人员信息
let mockTechnician: any = {
id: this.data.techId,
name: '',
account: '',
phone: '',
area: '',
status: 'active',
registerTime: new Date().getTime()
}
// 根据ID设置不同的模拟数据
if (this.data.techId === '1') {
mockTechnician = {
id: '1',
name: '王五',
account: 'tech001',
phone: '13800138001',
area: 'A栋、B栋',
status: 'active',
registerTime: new Date().getTime() - 7 * 24 * 60 * 60 * 1000
}
} else if (this.data.techId === '2') {
mockTechnician = {
id: '2',
name: '赵六',
account: 'tech002',
phone: '13800138002',
area: 'C栋、D栋',
status: 'active',
registerTime: new Date().getTime() - 5 * 24 * 60 * 60 * 1000
}
} else if (this.data.techId === '3') {
mockTechnician = {
id: '3',
name: '钱七',
account: 'tech003',
phone: '13800138003',
area: 'E栋、F栋',
status: 'banned',
registerTime: new Date().getTime() - 3 * 24 * 60 * 60 * 1000
}
}
this.setData({
technician: mockTechnician,
loading: false
})
}, 1000)
},
// 输入处理函数
onNameInput(e: WechatMiniprogram.Input) {
const technician = this.data.technician
technician.name = e.detail.value
this.setData({ technician })
},
onAccountInput(e: WechatMiniprogram.Input) {
// 账号不可修改,这里只是为了绑定输入事件
},
onPasswordInput(e: WechatMiniprogram.Input) {
this.setData({
password: e.detail.value
})
},
onPhoneInput(e: WechatMiniprogram.Input) {
const technician = this.data.technician
technician.phone = e.detail.value
this.setData({ technician })
},
onAreaInput(e: WechatMiniprogram.Input) {
const technician = this.data.technician
technician.area = e.detail.value
this.setData({ technician })
},
// 提交表单
submitForm(e: WechatMiniprogram.FormSubmit) {
const { technician, password } = this.data
// 表单验证
if (!technician.name) {
this.setData({ errorMsg: '请输入维修人员姓名' })
return
}
if (!technician.phone) {
this.setData({ errorMsg: '请输入联系电话' })
return
}
// 简单的手机号验证
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(technician.phone)) {
this.setData({ errorMsg: '请输入正确的手机号码' })
return
}
this.setData({ loading: true, errorMsg: '' })
// 模拟更新维修人员信息实际项目中应该调用后端API
setTimeout(() => {
// 假设更新成功
this.setData({ loading: false })
wx.showToast({
title: '保存成功',
icon: 'success',
success: () => {
// 延迟返回上一页,让用户看到成功提示
setTimeout(() => {
wx.navigateBack()
}, 1500)
}
})
}, 1000)
}
})

@ -0,0 +1,81 @@
<!-- admin/edit-tech.wxml -->
<navigation-bar title="编辑维修人员" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<form bindsubmit="submitForm">
<view class="form-group">
<text class="label">姓名 <text class="required">*</text></text>
<input
class="input"
placeholder="请输入维修人员姓名"
name="name"
value="{{technician.name}}"
bindinput="onNameInput"
required
/>
</view>
<view class="form-group">
<text class="label">账号 <text class="required">*</text></text>
<input
class="input"
placeholder="请输入登录账号"
name="account"
value="{{technician.account}}"
bindinput="onAccountInput"
disabled
/>
<text class="disabled-tip">账号不可修改</text>
</view>
<view class="form-group">
<text class="label">密码</text>
<input
class="input"
placeholder="留空表示不修改密码"
password
name="password"
value="{{password}}"
bindinput="onPasswordInput"
/>
</view>
<view class="form-group">
<text class="label">联系电话 <text class="required">*</text></text>
<input
class="input"
placeholder="请输入联系电话"
type="number"
name="phone"
value="{{technician.phone}}"
bindinput="onPhoneInput"
required
/>
</view>
<view class="form-group">
<text class="label">负责区域</text>
<input
class="input"
placeholder="例如A栋、B栋"
name="area"
value="{{technician.area}}"
bindinput="onAreaInput"
/>
</view>
<button
class="submit-btn"
type="primary"
form-type="submit"
loading="{{loading}}"
disabled="{{loading}}"
>
保存修改
</button>
<view class="tips" wx:if="{{errorMsg}}">
{{errorMsg}}
</view>
</form>
</view>

@ -0,0 +1,61 @@
/* admin/edit-tech.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
.form-group {
margin-bottom: 30rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.required {
color: #e64340;
}
.input {
width: 100%;
height: 88rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 0 30rpx;
box-sizing: border-box;
font-size: 32rpx;
border: 1rpx solid #e0e0e0;
}
.input[disabled] {
background-color: #f5f5f5;
color: #999;
}
.disabled-tip {
font-size: 24rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
.submit-btn {
margin-top: 60rpx;
width: 100%;
height: 96rpx;
font-size: 32rpx;
border-radius: 8rpx;
line-height: 96rpx;
}
.tips {
margin-top: 20rpx;
text-align: center;
font-size: 28rpx;
color: #e64340;
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "管理员中心",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,63 @@
// admin/index.ts
Page({
data: {
adminAccount: ''
},
onLoad() {
// 检查管理员登录状态
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
return
}
// 获取管理员账号信息
const account = wx.getStorageSync('adminAccount')
this.setData({
adminAccount: account || 'admin'
})
},
// 跳转到报修单管理页面
goToRepairsList() {
wx.navigateTo({
url: '/pages/admin/repairs-list'
})
},
// 跳转到维修人员管理页面
goToTechManagement() {
wx.navigateTo({
url: '/pages/admin/tech-management'
})
},
// 退出登录
logout() {
wx.showModal({
title: '确认退出',
content: '确定要退出管理员登录吗?',
success: (res) => {
if (res.confirm) {
// 清除登录状态
wx.removeStorageSync('adminLoggedIn')
wx.removeStorageSync('adminAccount')
// 跳转到登录页面
wx.redirectTo({
url: '/pages/admin/login'
})
}
}
})
},
// 返回上一页
onUnload() {
// 页面卸载时的处理
}
})

@ -0,0 +1,33 @@
<!-- admin/index.wxml -->
<navigation-bar title="管理员中心" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<view class="header">
<view class="welcome">
<text class="greeting">欢迎,管理员</text>
<text class="account">{{adminAccount}}</text>
</view>
<view class="logout-btn" bindtap="logout">退出登录</view>
</view>
<view class="menu-list">
<view class="menu-item" bindtap="goToRepairsList">
<view class="menu-icon repairs"></view>
<view class="menu-text">报修单管理</view>
<view class="menu-arrow"></view>
</view>
<view class="menu-item" bindtap="goToTechManagement">
<view class="menu-icon tech"></view>
<view class="menu-text">维修人员管理</view>
<view class="menu-arrow"></view>
</view>
</view>
<view class="system-info">
<text class="info-text">系统版本v1.0.0</text>
<text class="info-text">更新时间2024-01-01</text>
</view>
</view>

@ -0,0 +1,123 @@
/* admin/index.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
.header {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.welcome {
flex: 1;
}
.greeting {
font-size: 36rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 10rpx;
}
.account {
font-size: 28rpx;
color: #666;
display: block;
}
.logout-btn {
font-size: 28rpx;
padding: 0 10rpx;
height: 60rpx;
line-height: 60rpx;
background-color: #fff;
color: #e64340;
border: 1rpx solid #e64340;
border-radius: 8rpx;
width: 100rpx;
text-align: center;
box-sizing: border-box;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
flex-shrink: 0;
display: block;
margin: 0;
/* 增加触摸反馈 */
transition: background-color 0.2s;
}
.menu-list {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.menu-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.menu-item:last-child {
border-bottom: none;
}
.menu-icon {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
margin-right: 24rpx;
}
.menu-icon.repairs {
background-color: #e8f4fd;
}
.menu-icon.tech {
background-color: #e8f5e9;
}
.menu-icon.orders {
background-color: #fff3e0;
}
.menu-text {
flex: 1;
font-size: 32rpx;
color: #333;
}
.menu-arrow {
width: 20rpx;
height: 20rpx;
border-top: 2rpx solid #999;
border-right: 2rpx solid #999;
transform: rotate(45deg);
}
.system-info {
text-align: center;
padding: 30rpx;
}
.info-text {
display: block;
font-size: 24rpx;
color: #999;
margin-bottom: 10rpx;
}

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "管理员登录",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"backgroundColor": "#f8f8f8"
}

@ -0,0 +1,65 @@
// admin/login.ts
Page({
data: {
account: '',
password: '',
loading: false,
errorMsg: ''
},
// 使用model:value后可以保留这个方法作为额外处理如果需要的话
onAccountInput(e: any) {
// 可选:添加额外的输入处理逻辑
},
onPasswordInput(e: any) {
// 可选:添加额外的输入处理逻辑
},
login() {
const { account, password } = this.data
if (!account || !password) {
this.setData({
errorMsg: '请输入账号和密码'
})
return
}
// 简单的管理员账号密码验证实际项目中应该调用后端API
// 这里使用固定的管理员账号密码作为演示
if (account === 'admin' && password === 'admin123') {
this.setData({
loading: true,
errorMsg: ''
})
// 保存管理员登录状态
wx.setStorageSync('adminLoggedIn', true)
wx.setStorageSync('adminAccount', account)
// 延迟一小段时间模拟API调用
setTimeout(() => {
this.setData({ loading: false })
// 跳转到管理员主页
wx.redirectTo({
url: '/pages/admin/index'
})
}, 500)
} else {
this.setData({
errorMsg: '账号或密码错误'
})
}
},
onLoad() {
// 检查是否已经登录,如果已登录则直接跳转到主页
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/index'
})
}
}
})

@ -0,0 +1,44 @@
<!-- admin/login.wxml -->
<view class="container">
<view class="header">
<text class="title">管理员登录</text>
<text class="subtitle">请输入管理员账号和密码</text>
</view>
<view class="form">
<view class="input-group">
<text class="label">账号</text>
<input
class="input"
type="text"
placeholder="请输入管理员账号"
bindinput="onAccountInput"
model:value="{{account}}"
/>
</view>
<view class="input-group">
<text class="label">密码</text>
<input
class="input"
type="password"
placeholder="请输入管理员密码"
bindinput="onPasswordInput"
model:value="{{password}}"
/>
</view>
<button
type="primary"
bindtap="login"
loading="{{loading}}"
disabled="{{loading}}"
>
登录
</button>
<view class="tips" wx:if="{{errorMsg}}">
{{errorMsg}}
</view>
</view>
</view>

@ -0,0 +1,70 @@
/* admin/login.wxss */
.container {
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 60rpx 40rpx;
box-sizing: border-box;
background-color: #f8f8f8;
}
.header {
text-align: center;
margin-bottom: 80rpx;
}
.title {
font-size: 48rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 20rpx;
}
.subtitle {
font-size: 28rpx;
color: #666;
display: block;
}
.form {
flex: 1;
}
.input-group {
margin-bottom: 40rpx;
}
.label {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 16rpx;
}
.input {
width: 100%;
height: 88rpx;
background-color: #fff;
border-radius: 8rpx;
padding: 0 30rpx;
box-sizing: border-box;
font-size: 32rpx;
border: 1rpx solid #e0e0e0;
}
.login-btn {
margin-top: 60rpx;
width: 100%;
height: 96rpx;
font-size: 32rpx;
border-radius: 8rpx;
line-height: 96rpx;
}
.tips {
margin-top: 20rpx;
text-align: center;
font-size: 28rpx;
color: #e64340;
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "报修单详情",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,203 @@
// admin/repair-detail.ts
import { getRepairById, RepairItem } from '../../utils/report'
Page({
data: {
repairId: '',
repairInfo: {
id: '',
title: '',
dormitory: '',
room: '',
contact: '',
phone: '',
category: '',
status: '',
createTime: 0,
description: '',
images: [],
assignedTech: '',
processRecords: []
},
showActions: false,
loading: false
},
onLoad(options: any) {
// 检查管理员登录状态
this.checkAdminLogin()
// 获取报修单ID
if (options.id) {
this.setData({
repairId: options.id
})
// 加载报修单详情
this.loadRepairDetail()
}
},
// 检查管理员登录状态
checkAdminLogin() {
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
}
},
// 加载报修单详情 - 使用系统中真实的报修单数据
loadRepairDetail() {
this.setData({ loading: true })
try {
// 从系统中获取真实的报修单详情
const repairDetail = getRepairById(this.data.repairId)
if (!repairDetail) {
wx.showToast({ title: '报修单不存在', icon: 'none' })
this.setData({ loading: false })
return
}
// 转换数据格式以匹配UI需求
const formattedDetail = {
id: repairDetail.id,
title: repairDetail.title,
dormitory: repairDetail.dormBuilding,
room: repairDetail.dormRoom,
contact: repairDetail.contactName,
phone: repairDetail.contactPhone,
category: repairDetail.category,
status: this.convertStatus(repairDetail.status),
createTime: repairDetail.createdAt,
description: repairDetail.description || '暂无描述',
images: repairDetail.photos || [],
assignedTech: repairDetail.technicianName || '',
// 构建处理记录
processRecords: [
{
time: repairDetail.createdAt,
content: '报修单已提交'
}
]
}
// 根据状态添加额外的处理记录
if (repairDetail.technicianName) {
formattedDetail.processRecords.push({
time: repairDetail.updatedAt,
content: `${repairDetail.technicianName} 已接单`
})
}
if (repairDetail.status === 'resolved') {
formattedDetail.processRecords.push({
time: repairDetail.updatedAt,
content: '维修已完成'
})
}
this.setData({
repairInfo: formattedDetail,
showActions: formattedDetail.status === 'pending', // 只有待处理的订单才显示操作按钮
loading: false
})
} catch (error) {
console.error('加载报修单详情失败:', error)
this.setData({ loading: false })
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
// 状态转换函数将系统状态转换为UI显示状态
convertStatus(status: string): string {
const statusMap: {[key: string]: string} = {
'unassigned': 'pending',
'in_progress': 'processing',
'resolved': 'completed'
}
return statusMap[status] || 'pending'
},
// 获取状态文本
getStatusText(status: string) {
switch(status) {
case 'pending':
return '待处理'
case 'processing':
return '处理中'
case 'completed':
return '已完成'
default:
return '未知状态'
}
},
// 获取状态样式类
getStatusClass(status: string) {
return status
},
// 格式化时间
formatTime(timestamp: number) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
},
// 预览图片
previewImage(e: WechatMiniprogram.CustomEvent) {
const index = e.currentTarget.dataset.index
wx.previewImage({
urls: this.data.repairInfo.images,
current: this.data.repairInfo.images[index]
})
},
// 更改报修单状态
changeStatus(e: WechatMiniprogram.CustomEvent) {
const newStatus = e.currentTarget.dataset.status
const statusText = newStatus === 'processing' ? '处理中' : '已完成'
wx.showModal({
title: '确认操作',
content: `确定要将此报修单标记为${statusText}吗?`,
success: (res) => {
if (res.confirm) {
this.setData({ loading: true })
// 模拟更新状态实际项目中应该调用后端API
setTimeout(() => {
// 更新状态
const updatedInfo = { ...this.data.repairInfo }
updatedInfo.status = newStatus
// 添加处理记录
const newRecord = {
time: new Date().getTime(),
content: `管理员将状态更新为${statusText}`
}
updatedInfo.processRecords.push(newRecord)
this.setData({
repairInfo: updatedInfo,
showActions: false, // 状态改变后隐藏操作按钮
loading: false
})
wx.showToast({
title: '状态更新成功',
icon: 'success'
})
}, 1000)
}
}
})
}
})

@ -0,0 +1,99 @@
<!-- admin/repair-detail.wxml -->
<navigation-bar title="报修单详情" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<scroll-view scroll-y>
<!-- 基本信息卡片 -->
<view class="card">
<view class="card-header">
<text class="card-title">基本信息</text>
<view class="status-badge {{getStatusClass(repairInfo.status)}}">{{getStatusText(repairInfo.status)}}</view>
</view>
<view class="info-grid">
<view class="info-item">
<text class="info-label">报修标题</text>
<text class="info-value">{{repairInfo.title}}</text>
</view>
<view class="info-item">
<text class="info-label">报修类别</text>
<text class="info-value">{{repairInfo.category}}</text>
</view>
<view class="info-item">
<text class="info-label">宿舍楼</text>
<text class="info-value">{{repairInfo.dormitory}}</text>
</view>
<view class="info-item">
<text class="info-label">房间号</text>
<text class="info-value">{{repairInfo.room}}</text>
</view>
<view class="info-item">
<text class="info-label">联系人</text>
<text class="info-value">{{repairInfo.contact}}</text>
</view>
<view class="info-item">
<text class="info-label">联系电话</text>
<text class="info-value">{{repairInfo.phone}}</text>
</view>
<view class="info-item">
<text class="info-label">提交时间</text>
<text class="info-value">{{formatTime(repairInfo.createTime)}}</text>
</view>
<view class="info-item" wx:if="{{repairInfo.assignedTech}}">
<text class="info-label">分配维修员</text>
<text class="info-value">{{repairInfo.assignedTech}}</text>
</view>
</view>
</view>
<!-- 问题描述卡片 -->
<view class="card">
<view class="card-header">
<text class="card-title">问题描述</text>
</view>
<view class="description">
{{repairInfo.description || '暂无描述'}}
</view>
</view>
<!-- 图片展示 -->
<view class="card" wx:if="{{repairInfo.images && repairInfo.images.length > 0}}">
<view class="card-header">
<text class="card-title">现场图片</text>
</view>
<view class="image-grid">
<view wx:for="{{repairInfo.images}}" wx:key="index" class="image-item">
<image src="{{item}}" mode="aspectFill" bindtap="previewImage" data-index="{{index}}"></image>
</view>
</view>
</view>
<!-- 处理记录卡片 -->
<view class="card" wx:if="{{repairInfo.processRecords && repairInfo.processRecords.length > 0}}">
<view class="card-header">
<text class="card-title">处理记录</text>
</view>
<view class="records-list">
<view wx:for="{{repairInfo.processRecords}}" wx:key="index" class="record-item">
<view class="record-time">{{formatTime(item.time)}}</view>
<view class="record-content">{{item.content}}</view>
</view>
</view>
</view>
<view class="bottom-space"></view>
</scroll-view>
<!-- 操作按钮区域 -->
<view class="action-bar" wx:if="{{showActions}}">
<button class="action-btn primary" bindtap="changeStatus" data-status="processing">标记为处理中</button>
<button class="action-btn success" bindtap="changeStatus" data-status="completed">标记为已完成</button>
</view>
</view>

@ -0,0 +1,172 @@
/* admin/repair-detail.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
.card {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.card-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.status-badge {
padding: 6rpx 16rpx;
border-radius: 6rpx;
font-size: 24rpx;
}
.status-badge.pending {
background-color: #fff3e0;
color: #ff9800;
}
.status-badge.processing {
background-color: #e3f2fd;
color: #2196f3;
}
.status-badge.completed {
background-color: #e8f5e9;
color: #4caf50;
}
.info-grid {
display: flex;
flex-wrap: wrap;
}
.info-item {
width: 50%;
margin-bottom: 20rpx;
box-sizing: border-box;
}
.info-item:nth-child(odd) {
padding-right: 20rpx;
}
.info-item:nth-child(even) {
padding-left: 20rpx;
}
.info-label {
display: block;
font-size: 26rpx;
color: #999;
margin-bottom: 8rpx;
}
.info-value {
display: block;
font-size: 28rpx;
color: #333;
word-break: break-all;
}
.description {
font-size: 28rpx;
color: #333;
line-height: 44rpx;
word-break: break-all;
}
.image-grid {
display: flex;
flex-wrap: wrap;
margin: 0 -10rpx;
}
.image-item {
width: 33.33%;
padding: 10rpx;
box-sizing: border-box;
}
.image-item image {
width: 100%;
aspect-ratio: 1;
border-radius: 8rpx;
}
.records-list {
margin-top: 10rpx;
}
.record-item {
margin-bottom: 20rpx;
padding-bottom: 20rpx;
border-bottom: 1rpx solid #f5f5f5;
}
.record-item:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.record-time {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
display: block;
}
.record-content {
font-size: 28rpx;
color: #666;
line-height: 40rpx;
}
.bottom-space {
height: 120rpx;
}
.action-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
display: flex;
gap: 20rpx;
}
.action-btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
font-size: 32rpx;
border-radius: 8rpx;
}
.action-btn.primary {
background-color: #07c160;
color: #fff;
}
.action-btn.success {
background-color: #2196f3;
color: #fff;
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "报修单管理",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,164 @@
// admin/repairs-list.ts
import { listRepairs, RepairItem } from '../../utils/report'
Page({
data: {
repairsList: [],
currentFilter: 'all',
searchKeyword: '',
loading: false
},
onLoad() {
// 检查管理员登录状态
this.checkAdminLogin()
// 加载报修单数据
this.loadRepairsData()
},
// 检查管理员登录状态
checkAdminLogin() {
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
}
},
// 加载报修单数据 - 使用与学生页面相同的数据源
loadRepairsData() {
this.setData({ loading: true })
try {
// 直接使用系统中统一的报修单数据
const allRepairs = listRepairs()
// 转换数据格式以匹配UI需求
const formattedRepairs = allRepairs.map((repair: RepairItem) => ({
id: repair.id,
title: repair.title,
dormitory: repair.dormBuilding,
room: repair.dormRoom,
contact: repair.contactName,
phone: repair.contactPhone,
category: repair.category,
// 转换状态值以匹配UI显示
status: this.convertStatus(repair.status),
createTime: repair.createdAt,
description: repair.description,
updatedAt: repair.updatedAt
}))
this.setData({
repairsList: formattedRepairs,
loading: false
})
} catch (error) {
console.error('加载报修单数据失败:', error)
this.setData({ loading: false })
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
// 状态转换函数将系统状态转换为UI显示状态
convertStatus(status: string): string {
const statusMap: {[key: string]: string} = {
'unassigned': 'pending',
'in_progress': 'processing',
'resolved': 'completed'
}
return statusMap[status] || 'pending'
},
// 筛选报修单
filterRepairs(e: WechatMiniprogram.CustomEvent) {
const filterType = e.currentTarget.dataset.type
this.setData({
currentFilter: filterType
})
// 重新加载数据并根据筛选条件过滤
this.loadRepairsData()
// 如果有筛选条件,进行客户端过滤
if (filterType !== 'all') {
const filteredRepairs = this.data.repairsList.filter((item: any) => {
return item.status === filterType
})
this.setData({
repairsList: filteredRepairs
})
}
},
// 搜索输入
onSearchInput(e: WechatMiniprogram.Input) {
this.setData({
searchKeyword: e.detail.value
})
},
// 搜索报修单
searchRepairs() {
const keyword = this.data.searchKeyword.trim()
if (!keyword) {
// 如果关键词为空,重新加载所有数据
this.loadRepairsData()
return
}
// 重新加载数据并根据关键词过滤
this.loadRepairsData()
const filteredRepairs = this.data.repairsList.filter((item: any) => {
return item.title.includes(keyword) ||
item.dormitory.includes(keyword) ||
item.room.includes(keyword) ||
item.contact.includes(keyword)
})
this.setData({
repairsList: filteredRepairs
})
},
// 查看报修单详情
viewRepairDetail(e: WechatMiniprogram.CustomEvent) {
const repairId = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/admin/repair-detail?id=${repairId}`
})
},
// 获取状态文本
getStatusText(status: string) {
switch(status) {
case 'pending':
return '待处理'
case 'processing':
return '处理中'
case 'completed':
return '已完成'
default:
return '未知状态'
}
},
// 获取状态样式类
getStatusClass(status: string) {
return status
},
// 格式化时间
formatTime(timestamp: number) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
})

@ -0,0 +1,49 @@
<!-- admin/repairs-list.wxml -->
<navigation-bar title="报修单管理" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<view class="filter-bar">
<view class="filter-item {{currentFilter === 'all' ? 'active' : ''}}" bindtap="filterRepairs" data-type="all">全部</view>
<view class="filter-item {{currentFilter === 'pending' ? 'active' : ''}}" bindtap="filterRepairs" data-type="pending">待处理</view>
<view class="filter-item {{currentFilter === 'processing' ? 'active' : ''}}" bindtap="filterRepairs" data-type="processing">处理中</view>
<view class="filter-item {{currentFilter === 'completed' ? 'active' : ''}}" bindtap="filterRepairs" data-type="completed">已完成</view>
</view>
<view class="search-bar">
<input
class="search-input"
placeholder="搜索报修单(标题/宿舍)"
bindinput="onSearchInput"
value="{{searchKeyword}}"
/>
<view class="search-btn" bindtap="searchRepairs">搜索</view>
</view>
<scroll-view class="repairs-list" scroll-y>
<block wx:if="{{repairsList.length > 0}}">
<view wx:for="{{repairsList}}" wx:key="id" class="repair-item" bindtap="viewRepairDetail" data-id="{{item.id}}">
<view class="repair-header">
<text class="repair-title">{{item.title}}</text>
<view class="repair-status {{getStatusClass(item.status)}}">{{getStatusText(item.status)}}</view>
</view>
<view class="repair-info">
<text class="info-item">宿舍:{{item.dormitory}}</text>
<text class="info-item">房间:{{item.room}}</text>
<text class="info-item">联系人:{{item.contact}}</text>
<text class="info-item">电话:{{item.phone}}</text>
</view>
<view class="repair-footer">
<text class="repair-time">{{formatTime(item.createTime)}}</text>
<text class="repair-category">类别:{{item.category}}</text>
</view>
</view>
</block>
<view wx:else class="empty">
<text class="empty-text">暂无报修单</text>
</view>
</scroll-view>
<view class="loading" wx:if="{{loading}}">
<text>加载中...</text>
</view>
</view>

@ -0,0 +1,161 @@
/* admin/repairs-list.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
.filter-bar {
display: flex;
background-color: #fff;
border-radius: 8rpx;
padding: 0 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.filter-item {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
position: relative;
}
.filter-item.active {
color: #07c160;
}
.filter-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 30%;
width: 40%;
height: 4rpx;
background-color: #07c160;
}
.search-bar {
display: flex;
background-color: #fff;
border-radius: 8rpx;
padding: 10rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.search-input {
flex: 1;
height: 60rpx;
font-size: 28rpx;
padding: 0 20rpx;
}
.search-btn {
padding: 0 30rpx;
height: 60rpx;
line-height: 60rpx;
background-color: #07c160;
color: #fff;
border-radius: 6rpx;
font-size: 28rpx;
}
.repairs-list {
height: calc(100vh - 320rpx);
}
.repair-item {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.repair-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.repair-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
}
.repair-status {
padding: 6rpx 16rpx;
border-radius: 6rpx;
font-size: 24rpx;
margin-left: 20rpx;
}
.repair-status.pending {
background-color: #fff3e0;
color: #ff9800;
}
.repair-status.processing {
background-color: #e3f2fd;
color: #2196f3;
}
.repair-status.completed {
background-color: #e8f5e9;
color: #4caf50;
}
.repair-info {
display: flex;
flex-wrap: wrap;
margin-bottom: 20rpx;
}
.info-item {
font-size: 28rpx;
color: #666;
margin-right: 30rpx;
margin-bottom: 10rpx;
}
.repair-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.repair-time {
font-size: 24rpx;
color: #999;
}
.repair-category {
font-size: 24rpx;
color: #999;
}
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 300rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
.loading {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 28rpx;
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "维修人员管理",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,205 @@
// admin/tech-management.ts
import { TechnicianAccount, readAccounts } from '../../utils/techAuth'
Page({
data: {
technicians: [],
searchKeyword: '',
loading: false
},
onLoad() {
// 检查管理员登录状态
this.checkAdminLogin()
// 加载维修人员数据
this.loadTechnicians()
},
// 检查管理员登录状态
checkAdminLogin() {
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
}
},
// 加载维修人员数据 - 使用系统中已注册的维修人员
loadTechnicians() {
this.setData({ loading: true })
try {
// 从系统中获取所有注册的维修人员
const allTechnicians = readAccounts()
// 转换数据格式以匹配UI需求
const formattedTechnicians = allTechnicians.map((tech: TechnicianAccount) => ({
id: tech.id,
name: tech.name,
account: tech.phone, // 使用手机号作为账号显示
password: tech.password,
phone: tech.phone,
area: this.getAssignedArea(tech.id), // 模拟分配区域
status: 'active', // 默认为正常状态
registerTime: tech.createdAt
}))
this.setData({
technicians: formattedTechnicians,
loading: false
})
} catch (error) {
console.error('加载维修人员数据失败:', error)
this.setData({ loading: false })
wx.showToast({ title: '加载失败', icon: 'none' })
}
},
// 获取维修人员分配区域(模拟)
getAssignedArea(techId: string): string {
// 简单的模拟逻辑,实际应该从后端获取
const areaMap: {[key: string]: string} = {
't_1': 'A栋、B栋',
't_2': 'C栋、D栋',
't_3': 'E栋、F栋'
}
// 如果没有预定义区域,返回默认区域
return areaMap[techId] || '全部区域'
},
// 搜索输入
onSearchInput(e: WechatMiniprogram.Input) {
this.setData({
searchKeyword: e.detail.value
})
},
// 搜索维修人员
searchTechnicians() {
const keyword = this.data.searchKeyword.trim()
if (!keyword) {
// 如果关键词为空,重新加载所有数据
this.loadTechnicians()
return
}
// 重新加载数据并根据关键词过滤
this.loadTechnicians()
const filteredTechnicians = this.data.technicians.filter((tech: any) => {
return tech.name.includes(keyword) ||
tech.phone.includes(keyword) ||
tech.account.includes(keyword)
})
this.setData({
technicians: filteredTechnicians
})
},
// 添加维修人员
addTechnician() {
wx.navigateTo({
url: '/pages/admin/add-tech'
})
},
// 编辑维修人员
editTechnician(e: WechatMiniprogram.CustomEvent) {
const techId = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/admin/edit-tech?id=${techId}`
})
},
// 切换维修人员状态(封禁/解封)
toggleStatus(e: WechatMiniprogram.CustomEvent) {
const techId = e.currentTarget.dataset.id
const currentStatus = e.currentTarget.dataset.status
const newStatus = currentStatus === 'active' ? 'banned' : 'active'
const actionText = currentStatus === 'active' ? '封禁' : '解封'
wx.showModal({
title: `确认${actionText}`,
content: `确定要${actionText}该维修人员账号吗?`,
success: (res) => {
if (res.confirm) {
this.setData({ loading: true })
// 模拟更新状态实际项目中应该调用后端API
setTimeout(() => {
const updatedTechnicians = this.data.technicians.map((tech: any) => {
if (tech.id === techId) {
return { ...tech, status: newStatus }
}
return tech
})
this.setData({
technicians: updatedTechnicians,
loading: false
})
wx.showToast({
title: `${actionText}成功`,
icon: 'success'
})
}, 500)
}
}
})
},
// 删除维修人员
deleteTechnician(e: WechatMiniprogram.CustomEvent) {
const techId = e.currentTarget.dataset.id
const techName = e.currentTarget.dataset.name
wx.showModal({
title: '确认删除',
content: `确定要删除维修人员"${techName}"吗?此操作不可撤销。`,
success: (res) => {
if (res.confirm) {
this.setData({ loading: true })
// 模拟删除操作实际项目中应该调用后端API
setTimeout(() => {
const updatedTechnicians = this.data.technicians.filter((tech: any) => tech.id !== techId)
this.setData({
technicians: updatedTechnicians,
loading: false
})
wx.showToast({
title: '删除成功',
icon: 'success'
})
}, 500)
}
}
})
},
// 查看维修人员接单情况
viewTechOrders(e: WechatMiniprogram.CustomEvent) {
const techId = e.currentTarget.dataset.id;
const techName = e.currentTarget.dataset.name;
wx.navigateTo({
url: `/pages/admin/tech-orders?id=${techId}&name=${encodeURIComponent(techName)}`
})
},
// 格式化时间
formatTime(timestamp: number) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
})

@ -0,0 +1,81 @@
<!-- admin/tech-management.wxml -->
<navigation-bar title="维修人员管理" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<view class="search-bar">
<input
class="search-input"
placeholder="搜索维修人员"
bindinput="onSearchInput"
value="{{searchKeyword}}"
/>
<view class="search-btn" bindtap="searchTechnicians">搜索</view>
</view>
<view class="actions">
<button type="primary" size="mini" bindtap="addTechnician">添加维修人员</button>
</view>
<scroll-view class="tech-list" scroll-y>
<block wx:if="{{technicians.length > 0}}">
<view wx:for="{{technicians}}" wx:key="id" class="tech-item">
<view class="tech-info">
<view class="tech-header">
<text class="tech-name">{{item.name}}</text>
<view class="tech-status {{item.status === 'active' ? 'active' : 'banned'}}">{{item.status === 'active' ? '正常' : '已封禁'}}</view>
</view>
<view class="tech-details">
<text class="detail-item">账号:{{item.account}}</text>
<text class="detail-item">电话:{{item.phone}}</text>
<text class="detail-item">负责区域:{{item.area}}</text>
<text class="detail-item">注册时间:{{formatTime(item.registerTime)}}</text>
</view>
</view>
<view class="tech-actions">
<button
size="mini"
class="action-btn view-orders"
bindtap="viewTechOrders"
data-id="{{item.id}}"
data-name="{{item.name}}"
>
查看接单
</button>
<button
size="mini"
class="action-btn edit"
bindtap="editTechnician"
data-id="{{item.id}}"
>
编辑
</button>
<button
size="mini"
class="action-btn {{item.status === 'active' ? 'ban' : 'unban'}}"
bindtap="toggleStatus"
data-id="{{item.id}}"
data-status="{{item.status}}"
>
{{item.status === 'active' ? '封禁' : '解封'}}
</button>
<button
size="mini"
class="action-btn delete"
bindtap="deleteTechnician"
data-id="{{item.id}}"
data-name="{{item.name}}"
>
删除
</button>
</view>
</view>
</block>
<view wx:else class="empty">
<text class="empty-text">暂无维修人员</text>
</view>
</scroll-view>
<view class="loading" wx:if="{{loading}}">
<text>加载中...</text>
</view>
</view>

@ -0,0 +1,157 @@
/* admin/tech-management.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
.search-bar {
display: flex;
background-color: #fff;
border-radius: 8rpx;
padding: 10rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.search-input {
flex: 1;
height: 60rpx;
font-size: 28rpx;
padding: 0 20rpx;
}
.search-btn {
padding: 0 30rpx;
height: 60rpx;
line-height: 60rpx;
background-color: #07c160;
color: #fff;
border-radius: 6rpx;
font-size: 28rpx;
}
.actions {
margin-bottom: 20rpx;
display: flex;
justify-content: flex-start;
}
.tech-list {
height: calc(100vh - 280rpx);
}
.tech-item {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.tech-info {
margin-bottom: 20rpx;
}
.tech-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.tech-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.tech-status {
padding: 6rpx 16rpx;
border-radius: 6rpx;
font-size: 24rpx;
}
.tech-status.active {
background-color: #e8f5e9;
color: #4caf50;
}
.tech-status.banned {
background-color: #ffebee;
color: #f44336;
}
.tech-details {
display: flex;
flex-wrap: wrap;
}
.detail-item {
font-size: 26rpx;
color: #666;
margin-right: 40rpx;
margin-bottom: 10rpx;
}
.tech-actions {
display: flex;
justify-content: flex-end;
gap: 10rpx;
flex-wrap: wrap;
}
.action-btn {
font-size: 24rpx;
padding: 0 20rpx;
}
.action-btn.view {
background-color: #e3f2fd;
color: #2196f3;
border-color: #e3f2fd;
}
.action-btn.edit {
background-color: #fff3e0;
color: #ff9800;
border-color: #fff3e0;
}
.action-btn.ban {
background-color: #ffebee;
color: #f44336;
border-color: #ffebee;
}
.action-btn.unban {
background-color: #e8f5e9;
color: #4caf50;
border-color: #e8f5e9;
}
.action-btn.delete {
background-color: #ffebee;
color: #f44336;
border-color: #ffebee;
}
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 300rpx;
}
.empty-text {
font-size: 28rpx;
color: #999;
}
.loading {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 28rpx;
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "维修人员接单情况",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,294 @@
// admin/tech-orders.ts
import { listRepairs, RepairItem } from '../../utils/report'
import { readAccounts, TechnicianAccount } from '../../utils/techAuth'
Page({
data: {
techId: '',
techName: '',
technician: {
id: '',
name: '',
account: '',
phone: '',
area: ''
},
stats: {
total: 0,
pending: 0,
processing: 0,
completed: 0
},
ordersList: [],
currentFilter: 'all',
loading: false
},
onLoad(options: any) {
// 检查管理员登录状态
this.checkAdminLogin()
// 获取维修人员ID和名称同时支持techId和id参数
const techId = options.id || options.techId
const techName = options.name || ''
if (techId) {
this.setData({
techId: techId,
techName: techName
})
// 加载维修人员信息和接单情况
this.loadTechInfoAndOrders()
}
},
// 检查管理员登录状态
checkAdminLogin() {
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
}
},
// 加载维修人员信息和接单情况
loadTechInfoAndOrders() {
this.setData({ loading: true })
try {
// 获取所有维修人员账号
const allTechnicians = readAccounts()
// 根据ID查找对应的维修人员
const technicianInfo = allTechnicians.find((tech: TechnicianAccount) => tech.id === this.data.techId)
// 构建维修人员信息对象
let technician = {
id: this.data.techId,
name: technicianInfo ? technicianInfo.name : '未知维修员',
account: technicianInfo ? `tech_${technicianInfo.phone.slice(-4)}` : '',
phone: technicianInfo ? technicianInfo.phone : '',
area: '默认区域' // 由于实际数据中没有负责区域,这里设置为默认值
}
// 获取所有报修单
const allRepairs = listRepairs()
// 过滤出该维修人员负责的报修单
const technicianRepairs = allRepairs.filter((repair: RepairItem) =>
repair.technicianId === this.data.techId
)
// 将报修单转换为界面所需的格式
const ordersList = technicianRepairs.map((repair: RepairItem) => ({
id: repair.id,
title: repair.title,
dormitory: repair.dormBuilding,
room: repair.dormRoom,
contact: repair.contactName,
phone: repair.contactPhone,
category: repair.category,
status: this.convertStatus(repair.status),
createTime: repair.createdAt,
assignedTime: repair.updatedAt,
completedTime: repair.status === 'resolved' ? repair.updatedAt : undefined
}))
// 计算统计数据
const stats = {
total: ordersList.length,
pending: ordersList.filter((order: any) => order.status === 'pending').length,
processing: ordersList.filter((order: any) => order.status === 'processing').length,
completed: ordersList.filter((order: any) => order.status === 'completed').length
}
// 如果没有找到数据,添加一些模拟数据作为示例
if (ordersList.length === 0) {
const mockOrders = [
{
id: 'demo_1',
title: '水龙头漏水',
dormitory: 'A栋',
room: '305',
contact: '张三',
phone: '13800138000',
category: '水电维修',
status: 'pending',
createTime: Date.now() - 3600000
}
]
this.setData({
technician,
ordersList: mockOrders,
stats: {
total: 1,
pending: 1,
processing: 0,
completed: 0
},
loading: false
})
} else {
this.setData({
technician,
ordersList,
stats,
loading: false
})
}
} catch (error) {
console.error('加载数据失败:', error)
this.setData({
loading: false
})
wx.showToast({
title: '加载失败',
icon: 'none'
})
}
},
// 筛选订单
filterOrders(e: WechatMiniprogram.CustomEvent) {
const filterType = e.currentTarget.dataset.type
this.setData({
currentFilter: filterType
})
try {
// 获取所有维修人员账号
const allTechnicians = readAccounts()
const technicianInfo = allTechnicians.find((tech: TechnicianAccount) => tech.id === this.data.techId)
let technician = {
id: this.data.techId,
name: technicianInfo ? technicianInfo.name : '未知维修员',
account: technicianInfo ? `tech_${technicianInfo.phone.slice(-4)}` : '',
phone: technicianInfo ? technicianInfo.phone : '',
area: '默认区域'
}
// 获取所有报修单
const allRepairs = listRepairs()
// 过滤出该维修人员负责的报修单
let technicianRepairs = allRepairs.filter((repair: RepairItem) =>
repair.technicianId === this.data.techId
)
// 根据筛选条件进一步过滤
if (filterType !== 'all') {
// 将筛选条件转换为系统中的状态定义
let statusToFilter: string
switch(filterType) {
case 'pending':
statusToFilter = 'unassigned'
break
case 'processing':
statusToFilter = 'in_progress'
break
case 'completed':
statusToFilter = 'resolved'
break
default:
statusToFilter = ''
}
if (statusToFilter) {
technicianRepairs = technicianRepairs.filter((repair: RepairItem) =>
repair.status === statusToFilter
)
}
}
// 将报修单转换为界面所需的格式
const ordersList = technicianRepairs.map((repair: RepairItem) => ({
id: repair.id,
title: repair.title,
dormitory: repair.dormBuilding,
room: repair.dormRoom,
contact: repair.contactName,
phone: repair.contactPhone,
category: repair.category,
status: this.convertStatus(repair.status),
createTime: repair.createdAt,
assignedTime: repair.updatedAt,
completedTime: repair.status === 'resolved' ? repair.updatedAt : undefined
}))
// 计算统计数据
const stats = {
total: ordersList.length,
pending: ordersList.filter((order: any) => order.status === 'pending').length,
processing: ordersList.filter((order: any) => order.status === 'processing').length,
completed: ordersList.filter((order: any) => order.status === 'completed').length
}
this.setData({
technician,
ordersList,
stats,
loading: false
})
} catch (error) {
console.error('筛选数据失败:', error)
wx.showToast({
title: '筛选失败',
icon: 'none'
})
}
},
// 查看订单详情
viewOrderDetail(e: WechatMiniprogram.CustomEvent) {
const orderId = e.currentTarget.dataset.id
wx.navigateTo({
url: `/pages/admin/repair-detail?id=${orderId}`
})
},
// 转换状态格式,使其与系统中的状态定义匹配
convertStatus(status: string): string {
switch(status) {
case 'unassigned':
return 'pending'
case 'in_progress':
return 'processing'
case 'resolved':
return 'completed'
default:
return 'pending'
}
},
// 获取状态文本
getStatusText(status: string) {
switch(status) {
case 'pending':
return '待处理'
case 'processing':
return '处理中'
case 'completed':
return '已完成'
default:
return '未知状态'
}
},
// 获取状态样式类
getStatusClass(status: string) {
return status
},
// 格式化时间
formatTime(timestamp: number) {
const date = new Date(timestamp)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hour}:${minute}`
}
})

@ -0,0 +1,68 @@
<!-- admin/tech-orders.wxml -->
<navigation-bar title="维修人员接单情况" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<!-- 维修人员信息卡片 -->
<view class="tech-card">
<text class="tech-name">{{technician.name}}</text>
<text class="tech-info">账号:{{technician.account}}</text>
<text class="tech-info">电话:{{technician.phone}}</text>
<text class="tech-info">负责区域:{{technician.area}}</text>
</view>
<!-- 统计信息 -->
<view class="stats-container">
<view class="stat-item">
<text class="stat-number">{{stats.total}}</text>
<text class="stat-label">总接单</text>
</view>
<view class="stat-item">
<text class="stat-number">{{stats.pending}}</text>
<text class="stat-label">待处理</text>
</view>
<view class="stat-item">
<text class="stat-number">{{stats.processing}}</text>
<text class="stat-label">处理中</text>
</view>
<view class="stat-item">
<text class="stat-number">{{stats.completed}}</text>
<text class="stat-label">已完成</text>
</view>
</view>
<!-- 筛选栏 -->
<view class="filter-bar">
<view class="filter-item {{currentFilter === 'all' ? 'active' : ''}}" bindtap="filterOrders" data-type="all">全部</view>
<view class="filter-item {{currentFilter === 'pending' ? 'active' : ''}}" bindtap="filterOrders" data-type="pending">待处理</view>
<view class="filter-item {{currentFilter === 'processing' ? 'active' : ''}}" bindtap="filterOrders" data-type="processing">处理中</view>
<view class="filter-item {{currentFilter === 'completed' ? 'active' : ''}}" bindtap="filterOrders" data-type="completed">已完成</view>
</view>
<!-- 订单列表 -->
<scroll-view class="orders-list" scroll-y>
<block wx:if="{{ordersList.length > 0}}">
<view wx:for="{{ordersList}}" wx:key="id" class="order-item" bindtap="viewOrderDetail" data-id="{{item.id}}">
<view class="order-header">
<text class="order-title">{{item.title}}</text>
<view class="order-status {{getStatusClass(item.status)}}">{{getStatusText(item.status)}}</view>
</view>
<view class="order-info">
<text class="info-item">宿舍:{{item.dormitory}}</text>
<text class="info-item">房间:{{item.room}}</text>
<text class="info-item">联系人:{{item.contact}}</text>
</view>
<view class="order-footer">
<text class="order-time">{{formatTime(item.createTime)}}</text>
<text class="order-category">类别:{{item.category}}</text>
</view>
</view>
</block>
<view wx:else class="empty">
<text class="empty-text">暂无接单记录</text>
</view>
</scroll-view>
<view class="loading" wx:if="{{loading}}">
<text>加载中...</text>
</view>
</view>

@ -0,0 +1,183 @@
/* admin/tech-orders.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
/* 维修人员信息卡片 */
.tech-card {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
}
.tech-name {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 16rpx;
display: block;
}
.tech-info {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
/* 统计信息 */
.stats-container {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
display: flex;
justify-content: space-between;
}
.stat-item {
flex: 1;
text-align: center;
}
.stat-number {
display: block;
font-size: 40rpx;
font-weight: bold;
color: #1989fa;
margin-bottom: 8rpx;
}
.stat-label {
display: block;
font-size: 24rpx;
color: #666;
}
/* 筛选栏 */
.filter-bar {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 30rpx;
display: flex;
justify-content: space-around;
}
.filter-item {
padding: 16rpx 24rpx;
font-size: 28rpx;
color: #666;
border-radius: 6rpx;
}
.filter-item.active {
background-color: #1989fa;
color: #fff;
}
/* 订单列表 */
.orders-list {
height: 80vh;
}
.order-item {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.order-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.order-status {
padding: 6rpx 16rpx;
border-radius: 4rpx;
font-size: 24rpx;
}
.order-status.pending {
background-color: #fff7e6;
color: #fa8c16;
}
.order-status.processing {
background-color: #e6f7ff;
color: #1890ff;
}
.order-status.completed {
background-color: #f6ffed;
color: #52c41a;
}
.order-info {
margin-bottom: 20rpx;
}
.info-item {
font-size: 28rpx;
color: #666;
margin-right: 30rpx;
}
.order-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.order-time {
font-size: 24rpx;
color: #999;
}
.order-category {
font-size: 24rpx;
color: #999;
}
/* 空状态 */
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
}
.empty-text {
font-size: 32rpx;
color: #999;
}
/* 加载状态 */
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.5);
padding: 20rpx 40rpx;
border-radius: 8rpx;
color: #fff;
font-size: 28rpx;
}

@ -0,0 +1,8 @@
{
"navigationBarTitleText": "用户管理",
"navigationBarBackgroundColor": "#fff",
"navigationBarTextStyle": "black",
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,190 @@
// admin/user-management.ts
Page({
data: {
usersList: [],
searchKeyword: '',
currentFilter: 'all',
loading: false,
defaultAvatarUrl: 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
},
onLoad() {
// 检查管理员登录状态
this.checkAdminLogin()
// 加载用户列表
this.loadUsers()
},
// 检查管理员登录状态
checkAdminLogin() {
const isLoggedIn = wx.getStorageSync('adminLoggedIn')
if (!isLoggedIn) {
wx.redirectTo({
url: '/pages/admin/login'
})
}
},
// 加载用户列表
loadUsers() {
this.setData({ loading: true })
// 模拟获取用户数据实际项目中应该调用后端API
setTimeout(() => {
const mockUsers = [
{
id: '1',
name: '张三',
studentId: '2021001',
phone: '13800138000',
dormitory: 'A栋',
room: '305',
status: 'active',
avatar: ''
},
{
id: '2',
name: '李四',
studentId: '2021002',
phone: '13800138001',
dormitory: 'B栋',
room: '402',
status: 'active',
avatar: ''
},
{
id: '3',
name: '王五',
studentId: '2021003',
phone: '13800138002',
dormitory: 'C栋',
room: '203',
status: 'banned',
avatar: ''
},
{
id: '4',
name: '赵六',
studentId: '2021004',
phone: '13800138003',
dormitory: 'D栋',
room: '501',
status: 'active',
avatar: ''
},
{
id: '5',
name: '钱七',
studentId: '2021005',
phone: '13800138004',
dormitory: 'A栋',
room: '106',
status: 'active',
avatar: ''
}
]
this.setData({
usersList: mockUsers,
loading: false
})
}, 1000)
},
// 搜索输入处理
onSearchInput(e: WechatMiniprogram.Input) {
this.setData({
searchKeyword: e.detail.value
})
},
// 搜索用户
handleSearch() {
const keyword = this.data.searchKeyword.trim()
if (keyword) {
// 根据关键词过滤用户实际项目中应该调用后端API进行搜索
const filteredUsers = this.data.usersList.filter((user: any) =>
user.name.includes(keyword) ||
user.phone.includes(keyword) ||
user.studentId.includes(keyword)
)
this.setData({
usersList: filteredUsers
})
} else {
// 如果关键词为空,重新加载所有用户
this.loadUsers()
}
},
// 筛选用户
filterUsers(e: WechatMiniprogram.CustomEvent) {
const filterType = e.currentTarget.dataset.type
this.setData({
currentFilter: filterType
})
// 根据筛选条件过滤用户
if (filterType === 'all') {
this.loadUsers() // 重新加载所有用户
} else {
const filteredUsers = this.data.usersList.filter((user: any) => user.status === filterType)
this.setData({
usersList: filteredUsers
})
}
},
// 查看用户详情
viewUserInfo(e: WechatMiniprogram.CustomEvent) {
const userId = e.currentTarget.dataset.id
// 可以跳转到用户详情页面或显示弹窗
wx.showModal({
title: '查看用户信息',
content: '此功能正在开发中',
showCancel: false
})
},
// 查看用户报修记录
viewRepairs(e: WechatMiniprogram.CustomEvent) {
const userId = e.currentTarget.dataset.id
// 跳转到用户报修记录页面
wx.navigateTo({
url: `/pages/admin/repairs-list?userId=${userId}`
})
},
// 切换用户状态(封禁/解封)
toggleUserStatus(e: WechatMiniprogram.CustomEvent) {
const userId = e.currentTarget.dataset.id
const currentStatus = e.currentTarget.dataset.status
const newStatus = currentStatus === 'active' ? 'banned' : 'active'
wx.showModal({
title: currentStatus === 'active' ? '确认封禁' : '确认解封',
content: currentStatus === 'active' ? '封禁后用户将无法使用系统' : '解封后用户可以正常使用系统',
success: (res) => {
if (res.confirm) {
// 更新用户状态实际项目中应该调用后端API
const updatedUsers = this.data.usersList.map((user: any) => {
if (user.id === userId) {
return { ...user, status: newStatus }
}
return user
})
this.setData({
usersList: updatedUsers
})
wx.showToast({
title: currentStatus === 'active' ? '封禁成功' : '解封成功',
icon: 'success'
})
}
}
})
}
})

@ -0,0 +1,55 @@
<!-- admin/user-management.wxml -->
<navigation-bar title="用户管理" back="{{false}}" color="black" background="#FFF"></navigation-bar>
<view class="container">
<!-- 搜索栏 -->
<view class="search-bar">
<view class="search-input">
<text class="search-icon">🔍</text>
<input placeholder="搜索用户名或电话" value="{{searchKeyword}}" bindinput="onSearchInput" />
</view>
<button size="mini" bindtap="handleSearch" class="search-btn">搜索</button>
</view>
<!-- 筛选栏 -->
<view class="filter-bar">
<view class="filter-item {{currentFilter === 'all' ? 'active' : ''}}" bindtap="filterUsers" data-type="all">全部</view>
<view class="filter-item {{currentFilter === 'active' ? 'active' : ''}}" bindtap="filterUsers" data-type="active">活跃</view>
<view class="filter-item {{currentFilter === 'banned' ? 'active' : ''}}" bindtap="filterUsers" data-type="banned">已封禁</view>
</view>
<!-- 用户列表 -->
<scroll-view class="users-list" scroll-y>
<block wx:if="{{usersList.length > 0}}">
<view wx:for="{{usersList}}" wx:key="id" class="user-item">
<view class="user-info">
<view class="avatar" bindtap="viewUserInfo" data-id="{{item.id}}">
<image src="{{item.avatar || defaultAvatarUrl}}" mode="aspectFill"></image>
</view>
<view class="basic-info">
<text class="user-name">{{item.name}}</text>
<text class="user-meta">学号:{{item.studentId}}</text>
<text class="user-meta">宿舍:{{item.dormitory}} {{item.room}}</text>
</view>
<view class="status-tag {{item.status === 'active' ? 'active' : 'banned'}}">
{{item.status === 'active' ? '活跃' : '已封禁'}}
</view>
</view>
<view class="action-buttons">
<button size="mini" bindtap="viewRepairs" data-id="{{item.id}}" class="action-btn info">报修记录</button>
<button size="mini" bindtap="toggleUserStatus" data-id="{{item.id}}" data-status="{{item.status}}"
class="action-btn {{item.status === 'active' ? 'warning' : 'primary'}}">
{{item.status === 'active' ? '封禁' : '解封'}}
</button>
</view>
</view>
</block>
<view wx:else class="empty">
<text class="empty-text">暂无用户数据</text>
</view>
</scroll-view>
<view class="loading" wx:if="{{loading}}">
<text>加载中...</text>
</view>
</view>

@ -0,0 +1,188 @@
/* admin/user-management.wxss */
.container {
padding: 30rpx;
background-color: #f8f8f8;
min-height: 100vh;
box-sizing: border-box;
}
/* 搜索栏 */
.search-bar {
display: flex;
align-items: center;
margin-bottom: 30rpx;
}
.search-input {
flex: 1;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 8rpx;
padding: 0 20rpx;
margin-right: 20rpx;
height: 80rpx;
border: 2rpx solid #e0e0e0;
}
.search-icon {
font-size: 28rpx;
margin-right: 10rpx;
color: #999;
}
.search-input input {
flex: 1;
height: 100%;
font-size: 28rpx;
}
.search-btn {
height: 80rpx;
line-height: 80rpx;
font-size: 28rpx;
background-color: #1989fa;
color: #fff;
}
/* 筛选栏 */
.filter-bar {
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 30rpx;
display: flex;
justify-content: space-around;
}
.filter-item {
padding: 16rpx 24rpx;
font-size: 28rpx;
color: #666;
border-radius: 6rpx;
}
.filter-item.active {
background-color: #1989fa;
color: #fff;
}
/* 用户列表 */
.users-list {
height: 80vh;
}
.user-item {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 20rpx;
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 20rpx;
}
.avatar {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 20rpx;
}
.avatar image {
width: 100%;
height: 100%;
}
.basic-info {
flex: 1;
}
.user-name {
display: block;
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.user-meta {
display: block;
font-size: 26rpx;
color: #666;
margin-bottom: 4rpx;
}
.status-tag {
padding: 6rpx 16rpx;
border-radius: 4rpx;
font-size: 24rpx;
}
.status-tag.active {
background-color: #f6ffed;
color: #52c41a;
}
.status-tag.banned {
background-color: #fff1f0;
color: #ff4d4f;
}
.action-buttons {
display: flex;
justify-content: flex-end;
}
.action-btn {
margin-left: 20rpx;
font-size: 26rpx;
padding: 0 30rpx;
height: 70rpx;
line-height: 70rpx;
}
.action-btn.info {
background-color: #e6f7ff;
color: #1890ff;
}
.action-btn.warning {
background-color: #fff7e6;
color: #fa8c16;
}
.action-btn.primary {
background-color: #f6ffed;
color: #52c41a;
}
/* 空状态 */
.empty {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
}
.empty-text {
font-size: 32rpx;
color: #999;
}
/* 加载状态 */
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.5);
padding: 20rpx 40rpx;
border-radius: 8rpx;
color: #fff;
font-size: 28rpx;
}

@ -0,0 +1,5 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,216 @@
// index.ts
// 获取应用实例
const app = getApp<IAppOption>()
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
import { getRole, setRole } from '../../utils/role'
import { isLoggedIn, logout, clearAllAccounts } from '../../utils/techAuth'
Component({
data: {
motto: '报修系统',
userInfo: {
avatarUrl: defaultAvatarUrl,
nickName: '',
},
hasUserInfo: false,
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
userRole: 'student' as 'student' | 'technician',
isTechLoggedIn: false,
},
lifetimes: {
attached() {
this.checkUserRole()
}
},
pageLifetimes: {
show() {
this.checkUserRole()
}
},
methods: {
checkUserRole() {
const role = getRole()
const isTechLoggedIn = isLoggedIn()
// 检查是否有保存的登录状态
const lastLoginRole = wx.getStorageSync('last_login_role')
const lastLoginTime = wx.getStorageSync('last_login_time')
// 如果当前是学生角色但有维修人员登录记录且登录时间在24小时内则自动恢复
if (role === 'student' && lastLoginRole === 'technician' && isTechLoggedIn) {
const now = Date.now()
const oneDay = 24 * 60 * 60 * 1000 // 24小时
if (lastLoginTime && (now - lastLoginTime) < oneDay) {
setRole('technician')
wx.showToast({ title: '欢迎回来,维修人员', icon: 'success' })
}
}
// 重新获取角色状态
const currentRole = getRole()
const currentTechLoggedIn = isLoggedIn()
this.setData({
userRole: currentRole,
isTechLoggedIn: currentTechLoggedIn
})
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '/pages/logs/logs',
})
},
// 跳转到管理员登录页面
goToAdmin: function() {
wx.navigateTo({
url: '/pages/admin/login'
});
},
goToReport() {
if (this.data.userRole === 'technician') {
wx.showToast({ title: '维修人员不能提交报修', icon: 'none' })
return
}
wx.navigateTo({ url: '/pages/report/form' })
},
goToMyRepairs() {
wx.navigateTo({ url: '/pages/repairs/list' })
},
goToTech() {
if (this.data.isTechLoggedIn) {
wx.navigateTo({ url: '/pages/tech/list' })
} else {
wx.navigateTo({ url: '/pages/tech/auth' })
}
},
switchToStudent() {
setRole('student')
// 保存学生角色状态
wx.setStorageSync('last_login_role', 'student')
wx.setStorageSync('last_login_time', Date.now())
wx.showToast({ title: '已切换到学生模式', icon: 'success' })
this.checkUserRole()
},
showIdentitySwitch() {
const currentRole = this.data.userRole
const isTechLoggedIn = this.data.isTechLoggedIn
if (currentRole === 'student') {
// 学生切换到维修人员 - 直接跳转到维修人员界面
wx.navigateTo({
url: '/pages/tech/auth',
success: () => {
console.log('跳转到维修人员界面')
},
fail: (err) => {
console.error('跳转失败:', err)
}
})
} else {
// 维修人员切换回学生
if (isTechLoggedIn) {
wx.showActionSheet({
itemList: ['切换回学生身份', '退出维修人员登录'],
success: (res) => {
if (res.tapIndex === 0) {
this.switchToStudent()
} else if (res.tapIndex === 1) {
this.showLogoutConfirm()
}
}
})
} else {
this.switchToStudent()
}
}
},
// 显示管理员入口提示
showAdminAccessConfirm: function() {
wx.showModal({
title: '管理员入口',
content: '这是管理员专用入口,输入管理员账号密码后可进入管理系统',
showCancel: true,
cancelText: '取消',
confirmText: '进入',
success: function(res) {
if (res.confirm) {
this.goToAdmin();
}
}.bind(this)
});
},
showLogoutConfirm() {
wx.showModal({
title: '退出登录',
content: '确定要退出维修人员账号吗?',
confirmText: '确定退出',
cancelText: '取消',
confirmColor: '#dc3545',
success: (res) => {
if (res.confirm) {
this.doLogout()
}
}
})
},
doLogout() {
logout()
setRole('student')
// 清除登录状态
wx.removeStorageSync('last_login_role')
wx.removeStorageSync('last_login_time')
wx.showToast({ title: '已退出登录', icon: 'success' })
this.checkUserRole()
},
onChooseAvatar(e: any) {
const { avatarUrl } = e.detail
const { nickName } = this.data.userInfo
this.setData({
"userInfo.avatarUrl": avatarUrl,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
onInputChange(e: any) {
const nickName = e.detail.value
const { avatarUrl } = this.data.userInfo
this.setData({
"userInfo.nickName": nickName,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
getUserProfile() {
// 推荐使用wx.getUserProfile获取用户信息开发者每次通过该接口获取用户个人信息均需用户确认开发者妥善保管用户快速填写的头像昵称避免重复弹窗
wx.getUserProfile({
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log(res)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
},
clearAllTechAccounts() {
wx.showModal({
title: '清除所有维修人员账号',
content: '确定要删除所有已注册的维修人员账号吗?此操作不可恢复!',
confirmText: '确定删除',
cancelText: '取消',
confirmColor: '#dc3545',
success: (res) => {
if (res.confirm) {
clearAllAccounts()
wx.showToast({ title: '所有账号已清除', icon: 'success' })
// 刷新页面状态
this.checkUserRole()
}
}
})
}
},
})

@ -0,0 +1,62 @@
<!--index.wxml-->
<navigation-bar title="Weixin" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y type="list">
<view class="container">
<view class="userinfo">
<block wx:if="{{canIUseNicknameComp && !hasUserInfo}}">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
</button>
<view class="nickname-wrapper">
<text class="nickname-label">昵称</text>
<input type="text" class="nickname-input" placeholder="请输入昵称(支持中文)" bindinput="onInputChange" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
</view>
</block>
<block wx:elif="{{!hasUserInfo}}">
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
<view wx:else> 请使用2.10.4及以上版本基础库 </view>
</block>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>
</view>
<view class="usermotto">
<text class="user-motto">{{motto}}</text>
</view>
<view class="entry">
<block wx:if="{{userRole === 'student'}}">
<button class="primary" size="mini" type="primary" bindtap="goToReport">提交报修</button>
<button size="mini" bindtap="goToMyRepairs">我的报修</button>
</block>
<block wx:elif="{{userRole === 'technician'}}">
<button size="mini" bindtap="goToMyRepairs">报修列表</button>
<button size="mini" type="primary" bindtap="goToTech">维修管理</button>
</block>
</view>
<!-- 身份切换区域 -->
<view class="identity-section">
<view class="current-identity">
<text class="identity-label">当前身份:</text>
<text class="identity-value">{{userRole === 'student' ? '学生' : '维修人员'}}</text>
<text class="login-status" wx:if="{{userRole === 'technician' && isTechLoggedIn}}">(已登录)</text>
</view>
<view class="identity-actions">
<button class="switch-btn" size="mini" bindtap="showIdentitySwitch">切换身份</button>
</view>
<view class="identity-actions">
<button class="clear-btn" size="mini" bindtap="clearAllTechAccounts">清除维修人员账号</button>
</view>
<!-- 管理员入口 -->
<view class="admin-section">
<view class="admin-entry-row">
<text class="admin-hint">管理系统</text>
<button class="admin-btn" size="mini" type="warn" bindtap="showAdminAccessConfirm">管理员入口</button>
</view>
</view>
</view>
</view>
</scroll-view>

@ -0,0 +1,183 @@
/**index.wxss**/
page {
height: 100vh;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
}
.scrollarea {
flex: 1;
overflow-y: hidden;
}
.userinfo {
display: flex;
flex-direction: column;
align-items: center;
color: #aaa;
width: 80%;
}
.userinfo-avatar {
overflow: hidden;
width: 128rpx;
height: 128rpx;
margin: 20rpx;
border-radius: 50%;
}
.usermotto {
margin-top: 40px;
}
.avatar-wrapper {
padding: 0;
width: 56px !important;
border-radius: 8px;
margin-top: 40px;
margin-bottom: 40px;
}
.avatar {
display: block;
width: 56px;
height: 56px;
}
.nickname-wrapper {
display: flex;
width: 100%;
padding: 16px;
box-sizing: border-box;
border-top: .5px solid rgba(0, 0, 0, 0.1);
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
color: black;
}
.nickname-label {
width: 105px;
}
.nickname-input {
flex: 1;
}
.entry {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.entry button {
border-radius: 6px;
}
.entry button[size="mini"] {
font-size: 12px;
padding: 6px 12px;
}
/* 身份切换区域样式 */
.identity-section {
margin-top: 24px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.current-identity {
display: flex;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
.identity-label {
font-size: 14px;
color: #666;
margin-right: 8px;
}
.identity-value {
font-size: 14px;
font-weight: 600;
color: #1677ff;
margin-right: 8px;
}
.login-status {
font-size: 12px;
color: #22c55e;
}
.identity-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.switch-btn {
background: #1677ff;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
padding: 6px 12px;
}
.clear-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
padding: 6px 12px;
margin-top: 16px;
}
.login-btn {
background: #28a745;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
padding: 6px 12px;
}
.logout-btn {
background: #dc3545;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
padding: 6px 12px;
}
/* 管理员入口样式 */
.admin-section {
margin-top: 16px;
}
.admin-entry-row {
display: flex;
align-items: center;
gap: 12px;
}
.admin-hint {
font-size: 14px;
color: #666;
display: inline-flex;
align-items: center;
}
.admin-btn {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 12px;
padding: 6px 12px;
}

@ -0,0 +1,5 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,21 @@
// logs.ts
// const util = require('../../utils/util.js')
import { formatTime } from '../../utils/util'
Component({
data: {
logs: [],
},
lifetimes: {
attached() {
this.setData({
logs: (wx.getStorageSync('logs') || []).map((log: string) => {
return {
date: formatTime(new Date(log)),
timeStamp: log
}
}),
})
}
},
})

@ -0,0 +1,7 @@
<!--logs.wxml-->
<navigation-bar title="查看启动日志" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y type="list">
<block wx:for="{{logs}}" wx:key="timeStamp" wx:for-item="log">
<view class="log-item">{{index + 1}}. {{log.date}}</view>
</block>
</scroll-view>

@ -0,0 +1,16 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
}
.scrollarea {
flex: 1;
overflow-y: hidden;
}
.log-item {
margin-top: 20rpx;
text-align: center;
}
.log-item:last-child {
padding-bottom: env(safe-area-inset-bottom);
}

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "报修详情"
}

@ -0,0 +1,147 @@
import { getRepairById, updateRepairStatus, RepairStatus } from '../../utils/report'
import { getRole } from '../../utils/role'
import { isLoggedIn } from '../../utils/techAuth'
Component({
data: {
item: null as any,
userRole: 'student' as 'student' | 'technician',
canChangeStatus: false,
rating: 0,
},
lifetimes: {
attached() {
this.checkPermissions()
this.load()
}
},
methods: {
checkPermissions() {
const role = getRole()
const isTechLoggedIn = isLoggedIn()
const canChangeStatus = role === 'technician' && isTechLoggedIn
this.setData({
userRole: role,
canChangeStatus: canChangeStatus
})
},
onShow() {
this.checkPermissions()
this.load()
},
load() {
const id = this.getPageId()
if (!id) return
const item = getRepairById(id)
if (!item) {
wx.showToast({ title: '记录不存在', icon: 'none' })
setTimeout(() => wx.navigateBack(), 500)
return
}
// 检查是否已经评价过
const ratings = wx.getStorageSync('repair_ratings') || {}
const existingRating = ratings[id]
if (existingRating) {
item.rated = true
item.rating = existingRating.rating
this.setData({ rating: existingRating.rating })
}
this.setData({ item })
},
getPageId(): string | undefined {
// @ts-ignore
const page = getCurrentPages().slice(-1)[0]
// skyline 环境下使用 options
// @ts-ignore
return page && page.options && page.options.id
},
changeStatus(e: any) {
if (!this.data.canChangeStatus) {
wx.showToast({ title: '只有维修人员可以更改状态', icon: 'none' })
return
}
const status = e.currentTarget.dataset.status as RepairStatus
const id = this.data.item && this.data.item.id
if (!id) return
const updated = updateRepairStatus(id, status)
if (updated) {
this.setData({ item: updated })
wx.showToast({ title: '状态更新成功', icon: 'success' })
// 如果是维修人员更改状态,延迟返回上一页
setTimeout(() => {
wx.navigateBack()
}, 1000)
}
},
previewImage(e: any) {
const current = e.currentTarget.dataset.src
const urls = e.currentTarget.dataset.urls
console.log('图片预览参数:', { current, urls })
if (!current) {
wx.showToast({ title: '图片路径无效', icon: 'none' })
return
}
// 确保urls是数组格式
let urlArray = []
if (Array.isArray(urls)) {
urlArray = urls
} else if (typeof urls === 'string') {
try {
urlArray = JSON.parse(urls)
} catch {
urlArray = [urls]
}
} else {
urlArray = [current]
}
wx.previewImage({
current: current,
urls: urlArray,
success: () => {
console.log('图片预览成功')
},
fail: (err) => {
console.error('图片预览失败:', err)
wx.showToast({ title: '图片预览失败', icon: 'none' })
}
})
},
setRating(e: any) {
const rating = e.currentTarget.dataset.rating
this.setData({ rating })
},
submitRating() {
const { rating, item } = this.data
if (!rating || !item) return
// 保存评价到本地存储
const ratings = wx.getStorageSync('repair_ratings') || {}
ratings[item.id] = {
rating: rating,
technicianId: item.technicianId,
technicianName: item.technicianName,
repairId: item.id,
repairTitle: item.title,
ratedAt: Date.now()
}
wx.setStorageSync('repair_ratings', ratings)
// 更新维修记录,标记为已评价
const updatedItem = { ...item, rated: true, rating: rating }
this.setData({ item: updatedItem })
wx.showToast({
title: '评价提交成功',
icon: 'success'
})
}
}
})

@ -0,0 +1,82 @@
<navigation-bar title="报修详情" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y>
<block wx:if="{{item}}">
<view class="card">
<view class="title">{{item.title}}</view>
<view class="row">宿舍:{{item.dormBuilding}}-{{item.dormRoom}}</view>
<view class="row">类别:{{item.category}}</view>
<view class="row">联系人:{{item.contactName}}{{item.contactPhone}}</view>
<view class="row">状态:{{item.status === 'unassigned' ? '待分配' : (item.status === 'in_progress' ? '处理中' : '已解决')}}</view>
<view class="row" wx:if="{{item.technicianName}}">维修人员:{{item.technicianName}}</view>
<view class="row" wx:if="{{item.updatedAt}}">更新时间:{{item.updatedAt}}</view>
<view class="row">描述:{{item.description || '无'}}</view>
<view class="row" wx:if="{{item.selfHelpTips && item.selfHelpTips.length}}">
<view>自助尝试:</view>
<view class="tips-list">
<block wx:for="{{item.selfHelpTips}}" wx:key="*this">
<view class="tip-item">{{item}}</view>
</block>
</view>
</view>
<view class="photos" wx:if="{{item.photos && item.photos.length}}">
<view class="photos-title">相关图片:</view>
<view class="photos-grid">
<block wx:for="{{item.photos}}" wx:key="*this">
<image class="photo" src="{{item}}" mode="aspectFill" bindtap="previewImage" data-src="{{item}}" data-urls="{{item.photos}}" />
</block>
</view>
<view class="photos-tip" wx:if="{{userRole === 'technician'}}">点击图片可放大查看</view>
</view>
</view>
<view class="actions" wx:if="{{canChangeStatus}}">
<button size="mini" data-status="submitted" bindtap="changeStatus">标记为已提交</button>
<button size="mini" data-status="in_progress" bindtap="changeStatus">处理中</button>
<button size="mini" type="primary" data-status="resolved" bindtap="changeStatus">已解决</button>
</view>
<view class="actions" wx:elif="{{userRole === 'student'}}">
<text class="readonly-tip">只有维修人员可以更改状态</text>
</view>
<!-- 维修评价区域 -->
<view class="rating-section" wx:if="{{userRole === 'student' && item.status === 'resolved' && item.technicianName}}">
<view class="rating-title">维修评价</view>
<view class="rating-content">
<view class="rating-label">维修人员:{{item.technicianName}}</view>
<!-- 已评价状态 -->
<view wx:if="{{item.rated}}" class="rated-status">
<view class="stars-display">
<block wx:for="{{[1,2,3,4,5]}}" wx:key="*this">
<text class="star active">★</text>
</block>
</view>
<view class="rating-result">
您已评价:{{item.rating}}星 - {{item.rating === 1 ? '很差' : (item.rating === 2 ? '较差' : (item.rating === 3 ? '一般' : (item.rating === 4 ? '满意' : '非常满意')))}}
</view>
</view>
<!-- 未评价状态 -->
<view wx:else>
<view class="rating-label">请为维修人员评分:</view>
<view class="stars">
<block wx:for="{{[1,2,3,4,5]}}" wx:key="*this">
<text class="star {{rating >= item ? 'active' : ''}}"
data-rating="{{item}}"
bindtap="setRating">★</text>
</block>
</view>
<view class="rating-text" wx:if="{{rating}}">
{{rating === 1 ? '很差' : (rating === 2 ? '较差' : (rating === 3 ? '一般' : (rating === 4 ? '满意' : '非常满意')))}}
</view>
<view class="rating-actions" wx:if="{{rating}}">
<button class="submit-rating-btn" size="mini" bindtap="submitRating">提交评价</button>
</view>
<view class="rating-tip" wx:if="{{!rating}}">点击星星进行评分</view>
</view>
</view>
</view>
</block>
</scroll-view>

@ -0,0 +1,147 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
}
.scrollarea { flex: 1; }
.card { padding: 16px; }
.title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
.row { color: #444; margin: 6px 0; }
.photos { margin-top: 8px; }
.photos-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.photos-grid {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.photo {
width: 96px;
height: 96px;
border-radius: 6px;
cursor: pointer;
transition: transform 0.2s ease;
}
.photo:active {
transform: scale(0.95);
}
/* 维修评价样式 */
.rating-section {
margin: 16px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.rating-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.rating-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.rating-label {
font-size: 14px;
color: #666;
}
.stars {
display: flex;
gap: 4px;
align-items: center;
}
.star {
font-size: 24px;
color: #ddd;
cursor: pointer;
transition: color 0.2s ease;
}
.star.active {
color: #ffc107;
}
.stars-display {
display: flex;
gap: 4px;
align-items: center;
}
.stars-display .star {
font-size: 20px;
color: #ffc107;
}
.rating-text {
font-size: 14px;
color: #1677ff;
font-weight: 500;
}
.rating-actions {
margin-top: 8px;
}
.submit-rating-btn {
background: #1677ff;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
padding: 6px 16px;
}
.rating-tip {
font-size: 12px;
color: #999;
text-align: center;
}
.rated-status {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
}
.rating-result {
font-size: 14px;
color: #28a745;
font-weight: 500;
text-align: center;
}
.photos-tip {
font-size: 12px;
color: #999;
text-align: center;
margin-top: 4px;
}
.actions { padding: 12px 16px; display: flex; gap: 8px; }
.tips-list { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
.tip-item { background: #f7f8fa; border: 1px solid #eef0f3; padding: 8px 10px; border-radius: 6px; color: #333; }
.readonly-tip {
color: #999;
font-size: 12px;
text-align: center;
padding: 12px;
background: #f5f5f5;
border-radius: 6px;
}

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "我的报修"
}

@ -0,0 +1,41 @@
import { listRepairs } from '../../utils/report'
import { getRole } from '../../utils/role'
import { isLoggedIn } from '../../utils/techAuth'
Component({
data: {
repairs: [] as ReturnType<typeof listRepairs>,
userRole: 'student' as 'student' | 'technician',
isTechLoggedIn: false,
},
lifetimes: {
attached() {
this.checkUserRole()
this.refresh()
}
},
methods: {
checkUserRole() {
const role = getRole()
const isTechLoggedIn = isLoggedIn()
this.setData({
userRole: role,
isTechLoggedIn: isTechLoggedIn
})
},
onShow() {
this.checkUserRole()
this.refresh()
},
refresh() {
this.setData({ repairs: listRepairs() })
},
toDetail(e: any) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/repairs/detail?id=${id}` })
}
}
})

@ -0,0 +1,19 @@
<navigation-bar title="{{userRole === 'student' ? '我的报修' : '报修列表'}}" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y>
<view class="list">
<block wx:if="{{repairs.length}}">
<block wx:for="{{repairs}}" wx:key="id">
<view class="item" data-id="{{item.id}}" bindtap="toDetail">
<view class="title">{{item.title}}</view>
<view class="meta">{{item.dormBuilding}}-{{item.dormRoom}} {{item.category}} {{item.status}}
</view>
<view class="meta" wx:if="{{userRole === 'technician'}}">联系人:{{item.contactName}}{{item.contactPhone}}</view>
</view>
</block>
</block>
<view wx:else class="empty">{{userRole === 'student' ? '暂无报修' : '暂无报修记录'}}</view>
</view>
</scroll-view>

@ -0,0 +1,15 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
}
.scrollarea { flex: 1; }
.list { padding: 12px; }
.item { padding: 12px; border-bottom: 1px solid #f0f0f0; }
.title { font-weight: 600; color: #222; margin-bottom: 6px; }
.meta { color: #666; font-size: 12px; }
.empty { padding: 24px; color: #999; text-align: center; }

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "提交报修"
}

@ -0,0 +1,125 @@
import { createRepair } from '../../utils/report'
import { getRole } from '../../utils/role'
Component({
data: {
title: '',
dormBuilding: '',
dormRoom: '',
contactName: '',
contactPhone: '',
category: '',
categories: ['水电','网络','家具','其他'] as string[],
description: '',
photos: [] as string[],
step: 'form' as 'form' | 'tips',
tips: [] as string[],
userRole: 'student' as 'student' | 'technician',
},
lifetimes: {
attached() {
this.checkUserRole()
}
},
methods: {
checkUserRole() {
const role = getRole()
this.setData({ userRole: role })
if (role === 'technician') {
wx.showToast({ title: '维修人员不能提交报修', icon: 'none' })
setTimeout(() => wx.navigateBack(), 1000)
}
},
onInput(e: any) {
const key = e.currentTarget.dataset.key
const value = e && e.detail && e.detail.value
this.setData({ [key]: value })
},
onCategoryChange(e: any) {
const idx = Number((e && e.detail && e.detail.value) || 0)
const val = this.data.categories[idx]
this.setData({ category: val })
},
chooseImage() {
wx.chooseMedia({
count: 3,
mediaType: ['image'],
success: (res) => {
const paths = res.tempFiles.map(f => f.tempFilePath)
const photos = Array.from(new Set([...(this.data.photos || []), ...paths]))
this.setData({ photos })
}
})
},
removePhoto(e: any) {
const idx = e.currentTarget.dataset.index
const photos = (this.data.photos || []).slice()
photos.splice(idx, 1)
this.setData({ photos })
},
submit() {
const { title, dormBuilding, dormRoom, contactName, contactPhone, category } = this.data as any
if (!title || !dormBuilding || !dormRoom || !contactName || !contactPhone || !category) {
wx.showToast({ title: '请完善必填项', icon: 'none' })
return
}
if (!/^1\d{10}$/.test(contactPhone)) {
wx.showToast({ title: '手机号格式不正确', icon: 'none' })
return
}
// 第一阶段:先展示自助排查建议
const tips = this.getTipsByCategory(category)
this.setData({ tips, step: 'tips' })
}
,
finalSubmit() {
const { title, dormBuilding, dormRoom, contactName, contactPhone, category, description, photos } = this.data as any
const created = createRepair({
title,
dormBuilding,
dormRoom,
contactName,
contactPhone,
category,
description: description || '',
photos: photos || [],
selfHelpTips: this.data.tips || [],
})
wx.showToast({ title: '提交成功', icon: 'success' })
setTimeout(() => {
wx.redirectTo({ url: `/pages/repairs/detail?id=${created.id}` })
}, 400)
}
,
solvedBySelf() {
wx.showToast({ title: '已解决,感谢反馈', icon: 'none' })
setTimeout(() => wx.navigateBack(), 500)
}
,
getTipsByCategory(cat: string): string[] {
const map: Record<string, string[]> = {
'水电': [
'确认是否总闸关闭或跳闸,尝试合闸',
'检查同寝室其他插座是否正常,排除单点损坏',
'如有漏水,先关闭阀门并清理地面积水,避免触电风险'
],
'网络': [
'断开并重新连接宿舍 Wi-Fi',
'重启路由器,等待 2-3 分钟再测试',
'确认宿舍是否欠费或校园网维护公告'
],
'家具': [
'确认是否为松动螺丝,可用螺丝刀轻微紧固',
'避免重压或继续使用以免扩大损坏'
],
'其他': [
'尝试简单复位或重启相关设备',
'确保人身安全,必要时先远离风险源'
]
}
return map[cat] || map['其他']
}
}
})

@ -0,0 +1,63 @@
<navigation-bar title="提交报修" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y>
<view wx:if="{{step==='form'}}" class="form">
<view class="field">
<text class="label">问题标题</text>
<input class="input" placeholder="如:水龙头漏水(支持中文)" data-key="title" bindinput="onInput" type="text" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
</view>
<view class="field">
<text class="label">宿舍楼</text>
<input class="input" placeholder="如A栋支持中文" data-key="dormBuilding" bindinput="onInput" type="text" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
</view>
<view class="field">
<text class="label">房间号</text>
<input class="input" placeholder="如305支持中文" data-key="dormRoom" bindinput="onInput" type="text" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
</view>
<view class="field">
<text class="label">联系人</text>
<input class="input" placeholder="姓名(支持中文)" data-key="contactName" bindinput="onInput" type="text" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
</view>
<view class="field">
<text class="label">联系电话</text>
<input class="input" placeholder="手机号" type="number" data-key="contactPhone" bindinput="onInput" />
</view>
<view class="field">
<text class="label">类别</text>
<picker range="{{categories}}" bindchange="onCategoryChange">
<view class="picker">{{category || '请选择类别'}}</view>
</picker>
</view>
<view class="field">
<text class="label">问题描述</text>
<textarea class="textarea" placeholder="补充详细信息(支持中文)" auto-height data-key="description" bindinput="onInput" hold-keyboard="{{true}}" adjust-position="{{true}}" />
</view>
<view class="field">
<text class="label">照片</text>
<view class="photos">
<block wx:for="{{photos}}" wx:key="*this">
<view class="photo-item">
<image class="photo" src="{{item}}" mode="aspectFill" />
<text class="remove" data-index="{{index}}" bindtap="removePhoto">删除</text>
</view>
</block>
<button class="choose" size="mini" bindtap="chooseImage">选择照片</button>
</view>
</view>
<button class="submit" type="primary" bindtap="submit">下一步:查看自助建议</button>
</view>
<view wx:elif="{{step==='tips'}}" class="tips">
<view class="tips-title">根据您选择的类别,建议尝试:</view>
<view class="tips-list">
<block wx:for="{{tips}}" wx:key="*this">
<view class="tip-item">{{item}}</view>
</block>
</view>
<view class="tips-actions">
<button size="mini" bindtap="solvedBySelf">已解决</button>
<button size="mini" type="primary" bindtap="finalSubmit">我要报修</button>
</view>
</view>
</scroll-view>

@ -0,0 +1,41 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
}
.scrollarea {
flex: 1;
padding-top: 0;
}
.form {
padding: 16px;
}
.field { margin-bottom: 16px; }
.label { display: block; margin-bottom: 8px; color: #333; }
.input { padding: 8px 12px; border: 1px solid #eee; border-radius: 6px; }
.textarea { padding: 8px 12px; border: 1px solid #eee; border-radius: 6px; }
.photos { display: flex; flex-wrap: wrap; align-items: center; }
.photos .photo-item { margin: 0 8px 8px 0; }
.photo-item { position: relative; }
.photo { width: 96px; height: 96px; border-radius: 6px; }
.remove { position: absolute; right: 4px; top: 4px; color: #fff; background: rgba(0,0,0,.4); font-size: 12px; padding: 2px 4px; border-radius: 4px; }
.choose { margin-top: 8px; }
.submit { margin-top: 16px; }
.tips { padding: 16px; }
.tips-title { color: #222; margin-bottom: 12px; font-weight: 600; }
.tips-list { display: flex; flex-direction: column; }
.tip-item { background: #f7f8fa; border: 1px solid #eef0f3; padding: 10px 12px; border-radius: 6px; color: #333; }
.tips-list .tip-item { margin-top: 8px; }
.tips-actions { margin-top: 16px; display: flex; }
.tips-actions button { margin-right: 8px; }

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "维修人员登录/注册"
}

@ -0,0 +1,141 @@
import { login, registerAccount, isLoggedIn, getCurrentAccount, logout } from '../../utils/techAuth'
import { getRole, setRole } from '../../utils/role'
Component({
data: {
tab: 'login' as 'login' | 'register',
loginPhone: '',
loginPassword: '',
regName: '',
regPhone: '',
regPassword: '',
account: null as any,
},
lifetimes: {
attached() {
this.checkUrlParams()
this.checkUserRole()
this.checkAutoLogin()
if (isLoggedIn()) {
this.setData({ account: getCurrentAccount() })
}
}
},
methods: {
checkUserRole() {
// 允许所有用户访问维修人员入口进行角色切换
},
checkAutoLogin() {
// 检查是否有保存的登录状态
const lastLoginRole = wx.getStorageSync('last_login_role')
const lastLoginTime = wx.getStorageSync('last_login_time')
// 如果上次登录是维修人员且登录时间在24小时内且当前已登录则自动跳转
if (lastLoginRole === 'technician' && isLoggedIn()) {
const now = Date.now()
const oneDay = 24 * 60 * 60 * 1000 // 24小时
if (lastLoginTime && (now - lastLoginTime) < oneDay) {
wx.showToast({ title: '自动登录成功', icon: 'success' })
setTimeout(() => wx.redirectTo({ url: '/pages/tech/list' }), 500)
}
}
},
checkUrlParams() {
// 检查URL参数决定显示注册还是登录页面
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options || {}
console.log('URL参数:', options)
if (options.tab === 'register') {
console.log('切换到注册页面')
this.setData({ tab: 'register' })
} else if (options.tab === 'login') {
console.log('切换到登录页面')
this.setData({ tab: 'login' })
} else {
console.log('使用默认登录页面')
this.setData({ tab: 'login' })
}
},
switchTab(e: any) {
const tab = e.currentTarget.dataset.tab
this.setData({ tab })
},
onInput(e: any) {
const key = e.currentTarget.dataset.key
this.setData({ [key]: e.detail.value })
},
doLogin() {
const { loginPhone, loginPassword } = this.data as any
const res = login(loginPhone, loginPassword)
if (!res.ok) return wx.showToast({ title: res.message, icon: 'none' })
// 设置用户角色为维修人员
setRole('technician')
// 保存登录状态到本地存储
wx.setStorageSync('last_login_role', 'technician')
wx.setStorageSync('last_login_time', Date.now())
wx.showToast({ title: '登录成功', icon: 'success' })
setTimeout(() => wx.redirectTo({ url: '/pages/tech/list' }), 400)
},
doRegister() {
const { regName, regPhone, regPassword } = this.data as any
if (!regName || !regPhone || !regPassword) return wx.showToast({ title: '请完善信息', icon: 'none' })
const res = registerAccount(regName, regPhone, regPassword)
if (!res.ok) return wx.showToast({ title: res.message, icon: 'none' })
// 设置用户角色为维修人员
setRole('technician')
// 保存登录状态到本地存储
wx.setStorageSync('last_login_role', 'technician')
wx.setStorageSync('last_login_time', Date.now())
wx.showToast({ title: '注册并登录成功', icon: 'success' })
setTimeout(() => wx.redirectTo({ url: '/pages/tech/list' }), 400)
},
doLogout() {
logout()
// 重置用户角色为学生
setRole('student')
// 清除登录状态
wx.removeStorageSync('last_login_role')
wx.removeStorageSync('last_login_time')
this.setData({ account: null })
wx.showToast({ title: '已退出', icon: 'none' })
},
switchToStudent() {
// 切换到学生身份
setRole('student')
// 保存学生角色状态
wx.setStorageSync('last_login_role', 'student')
wx.setStorageSync('last_login_time', Date.now())
wx.showToast({ title: '已切换到学生模式', icon: 'success' })
// 返回主界面
setTimeout(() => {
const pages = getCurrentPages()
console.log('认证页面返回 - 页面栈长度:', pages.length)
if (pages.length > 1) {
wx.navigateBack({
success: () => {
console.log('认证页面返回成功')
},
fail: (err) => {
console.error('认证页面返回失败:', err)
// 如果返回失败,尝试跳转到主页面
wx.redirectTo({
url: '/pages/index/index'
})
}
})
} else {
// 如果页面栈只有一页,直接跳转到主页面
wx.redirectTo({
url: '/pages/index/index'
})
}
}, 500)
}
}
})

@ -0,0 +1,41 @@
<navigation-bar title="维修人员界面" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y>
<view class="container">
<block wx:if="{{account}}">
<view class="panel">
<view class="row">当前账号:{{account.name}}{{account.phone}}</view>
<button bindtap="doLogout">退出登录</button>
<button type="primary" bindtap="doLogin">进入工单</button>
</view>
</block>
<block wx:else>
<view class="panel" wx:if="{{tab==='login'}}">
<input class="input" placeholder="手机号" type="number" data-key="loginPhone" bindinput="onInput" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
<input class="input" placeholder="密码" password="{{true}}" data-key="loginPassword" bindinput="onInput" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
<button class="login-btn" type="primary" bindtap="doLogin">登录</button>
<button class="register-btn" data-tab="register" bindtap="switchTab">没有账号?去注册</button>
</view>
<view class="panel" wx:elif="{{tab==='register'}}">
<input class="input" placeholder="姓名" data-key="regName" bindinput="onInput" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
<input class="input" placeholder="手机号" type="number" data-key="regPhone" bindinput="onInput" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
<input class="input" placeholder="设置密码" password="{{true}}" data-key="regPassword" bindinput="onInput" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
<button type="primary" bindtap="doRegister">注册</button>
<button class="register-btn" data-tab="login" bindtap="switchTab">已有账号?去登录</button>
</view>
</block>
<!-- 身份切换区域 -->
<view class="identity-section">
<view class="current-identity">
<text class="identity-label">当前身份:</text>
<text class="identity-value">维修人员</text>
</view>
<view class="identity-actions">
<button class="switch-btn" size="mini" bindtap="switchToStudent">切换身份</button>
</view>
</view>
</view>
</scroll-view>

@ -0,0 +1,83 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
}
.container {
padding: 16px;
}
.scrollarea { flex: 1; }
.tabs { display: flex; gap: 16px; padding: 16px; border-bottom: 1px solid #f0f0f0; }
.tab { color: #333; }
.panel {
padding: 24px 16px;
display: flex;
flex-direction: column;
gap: 16px;
margin-top: 20px;
}
.input { padding: 10px 12px; border: 1px solid #eee; border-radius: 6px; }
.password-input {
-webkit-text-security: disc !important;
text-security: disc !important;
font-family: monospace !important;
letter-spacing: 4px !important;
font-size: 18px !important;
color: transparent !important;
text-shadow: 0 0 0 #000 !important;
}
.login-btn { margin-top: 24px; }
.register-btn { margin-top: 12px; color: #1677ff; background: transparent; border: none; }
/* 身份切换区域样式 */
.identity-section {
margin-top: 24px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.current-identity {
display: flex;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
}
.identity-label {
font-size: 14px;
color: #666;
margin-right: 8px;
}
.identity-value {
font-size: 14px;
font-weight: 600;
color: #1677ff;
margin-right: 8px;
}
.login-status {
font-size: 12px;
color: #22c55e;
}
.identity-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.switch-btn {
background: #1677ff;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
padding: 6px 12px;
}

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "维修工单"
}

@ -0,0 +1,246 @@
import { listRepairs, updateRepairStatus, RepairStatus, claimRepair, releaseRepairToPool } from '../../utils/report'
import { isLoggedIn, getCurrentAccount } from '../../utils/techAuth'
import { getRole } from '../../utils/role'
Component({
data: {
filter: 'pool' as 'all' | RepairStatus | 'mine' | 'pool',
repairs: [] as ReturnType<typeof listRepairs>,
counts: {
pool: 0,
mine: 0,
in_progress: 0,
resolved: 0,
all: 0,
} as any,
stats: {
total: 0,
pending: 0,
processing: 0,
completed: 0
},
showCompletion: false,
completedRepair: null as any,
currentTech: null as any,
showRatingsModal: false,
ratings: [] as any[],
averageRating: 0,
goodRatingRate: 0,
},
lifetimes: {
attached() {
this.checkUserRole()
if (!isLoggedIn()) {
wx.redirectTo({ url: '/pages/tech/auth' })
return
}
this.loadCurrentTech()
this.refresh()
}
},
methods: {
checkUserRole() {
// 允许所有用户访问维修管理,但需要先登录
},
loadCurrentTech() {
const currentTech = getCurrentAccount()
this.setData({ currentTech })
},
onShow() {
this.checkUserRole()
this.refresh()
},
goBack() {
console.log('点击返回按钮')
const pages = getCurrentPages()
console.log('当前页面栈长度:', pages.length)
if (pages.length > 1) {
wx.navigateBack({
success: () => {
console.log('返回成功')
},
fail: (err) => {
console.error('返回失败:', err)
// 如果返回失败,尝试跳转到主页面
wx.redirectTo({
url: '/pages/index/index'
})
}
})
} else {
// 如果页面栈只有一页,直接跳转到主页面
wx.redirectTo({
url: '/pages/index/index'
})
}
},
refresh() {
const all = listRepairs()
const techId = wx.getStorageSync('tech_session_id')
// 计算工单分类统计
const counts = {
pool: all.filter(r => r.status === 'unassigned').length,
mine: all.filter(r => r.technicianId === techId).length,
in_progress: all.filter(r => r.status === 'in_progress').length,
resolved: all.filter(r => r.status === 'resolved').length,
all: all.length,
}
// 计算当前维修人员的接单统计
const myRepairs = all.filter(r => r.technicianId === techId)
const stats = {
total: myRepairs.length,
pending: myRepairs.filter(r => r.status === 'unassigned').length,
processing: myRepairs.filter(r => r.status === 'in_progress').length,
completed: myRepairs.filter(r => r.status === 'resolved').length
}
const { filter } = this.data
let repairs = all
if (filter === 'mine') {
repairs = myRepairs
} else if (filter === 'pool') {
repairs = all.filter(r => r.status === 'unassigned')
} else if (filter !== 'all') {
repairs = all.filter(r => r.status === filter)
}
this.setData({ repairs, counts, stats })
},
setFilter(e: any) {
const filter = e.currentTarget.dataset.filter as any
this.setData({ filter }, () => this.refresh())
},
quickSet(e: any) {
const id = e.currentTarget.dataset.id
const status = e.currentTarget.dataset.status as RepairStatus
updateRepairStatus(id, status)
// 如果状态更改为已完成,显示完成页面
if (status === 'resolved') {
this.showCompletionPage(id)
} else {
this.refresh()
}
},
showCompletionPage(repairId: string) {
// 获取完成的工单信息
const allRepairs = listRepairs()
const completedRepair = allRepairs.find(r => r.id === repairId)
if (completedRepair) {
this.setData({
showCompletion: true,
completedRepair: completedRepair
})
// 3秒后自动跳转
setTimeout(() => {
this.hideCompletionPage()
}, 3000)
}
},
hideCompletionPage() {
this.setData({
showCompletion: false,
completedRepair: null
})
// 刷新页面并切换到待分配池
this.refresh()
this.setData({ filter: 'pool' }, () => this.refresh())
},
toDetail(e: any) {
const id = e.currentTarget.dataset.id
wx.navigateTo({ url: `/pages/repairs/detail?id=${id}` })
},
claim(e: any) {
const id = e.currentTarget.dataset.id
const techId = wx.getStorageSync('tech_session_id')
const accounts = wx.getStorageSync('tech_accounts') || []
const account = accounts.find((a: any) => a.id === techId)
const techName = account && account.name || '维修人员'
const res = claimRepair(id, techId, techName)
if (!res.ok) {
wx.showToast({ title: res.message, icon: 'none' })
} else {
wx.showToast({ title: '抢单成功', icon: 'success' })
}
this.refresh()
},
release(e: any) {
const id = e.currentTarget.dataset.id
const techId = wx.getStorageSync('tech_session_id')
const res = releaseRepairToPool(id, techId)
if (!res.ok) {
wx.showToast({ title: res.message, icon: 'none' })
} else {
wx.showToast({ title: '已放回待分配池', icon: 'none' })
}
this.refresh()
},
showRatings() {
this.loadRatings()
this.setData({ showRatingsModal: true })
},
hideRatings() {
this.setData({ showRatingsModal: false })
},
loadRatings() {
const currentTech = this.data.currentTech
if (!currentTech) return
// 获取所有评价数据
const allRatings = wx.getStorageSync('repair_ratings') || {}
// 筛选出当前维修人员的评价
const techRatings = Object.values(allRatings).filter((rating: any) =>
rating.technicianId === currentTech.id
)
// 格式化评价数据
const formattedRatings = techRatings.map((rating: any) => ({
...rating,
ratedAtText: this.formatDate(rating.ratedAt)
}))
// 计算统计数据
const averageRating = formattedRatings.length > 0
? (formattedRatings.reduce((sum: number, r: any) => sum + r.rating, 0) / formattedRatings.length).toFixed(1)
: 0
const goodRatings = formattedRatings.filter((r: any) => r.rating >= 4).length
const goodRatingRate = formattedRatings.length > 0
? Math.round((goodRatings / formattedRatings.length) * 100)
: 0
this.setData({
ratings: formattedRatings,
averageRating: averageRating,
goodRatingRate: goodRatingRate
})
},
formatDate(timestamp: number): string {
const date = new Date(timestamp)
const now = new Date()
const diff = now.getTime() - date.getTime()
// 如果是今天
if (diff < 24 * 60 * 60 * 1000 && date.getDate() === now.getDate()) {
return `今天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 如果是昨天
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
if (date.getDate() === yesterday.getDate()) {
return `昨天 ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
// 其他情况显示完整日期
return `${date.getMonth() + 1}/${date.getDate()} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
}
}
})

@ -0,0 +1,129 @@
<navigation-bar title="维修工单" back="{{true}}" color="black" background="#FFF"></navigation-bar>
<view class="floating-back" bindtap="goBack">返回</view>
<!-- 当前维修人员信息 -->
<view class="tech-info" wx:if="{{currentTech}}">
<view class="tech-name">当前维修人员:{{currentTech.name}}</view>
<view class="tech-phone">联系电话:{{currentTech.phone}}</view>
<view class="tech-actions">
<button class="view-ratings-btn" size="mini" bindtap="showRatings">查看评价</button>
</view>
</view>
<!-- 接单情况统计 -->
<view class="stats-container" wx:if="{{currentTech}}">
<view class="stat-title">我的接单情况</view>
<view class="stat-grid">
<view class="stat-item">
<view class="stat-number">{{stats.total}}</view>
<view class="stat-label">总接单</view>
</view>
<view class="stat-item">
<view class="stat-number">{{stats.pending}}</view>
<view class="stat-label">待处理</view>
</view>
<view class="stat-item">
<view class="stat-number">{{stats.processing}}</view>
<view class="stat-label">处理中</view>
</view>
<view class="stat-item">
<view class="stat-number">{{stats.completed}}</view>
<view class="stat-label">已完成</view>
</view>
</view>
</view>
<!-- 工单完成页面 -->
<view class="completion-overlay" wx:if="{{showCompletion}}">
<view class="completion-page">
<view class="completion-icon">✅</view>
<view class="completion-title">工单已完成</view>
<view class="completion-subtitle">任务已成功完成</view>
<view class="completion-details" wx:if="{{completedRepair}}">
<view class="detail-item">工单:{{completedRepair.title}}</view>
<view class="detail-item">位置:{{completedRepair.dormBuilding}}-{{completedRepair.dormRoom}}</view>
<view class="detail-item">类别:{{completedRepair.category}}</view>
</view>
<view class="completion-tip">3秒后自动返回抢单页面</view>
<button class="completion-btn" bindtap="hideCompletionPage">立即返回</button>
</view>
</view>
<!-- 评价查看弹窗 -->
<view class="ratings-overlay" wx:if="{{showRatingsModal}}">
<view class="ratings-modal">
<view class="ratings-header">
<view class="ratings-title">我的评价</view>
<button class="close-btn" bindtap="hideRatings">×</button>
</view>
<view class="ratings-content">
<view wx:if="{{ratings.length === 0}}" class="no-ratings">
<view class="no-ratings-icon">📝</view>
<view class="no-ratings-text">暂无评价记录</view>
<view class="no-ratings-tip">完成维修后,学生会为您评分</view>
</view>
<view wx:else class="ratings-list">
<block wx:for="{{ratings}}" wx:key="repairId">
<view class="rating-item">
<view class="rating-header">
<view class="repair-title">{{item.repairTitle}}</view>
<view class="rating-stars">
<block wx:for="{{[1,2,3,4,5]}}" wx:key="*this">
<text class="star {{item.rating >= item ? 'active' : ''}}">★</text>
</block>
</view>
</view>
<view class="rating-details">
<view class="rating-score">{{item.rating}}星 - {{item.rating === 1 ? '很差' : (item.rating === 2 ? '较差' : (item.rating === 3 ? '一般' : (item.rating === 4 ? '满意' : '非常满意')))}}
</view>
<view class="rating-time">{{item.ratedAtText}}</view>
</view>
</view>
</block>
</view>
<view wx:if="{{ratings.length > 0}}" class="ratings-summary">
<view class="summary-title">评价统计</view>
<view class="summary-content">
<view class="summary-item">总评价数:{{ratings.length}}</view>
<view class="summary-item">平均评分:{{averageRating}}星</view>
<view class="summary-item">好评率:{{goodRatingRate}}%</view>
</view>
</view>
</view>
</view>
</view>
<view class="tabs vertical">
<view class="tab card {{filter==='pool' ? 'active' : ''}}" data-filter="pool" bindtap="setFilter">待分配池({{counts.pool}}</view>
<view class="tab card {{filter==='mine' ? 'active' : ''}}" data-filter="mine" bindtap="setFilter">我的工单({{counts.mine}}</view>
<view class="tab card {{filter==='in_progress' ? 'active' : ''}}" data-filter="in_progress" bindtap="setFilter">处理中({{counts.in_progress}}</view>
<view class="tab card {{filter==='resolved' ? 'active' : ''}}" data-filter="resolved" bindtap="setFilter">已解决({{counts.resolved}}</view>
<view class="tab card {{filter==='all' ? 'active' : ''}}" data-filter="all" bindtap="setFilter">全部({{counts.all}}</view>
</view>
<scroll-view class="scrollarea" scroll-y>
<view class="list">
<block wx:if="{{repairs.length}}">
<block wx:for="{{repairs}}" wx:key="id">
<view class="item" data-id="{{item.id}}" bindtap="toDetail">
<view class="title">{{item.title}}</view>
<view class="meta">{{item.dormBuilding}}-{{item.dormRoom}} {{item.category}} {{item.status === 'unassigned' ? '待分配' : (item.status === 'in_progress' ? '处理中' : '已解决')}}
<block wx:if="{{item.technicianName}}"> 负责人:{{item.technicianName}}</block>
</view>
<view class="meta" wx:if="{{item.selfHelpTips && item.selfHelpTips.length}}">自助:{{item.selfHelpTips[0]}} 等{{item.selfHelpTips.length}}条</view>
<view class="actions">
<button wx:if="{{item.status==='unassigned'}}" size="mini" data-id="{{item.id}}" bindtap="claim">抢单</button>
<block wx:elif="{{item.status==='in_progress'}}">
<button size="mini" data-id="{{item.id}}" bindtap="release">放回池子</button>
<button size="mini" type="primary" data-id="{{item.id}}" data-status="resolved" bindtap="quickSet">完成</button>
</block>
</view>
</view>
</block>
</block>
<view wx:else class="empty">暂无工单</view>
</view>
</scroll-view>

@ -0,0 +1,378 @@
page {
height: 100vh;
display: flex;
flex-direction: column;
padding-top: env(safe-area-inset-top);
}
.tabs { display: flex; gap: 12px; padding: 60px 16px 16px 16px; border-bottom: 1px solid #f0f0f0; }
.tabs.vertical { flex-direction: column; gap: 12px; }
.tab { color: #333; }
.tab.active { color: #1677ff; font-weight: 600; border-color: #1677ff; }
.card { background: #fff; border: 1px solid #eaecef; border-radius: 8px; padding: 10px 12px; }
.scrollarea { flex: 1; }
.list { padding: 12px; }
.item { padding: 12px; border-bottom: 1px solid #f0f0f0; }
.title { font-weight: 600; color: #222; margin-bottom: 6px; }
.meta { color: #666; font-size: 12px; }
.actions { margin-top: 8px; display: flex; gap: 8px; }
.empty { padding: 24px; color: #999; text-align: center; }
.floating-back {
position: fixed;
left: 12px;
top: 8px;
z-index: 10;
padding: 6px 10px;
background: rgba(255,255,255,0.9);
border: 1px solid #eaecef;
border-radius: 14px;
color: #1677ff;
}
/* 维修人员信息样式 */
.tech-info {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 12px 16px;
margin: 12px 16px;
}
.tech-name {
font-size: 14px;
font-weight: 600;
color: #1677ff;
margin-bottom: 4px;
}
.tech-phone {
font-size: 12px;
color: #666;
}
/* 统计区域样式 */
.stats-container {
padding: 15px;
background: #fff;
margin-bottom: 10px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
border-radius: 8px;
margin: 12px 16px;
}
.stat-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.stat-grid {
display: flex;
justify-content: space-between;
}
.stat-item {
flex: 1;
text-align: center;
padding: 10px 5px;
}
.stat-number {
font-size: 24px;
font-weight: bold;
color: #07c160;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
color: #666;
}
/* 工单完成页面样式 */
.completion-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.completion-page {
background: white;
border-radius: 16px;
padding: 40px 24px;
text-align: center;
max-width: 320px;
width: 100%;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.completion-icon {
font-size: 64px;
margin-bottom: 16px;
animation: bounce 0.6s ease-in-out;
}
.completion-title {
font-size: 24px;
font-weight: 600;
color: #22c55e;
margin-bottom: 8px;
}
.completion-subtitle {
font-size: 16px;
color: #666;
margin-bottom: 24px;
}
.completion-details {
background: #f8f9fa;
border-radius: 12px;
padding: 16px;
margin-bottom: 24px;
text-align: left;
}
.detail-item {
font-size: 14px;
color: #333;
margin-bottom: 8px;
padding: 4px 0;
}
.detail-item:last-child {
margin-bottom: 0;
}
.completion-tip {
font-size: 12px;
color: #999;
margin-bottom: 16px;
}
.completion-btn {
background: #1677ff;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 16px;
font-weight: 500;
}
.completion-btn:active {
background: #0d5ae5;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
/* 评价查看功能样式 */
.tech-actions {
margin-top: 12px;
display: flex;
justify-content: center;
}
.view-ratings-btn {
background: #17a2b8;
color: white;
border: none;
border-radius: 6px;
font-size: 12px;
padding: 6px 16px;
}
.ratings-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.ratings-modal {
background: white;
border-radius: 12px;
width: 100%;
max-width: 400px;
max-height: 80vh;
overflow: hidden;
animation: slideUp 0.3s ease-out;
}
.ratings-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
}
.ratings-title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
color: #666;
padding: 0;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.ratings-content {
padding: 20px;
max-height: 60vh;
overflow-y: auto;
}
.no-ratings {
text-align: center;
padding: 40px 20px;
}
.no-ratings-icon {
font-size: 48px;
margin-bottom: 16px;
}
.no-ratings-text {
font-size: 16px;
color: #666;
margin-bottom: 8px;
}
.no-ratings-tip {
font-size: 14px;
color: #999;
}
.ratings-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.rating-item {
background: #f8f9fa;
border-radius: 8px;
padding: 16px;
border: 1px solid #e9ecef;
}
.rating-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.repair-title {
font-size: 14px;
font-weight: 500;
color: #333;
flex: 1;
}
.rating-stars {
display: flex;
gap: 2px;
}
.rating-stars .star {
font-size: 16px;
color: #ddd;
}
.rating-stars .star.active {
color: #ffc107;
}
.rating-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.rating-score {
font-size: 14px;
color: #1677ff;
font-weight: 500;
}
.rating-time {
font-size: 12px;
color: #999;
}
.ratings-summary {
margin-top: 24px;
padding: 16px;
background: #e3f2fd;
border-radius: 8px;
border: 1px solid #bbdefb;
}
.summary-title {
font-size: 16px;
font-weight: 600;
color: #1976d2;
margin-bottom: 12px;
}
.summary-content {
display: flex;
flex-direction: column;
gap: 8px;
}
.summary-item {
font-size: 14px;
color: #333;
}
@keyframes slideUp {
from {
transform: translateY(100%);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}

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

@ -0,0 +1,117 @@
// 报修数据模型与本地存储工具
export type RepairStatus = 'unassigned' | 'in_progress' | 'resolved'
export interface RepairItem {
id: string
title: string
dormBuilding: string
dormRoom: string
contactName: string
contactPhone: string
category: string
description: string
photos: string[]
selfHelpTips?: string[]
technicianId?: string
technicianName?: string
status: RepairStatus
createdAt: number
updatedAt: number
}
const STORAGE_KEY = 'repairs'
function readAllRepairs(): RepairItem[] {
try {
const list = wx.getStorageSync(STORAGE_KEY)
return Array.isArray(list) ? list : []
} catch (e) {
return []
}
}
function writeAllRepairs(list: RepairItem[]) {
wx.setStorageSync(STORAGE_KEY, list)
}
export function listRepairs(): RepairItem[] {
return readAllRepairs().sort((a, b) => b.updatedAt - a.updatedAt)
}
export function getRepairById(id: string): RepairItem | undefined {
return readAllRepairs().find(item => item.id === id)
}
export function createRepair(input: Omit<RepairItem, 'id' | 'status' | 'createdAt' | 'updatedAt'>): RepairItem {
const now = Date.now()
const newItem: RepairItem = {
...input,
id: generateId(),
status: 'unassigned',
createdAt: now,
updatedAt: now,
}
const list = readAllRepairs()
list.unshift(newItem)
writeAllRepairs(list)
return newItem
}
export function updateRepairStatus(id: string, status: RepairStatus): RepairItem | undefined {
const list = readAllRepairs()
const idx = list.findIndex(item => item.id === id)
if (idx === -1) return undefined
const updated: RepairItem = { ...list[idx], status, updatedAt: Date.now() }
list[idx] = updated
writeAllRepairs(list)
return updated
}
export function claimRepair(id: string, technicianId: string, technicianName: string): { ok: true, item: RepairItem } | { ok: false, message: string } {
const list = readAllRepairs()
const idx = list.findIndex(item => item.id === id)
if (idx === -1) return { ok: false, message: '工单不存在' }
const target = list[idx]
if (target.status !== 'unassigned') {
return { ok: false, message: '已被其他人抢单或状态已变更' }
}
const updated: RepairItem = {
...target,
technicianId,
technicianName,
status: 'in_progress',
updatedAt: Date.now(),
}
list[idx] = updated
writeAllRepairs(list)
return { ok: true, item: updated }
}
export function releaseRepairToPool(id: string, technicianId: string): { ok: true, item: RepairItem } | { ok: false, message: string } {
const list = readAllRepairs()
const idx = list.findIndex(item => item.id === id)
if (idx === -1) return { ok: false, message: '工单不存在' }
const target = list[idx]
if (target.status !== 'in_progress' || target.technicianId !== technicianId) {
return { ok: false, message: '无权限或状态不允许放回' }
}
const updated: RepairItem = {
...target,
technicianId: undefined,
technicianName: undefined,
status: 'unassigned',
updatedAt: Date.now(),
}
list[idx] = updated
writeAllRepairs(list)
return { ok: true, item: updated }
}
function generateId(): string {
// 简单本地唯一ID
return 'r_' + Math.random().toString(36).slice(2, 10) + '_' + Date.now().toString(36)
}

@ -0,0 +1,18 @@
export type UserRole = 'student' | 'technician'
const ROLE_KEY = 'user_role'
export function getRole(): UserRole {
try {
const r = wx.getStorageSync(ROLE_KEY)
return r === 'technician' ? 'technician' : 'student'
} catch {
return 'student'
}
}
export function setRole(role: UserRole) {
wx.setStorageSync(ROLE_KEY, role)
}

@ -0,0 +1,77 @@
export interface TechnicianAccount {
id: string
name: string
phone: string
password: string // demo: 明文存储,仅用于本地模拟
createdAt: number
}
const ACC_KEY = 'tech_accounts'
const SESSION_KEY = 'tech_session_id'
export function readAccounts(): TechnicianAccount[] {
try {
const list = wx.getStorageSync(ACC_KEY)
return Array.isArray(list) ? list : []
} catch {
return []
}
}
function writeAccounts(list: TechnicianAccount[]) {
wx.setStorageSync(ACC_KEY, list)
}
export function registerAccount(name: string, phone: string, password: string): { ok: true } | { ok: false, message: string } {
const list = readAccounts()
if (!/^1\d{10}$/.test(phone)) return { ok: false, message: '手机号格式不正确' }
if (list.some(a => a.phone === phone)) return { ok: false, message: '该手机号已注册' }
const acc: TechnicianAccount = { id: genId(), name, phone, password, createdAt: Date.now() }
list.push(acc)
writeAccounts(list)
wx.setStorageSync(SESSION_KEY, acc.id)
return { ok: true }
}
export function login(phone: string, password: string): { ok: true } | { ok: false, message: string } {
const list = readAccounts()
const acc = list.find(a => a.phone === phone && a.password === password)
if (!acc) return { ok: false, message: '手机号或密码错误' }
wx.setStorageSync(SESSION_KEY, acc.id)
return { ok: true }
}
export function logout() {
try { wx.removeStorageSync(SESSION_KEY) } catch {}
}
export function getCurrentAccount(): TechnicianAccount | null {
try {
const id = wx.getStorageSync(SESSION_KEY)
if (!id) return null
const list = readAccounts()
return list.find(a => a.id === id) || null
} catch {
return null
}
}
export function isLoggedIn(): boolean {
return !!getCurrentAccount()
}
function genId(): string {
return 't_' + Math.random().toString(36).slice(2, 10) + '_' + Date.now().toString(36)
}
export function clearAllAccounts(): void {
// 清除所有注册的账号
wx.removeStorageSync(ACC_KEY)
// 同时清除登录状态
wx.removeStorageSync(SESSION_KEY)
// 清除登录记录
wx.removeStorageSync('last_login_role')
wx.removeStorageSync('last_login_time')
}

@ -0,0 +1,19 @@
export const formatTime = (date: Date) => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return (
[year, month, day].map(formatNumber).join('/') +
' ' +
[hour, minute, second].map(formatNumber).join(':')
)
}
const formatNumber = (n: number) => {
const s = n.toString()
return s[1] ? s : '0' + s
}

@ -0,0 +1,15 @@
{
"name": "miniprogram-ts-less-quickstart",
"version": "1.0.0",
"description": "",
"scripts": {
},
"keywords": [],
"author": "",
"license": "",
"dependencies": {
},
"devDependencies": {
"miniprogram-api-typings": "^2.8.3-1"
}
}

@ -0,0 +1,50 @@
{
"description": "项目配置文件",
"miniprogramRoot": "miniprogram/",
"compileType": "miniprogram",
"setting": {
"useCompilerPlugins": [
"typescript"
],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"coverView": false,
"postcss": false,
"minified": false,
"enhance": false,
"showShadowRootInWxmlPanel": false,
"packNpmRelationList": [],
"ignoreUploadUnusedFiles": true,
"compileHotReLoad": false,
"skylineRenderEnable": true,
"es6": false,
"compileWorklet": false,
"uglifyFileName": false,
"uploadWithSourceMap": true,
"packNpmManually": false,
"minifyWXSS": true,
"minifyWXML": true,
"localPlugins": false,
"condition": false,
"swc": false,
"disableSWC": true,
"disableUseStrict": false
},
"simulatorType": "wechat",
"simulatorPluginLibVersion": {},
"condition": {},
"srcMiniprogramRoot": "miniprogram/",
"editorSetting": {
"tabIndent": "insertSpaces",
"tabSize": 2
},
"libVersion": "trial",
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wxdcc80031e1ba7950"
}

@ -0,0 +1,23 @@
{
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
"projectname": "%E5%AD%A6%E7%94%9F%E5%AE%BF%E8%88%8D%E6%8A%A5%E4%BF%AE%E7%B3%BB%E7%BB%9F",
"setting": {
"compileHotReLoad": true,
"urlCheck": true,
"coverView": false,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"useApiHook": true,
"useApiHostProcess": true,
"showShadowRootInWxmlPanel": false,
"useStaticServer": false,
"useLanDebug": false,
"showES6CompileOption": false,
"bigPackageSizeSupport": false,
"checkInvalidKey": true,
"ignoreDevUnusedFiles": true
},
"libVersion": "3.11.0"
}

@ -0,0 +1,30 @@
{
"compilerOptions": {
"strictNullChecks": true,
"noImplicitAny": true,
"module": "CommonJS",
"target": "ES2020",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"alwaysStrict": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
"strictPropertyInitialization": true,
"lib": ["ES2020"],
"typeRoots": [
"./typings"
]
},
"include": [
"./**/*.ts"
],
"exclude": [
"node_modules"
]
}

@ -0,0 +1,8 @@
/// <reference path="./types/index.d.ts" />
interface IAppOption {
globalData: {
userInfo?: WechatMiniprogram.UserInfo,
}
userInfoReadyCallback?: WechatMiniprogram.GetUserInfoSuccessCallback,
}

@ -0,0 +1,2 @@
/// <reference path="./wx/index.d.ts" />

@ -0,0 +1,74 @@
/*! *****************************************************************************
Copyright (c) 2021 Tencent, Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************** */
/// <reference path="./lib.wx.app.d.ts" />
/// <reference path="./lib.wx.page.d.ts" />
/// <reference path="./lib.wx.api.d.ts" />
/// <reference path="./lib.wx.cloud.d.ts" />
/// <reference path="./lib.wx.component.d.ts" />
/// <reference path="./lib.wx.behavior.d.ts" />
/// <reference path="./lib.wx.event.d.ts" />
declare namespace WechatMiniprogram {
type IAnyObject = Record<string, any>
type Optional<F> = F extends (arg: infer P) => infer R ? (arg?: P) => R : F
type OptionalInterface<T> = { [K in keyof T]: Optional<T[K]> }
interface AsyncMethodOptionLike {
success?: (...args: any[]) => void
}
type PromisifySuccessResult<
P,
T extends AsyncMethodOptionLike
> = P extends { success: any }
? void
: P extends { fail: any }
? void
: P extends { complete: any }
? void
: Promise<Parameters<Exclude<T['success'], undefined>>[0]>
}
declare const console: WechatMiniprogram.Console
declare const wx: WechatMiniprogram.Wx
/** 引入模块。返回模块通过 `module.exports` 或 `exports` 暴露的接口。 */
declare function require(
/** 需要引入模块文件相对于当前文件的相对路径,或 npm 模块名,或 npm 模块路径。不支持绝对路径 */
module: string
): any
/** 引入插件。返回插件通过 `main` 暴露的接口。 */
declare function requirePlugin(
/** 需要引入的插件的 alias */
module: string
): any
/** 插件引入当前使用者小程序。返回使用者小程序通过 [插件配置中 `export` 暴露的接口](https://developers.weixin.qq.com/miniprogram/dev/framework/plugin/using.html#%E5%AF%BC%E5%87%BA%E5%88%B0%E6%8F%92%E4%BB%B6)
*
*
*
* `2.11.1` */
declare function requireMiniProgram(): any
/** 当前模块对象 */
declare let module: {
/** 模块向外暴露的对象,使用 `require` 引用该模块时可以获取 */
exports: any
}
/** `module.exports` 的引用 */
declare let exports: any

File diff suppressed because it is too large Load Diff

@ -0,0 +1,270 @@
/*! *****************************************************************************
Copyright (c) 2021 Tencent, Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************** */
declare namespace WechatMiniprogram.App {
interface ReferrerInfo {
/** App appId
*
* referrerInfo.appId
* - 1020 profile appId
* - 1035 appId
* - 1036App appId
* - 1037 appId
* - 1038 appId
* - 1043 appId
*/
appId: string
/** 来源小程序传过来的数据scene=1037或1038时支持 */
extraData?: any
}
type SceneValues =
| 1001
| 1005
| 1006
| 1007
| 1008
| 1011
| 1012
| 1013
| 1014
| 1017
| 1019
| 1020
| 1023
| 1024
| 1025
| 1026
| 1027
| 1028
| 1029
| 1030
| 1031
| 1032
| 1034
| 1035
| 1036
| 1037
| 1038
| 1039
| 1042
| 1043
| 1044
| 1045
| 1046
| 1047
| 1048
| 1049
| 1052
| 1053
| 1056
| 1057
| 1058
| 1059
| 1064
| 1067
| 1069
| 1071
| 1072
| 1073
| 1074
| 1077
| 1078
| 1079
| 1081
| 1082
| 1084
| 1089
| 1090
| 1091
| 1092
| 1095
| 1096
| 1097
| 1099
| 1102
| 1124
| 1125
| 1126
| 1129
interface LaunchShowOption {
/** 打开小程序的路径 */
path: string
/** 打开小程序的query */
query: IAnyObject
/**
* - 1001使2.2.4
* - 1005
* - 1006
* - 1007
* - 1008
* - 1011
* - 1012
* - 1013
* - 1014
* - 1017
* - 10197.0.0
* - 1020 profile
* - 1023
* - 1024 profile
* - 1025
* - 1026
* - 1027使
* - 1028
* - 1029
* - 1030
* - 1031
* - 1032
* - 1034
* - 1035
* - 1036App
* - 1037
* - 1038
* - 1039
* - 1042
* - 1043
* - 1044 shareTicket [](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share.html)
* - 1045广
* - 1046广
* - 1047
* - 1048
* - 1049
* - 1052
* - 1053
* - 1056
* - 1057
* - 1058
* - 1059
* - 1064Wi-Fi
* - 1067广
* - 1069
* - 1071
* - 1072
* - 1073
* - 1074
* - 1077
* - 1078Wi-Fi
* - 1079
* - 1081
* - 1082
* - 1084广
* - 1089使2.2.4
* - 1090使
* - 1091
* - 1092
* - 1095广
* - 1096
* - 1097
* - 1099
* - 1102 profile
* - 1124
* - 1125
* - 1126
* - 1129访 [](https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/sitemap.html)
*/
scene: SceneValues
/** shareTicket详见 [获取更多转发信息]((转发#获取更多转发信息)) */
shareTicket: string
/** 当场景为由从另一个小程序或公众号或App打开时返回此字段 */
referrerInfo?: ReferrerInfo
}
interface PageNotFoundOption {
/** 不存在页面的路径 */
path: string
/** 打开不存在页面的 query */
query: IAnyObject
/** 是否本次启动的首个页面(例如从分享等入口进来,首个页面是开发者配置的分享页面) */
isEntryPage: boolean
}
interface Option {
/**
*
*
*/
onLaunch(options: LaunchShowOption): void
/**
*
*
*/
onShow(options: LaunchShowOption): void
/**
*
*
*/
onHide(): void
/**
*
* api
*/
onError(/** 错误信息,包含堆栈 */ error: string): void
/**
*
*
*
* ****
* 1. `onPageNotFound`
* 2. `onPageNotFound` `onPageNotFound`
*
* 1.9.90
*/
onPageNotFound(options: PageNotFoundOption): void
/**
* Promise 使 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html) 绑定监听。注意事项请参考 [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html)。
* **** [wx.onUnhandledRejection](https://developers.weixin.qq.com/miniprogram/dev/api/base/app/app-event/wx.onUnhandledRejection.html) 一致
*/
onUnhandledRejection: OnUnhandledRejectionCallback
/**
* 使 wx.onThemeChange
*
* 2.11.0
*/
onThemeChange: OnThemeChangeCallback
}
type Instance<T extends IAnyObject> = Option & T
type Options<T extends IAnyObject> = Partial<Option> &
T &
ThisType<Instance<T>>
type TrivialInstance = Instance<IAnyObject>
interface Constructor {
<T extends IAnyObject>(options: Options<T>): void
}
interface GetAppOption {
/** `App` AppApp
*
* 2.2.4
*/
allowDefault?: boolean
}
interface GetApp {
<T = IAnyObject>(opts?: GetAppOption): Instance<T>
}
}
declare let App: WechatMiniprogram.App.Constructor
declare let getApp: WechatMiniprogram.App.GetApp

@ -0,0 +1,68 @@
/*! *****************************************************************************
Copyright (c) 2021 Tencent, Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************** */
declare namespace WechatMiniprogram.Behavior {
type BehaviorIdentifier = string
type Instance<
TData extends DataOption,
TProperty extends PropertyOption,
TMethod extends MethodOption,
TCustomInstanceProperty extends IAnyObject = Record<string, never>
> = Component.Instance<TData, TProperty, TMethod, TCustomInstanceProperty>
type TrivialInstance = Instance<IAnyObject, IAnyObject, IAnyObject>
type TrivialOption = Options<IAnyObject, IAnyObject, IAnyObject>
type Options<
TData extends DataOption,
TProperty extends PropertyOption,
TMethod extends MethodOption,
TCustomInstanceProperty extends IAnyObject = Record<string, never>
> = Partial<Data<TData>> &
Partial<Property<TProperty>> &
Partial<Method<TMethod>> &
Partial<OtherOption> &
Partial<Lifetimes> &
ThisType<Instance<TData, TProperty, TMethod, TCustomInstanceProperty>>
interface Constructor {
<
TData extends DataOption,
TProperty extends PropertyOption,
TMethod extends MethodOption,
TCustomInstanceProperty extends IAnyObject = Record<string, never>
>(
options: Options<TData, TProperty, TMethod, TCustomInstanceProperty>
): BehaviorIdentifier
}
type DataOption = Component.DataOption
type PropertyOption = Component.PropertyOption
type MethodOption = Component.MethodOption
type Data<D extends DataOption> = Component.Data<D>
type Property<P extends PropertyOption> = Component.Property<P>
type Method<M extends MethodOption> = Component.Method<M>
type DefinitionFilter = Component.DefinitionFilter
type Lifetimes = Component.Lifetimes
type OtherOption = Omit<Component.OtherOption, 'options'>
}
/** 注册一个 `behavior`,接受一个 `Object` 类型的参数。*/
declare let Behavior: WechatMiniprogram.Behavior.Constructor

@ -0,0 +1,924 @@
/*! *****************************************************************************
Copyright (c) 2021 Tencent, Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************** */
interface IAPIError {
errMsg: string
}
interface IAPIParam<T = any> {
config?: ICloudConfig
success?: (res: T) => void
fail?: (err: IAPIError) => void
complete?: (val: T | IAPIError) => void
}
interface IAPISuccessParam {
errMsg: string
}
type IAPICompleteParam = IAPISuccessParam | IAPIError
type IAPIFunction<T, P extends IAPIParam<T>> = (param?: P) => Promise<T>
interface IInitCloudConfig {
env?:
| string
| {
database?: string
functions?: string
storage?: string
}
traceUser?: boolean
}
interface ICloudConfig {
env?: string
traceUser?: boolean
}
interface IICloudAPI {
init: (config?: IInitCloudConfig) => void
[api: string]: AnyFunction | IAPIFunction<any, any>
}
interface ICloudService {
name: string
getAPIs: () => { [name: string]: IAPIFunction<any, any> }
}
interface ICloudServices {
[serviceName: string]: ICloudService
}
interface ICloudMetaData {
session_id: string
}
declare class InternalSymbol {}
interface AnyObject {
[x: string]: any
}
type AnyArray = any[]
type AnyFunction = (...args: any[]) => any
/**
* extend wx with cloud
*/
interface WxCloud {
init: (config?: ICloudConfig) => void
callFunction(param: OQ<ICloud.CallFunctionParam>): void
callFunction(
param: RQ<ICloud.CallFunctionParam>
): Promise<ICloud.CallFunctionResult>
uploadFile(param: OQ<ICloud.UploadFileParam>): WechatMiniprogram.UploadTask
uploadFile(
param: RQ<ICloud.UploadFileParam>
): Promise<ICloud.UploadFileResult>
downloadFile(
param: OQ<ICloud.DownloadFileParam>
): WechatMiniprogram.DownloadTask
downloadFile(
param: RQ<ICloud.DownloadFileParam>
): Promise<ICloud.DownloadFileResult>
getTempFileURL(param: OQ<ICloud.GetTempFileURLParam>): void
getTempFileURL(
param: RQ<ICloud.GetTempFileURLParam>
): Promise<ICloud.GetTempFileURLResult>
deleteFile(param: OQ<ICloud.DeleteFileParam>): void
deleteFile(
param: RQ<ICloud.DeleteFileParam>
): Promise<ICloud.DeleteFileResult>
database: (config?: ICloudConfig) => DB.Database
CloudID: ICloud.ICloudIDConstructor
CDN: ICloud.ICDNConstructor
}
declare namespace ICloud {
interface ICloudAPIParam<T = any> extends IAPIParam<T> {
config?: ICloudConfig
}
// === API: callFunction ===
type CallFunctionData = AnyObject
interface CallFunctionResult extends IAPISuccessParam {
result: AnyObject | string | undefined
}
interface CallFunctionParam extends ICloudAPIParam<CallFunctionResult> {
name: string
data?: CallFunctionData
slow?: boolean
}
// === end ===
// === API: uploadFile ===
interface UploadFileResult extends IAPISuccessParam {
fileID: string
statusCode: number
}
interface UploadFileParam extends ICloudAPIParam<UploadFileResult> {
cloudPath: string
filePath: string
header?: AnyObject
}
// === end ===
// === API: downloadFile ===
interface DownloadFileResult extends IAPISuccessParam {
tempFilePath: string
statusCode: number
}
interface DownloadFileParam extends ICloudAPIParam<DownloadFileResult> {
fileID: string
cloudPath?: string
}
// === end ===
// === API: getTempFileURL ===
interface GetTempFileURLResult extends IAPISuccessParam {
fileList: GetTempFileURLResultItem[]
}
interface GetTempFileURLResultItem {
fileID: string
tempFileURL: string
maxAge: number
status: number
errMsg: string
}
interface GetTempFileURLParam extends ICloudAPIParam<GetTempFileURLResult> {
fileList: string[]
}
// === end ===
// === API: deleteFile ===
interface DeleteFileResult extends IAPISuccessParam {
fileList: DeleteFileResultItem[]
}
interface DeleteFileResultItem {
fileID: string
status: number
errMsg: string
}
interface DeleteFileParam extends ICloudAPIParam<DeleteFileResult> {
fileList: string[]
}
// === end ===
// === API: CloudID ===
abstract class CloudID {
constructor(cloudID: string)
}
interface ICloudIDConstructor {
new (cloudId: string): CloudID
(cloudId: string): CloudID
}
// === end ===
// === API: CDN ===
abstract class CDN {
target: string | ArrayBuffer | ICDNFilePathSpec
constructor(target: string | ArrayBuffer | ICDNFilePathSpec)
}
interface ICDNFilePathSpec {
type: 'filePath'
filePath: string
}
interface ICDNConstructor {
new (options: string | ArrayBuffer | ICDNFilePathSpec): CDN
(options: string | ArrayBuffer | ICDNFilePathSpec): CDN
}
// === end ===
}
// === Database ===
declare namespace DB {
/**
* The class of all exposed cloud database instances
*/
class Database {
readonly config: ICloudConfig
readonly command: DatabaseCommand
readonly Geo: IGeo
readonly serverDate: () => ServerDate
readonly RegExp: IRegExpConstructor
private constructor()
collection(collectionName: string): CollectionReference
}
class CollectionReference extends Query {
readonly collectionName: string
private constructor(name: string, database: Database)
doc(docId: string | number): DocumentReference
add(options: OQ<IAddDocumentOptions>): void
add(options: RQ<IAddDocumentOptions>): Promise<IAddResult>
}
class DocumentReference {
private constructor(docId: string | number, database: Database)
field(object: Record<string, any>): this
get(options: OQ<IGetDocumentOptions>): void
get(options?: RQ<IGetDocumentOptions>): Promise<IQuerySingleResult>
set(options: OQ<ISetSingleDocumentOptions>): void
set(options?: RQ<ISetSingleDocumentOptions>): Promise<ISetResult>
update(options: OQ<IUpdateSingleDocumentOptions>): void
update(
options?: RQ<IUpdateSingleDocumentOptions>
): Promise<IUpdateResult>
remove(options: OQ<IRemoveSingleDocumentOptions>): void
remove(
options?: RQ<IRemoveSingleDocumentOptions>
): Promise<IRemoveResult>
watch(options: IWatchOptions): RealtimeListener
}
class RealtimeListener {
// "And Now His Watch Is Ended"
close: () => Promise<void>
}
class Query {
where(condition: IQueryCondition): Query
orderBy(fieldPath: string, order: string): Query
limit(max: number): Query
skip(offset: number): Query
field(object: Record<string, any>): Query
get(options: OQ<IGetDocumentOptions>): void
get(options?: RQ<IGetDocumentOptions>): Promise<IQueryResult>
count(options: OQ<ICountDocumentOptions>): void
count(options?: RQ<ICountDocumentOptions>): Promise<ICountResult>
watch(options: IWatchOptions): RealtimeListener
}
interface DatabaseCommand {
eq(val: any): DatabaseQueryCommand
neq(val: any): DatabaseQueryCommand
gt(val: any): DatabaseQueryCommand
gte(val: any): DatabaseQueryCommand
lt(val: any): DatabaseQueryCommand
lte(val: any): DatabaseQueryCommand
in(val: any[]): DatabaseQueryCommand
nin(val: any[]): DatabaseQueryCommand
geoNear(options: IGeoNearCommandOptions): DatabaseQueryCommand
geoWithin(options: IGeoWithinCommandOptions): DatabaseQueryCommand
geoIntersects(
options: IGeoIntersectsCommandOptions
): DatabaseQueryCommand
and(
...expressions: Array<DatabaseLogicCommand | IQueryCondition>
): DatabaseLogicCommand
or(
...expressions: Array<DatabaseLogicCommand | IQueryCondition>
): DatabaseLogicCommand
nor(
...expressions: Array<DatabaseLogicCommand | IQueryCondition>
): DatabaseLogicCommand
not(expression: DatabaseLogicCommand): DatabaseLogicCommand
exists(val: boolean): DatabaseQueryCommand
mod(divisor: number, remainder: number): DatabaseQueryCommand
all(val: any[]): DatabaseQueryCommand
elemMatch(val: any): DatabaseQueryCommand
size(val: number): DatabaseQueryCommand
set(val: any): DatabaseUpdateCommand
remove(): DatabaseUpdateCommand
inc(val: number): DatabaseUpdateCommand
mul(val: number): DatabaseUpdateCommand
min(val: number): DatabaseUpdateCommand
max(val: number): DatabaseUpdateCommand
rename(val: string): DatabaseUpdateCommand
bit(val: number): DatabaseUpdateCommand
push(...values: any[]): DatabaseUpdateCommand
pop(): DatabaseUpdateCommand
shift(): DatabaseUpdateCommand
unshift(...values: any[]): DatabaseUpdateCommand
addToSet(val: any): DatabaseUpdateCommand
pull(val: any): DatabaseUpdateCommand
pullAll(val: any): DatabaseUpdateCommand
project: {
slice(val: number | [number, number]): DatabaseProjectionCommand
}
aggregate: {
__safe_props__?: Set<string>
abs(val: any): DatabaseAggregateCommand
add(val: any): DatabaseAggregateCommand
addToSet(val: any): DatabaseAggregateCommand
allElementsTrue(val: any): DatabaseAggregateCommand
and(val: any): DatabaseAggregateCommand
anyElementTrue(val: any): DatabaseAggregateCommand
arrayElemAt(val: any): DatabaseAggregateCommand
arrayToObject(val: any): DatabaseAggregateCommand
avg(val: any): DatabaseAggregateCommand
ceil(val: any): DatabaseAggregateCommand
cmp(val: any): DatabaseAggregateCommand
concat(val: any): DatabaseAggregateCommand
concatArrays(val: any): DatabaseAggregateCommand
cond(val: any): DatabaseAggregateCommand
convert(val: any): DatabaseAggregateCommand
dateFromParts(val: any): DatabaseAggregateCommand
dateToParts(val: any): DatabaseAggregateCommand
dateFromString(val: any): DatabaseAggregateCommand
dateToString(val: any): DatabaseAggregateCommand
dayOfMonth(val: any): DatabaseAggregateCommand
dayOfWeek(val: any): DatabaseAggregateCommand
dayOfYear(val: any): DatabaseAggregateCommand
divide(val: any): DatabaseAggregateCommand
eq(val: any): DatabaseAggregateCommand
exp(val: any): DatabaseAggregateCommand
filter(val: any): DatabaseAggregateCommand
first(val: any): DatabaseAggregateCommand
floor(val: any): DatabaseAggregateCommand
gt(val: any): DatabaseAggregateCommand
gte(val: any): DatabaseAggregateCommand
hour(val: any): DatabaseAggregateCommand
ifNull(val: any): DatabaseAggregateCommand
in(val: any): DatabaseAggregateCommand
indexOfArray(val: any): DatabaseAggregateCommand
indexOfBytes(val: any): DatabaseAggregateCommand
indexOfCP(val: any): DatabaseAggregateCommand
isArray(val: any): DatabaseAggregateCommand
isoDayOfWeek(val: any): DatabaseAggregateCommand
isoWeek(val: any): DatabaseAggregateCommand
isoWeekYear(val: any): DatabaseAggregateCommand
last(val: any): DatabaseAggregateCommand
let(val: any): DatabaseAggregateCommand
literal(val: any): DatabaseAggregateCommand
ln(val: any): DatabaseAggregateCommand
log(val: any): DatabaseAggregateCommand
log10(val: any): DatabaseAggregateCommand
lt(val: any): DatabaseAggregateCommand
lte(val: any): DatabaseAggregateCommand
ltrim(val: any): DatabaseAggregateCommand
map(val: any): DatabaseAggregateCommand
max(val: any): DatabaseAggregateCommand
mergeObjects(val: any): DatabaseAggregateCommand
meta(val: any): DatabaseAggregateCommand
min(val: any): DatabaseAggregateCommand
millisecond(val: any): DatabaseAggregateCommand
minute(val: any): DatabaseAggregateCommand
mod(val: any): DatabaseAggregateCommand
month(val: any): DatabaseAggregateCommand
multiply(val: any): DatabaseAggregateCommand
neq(val: any): DatabaseAggregateCommand
not(val: any): DatabaseAggregateCommand
objectToArray(val: any): DatabaseAggregateCommand
or(val: any): DatabaseAggregateCommand
pow(val: any): DatabaseAggregateCommand
push(val: any): DatabaseAggregateCommand
range(val: any): DatabaseAggregateCommand
reduce(val: any): DatabaseAggregateCommand
reverseArray(val: any): DatabaseAggregateCommand
rtrim(val: any): DatabaseAggregateCommand
second(val: any): DatabaseAggregateCommand
setDifference(val: any): DatabaseAggregateCommand
setEquals(val: any): DatabaseAggregateCommand
setIntersection(val: any): DatabaseAggregateCommand
setIsSubset(val: any): DatabaseAggregateCommand
setUnion(val: any): DatabaseAggregateCommand
size(val: any): DatabaseAggregateCommand
slice(val: any): DatabaseAggregateCommand
split(val: any): DatabaseAggregateCommand
sqrt(val: any): DatabaseAggregateCommand
stdDevPop(val: any): DatabaseAggregateCommand
stdDevSamp(val: any): DatabaseAggregateCommand
strcasecmp(val: any): DatabaseAggregateCommand
strLenBytes(val: any): DatabaseAggregateCommand
strLenCP(val: any): DatabaseAggregateCommand
substr(val: any): DatabaseAggregateCommand
substrBytes(val: any): DatabaseAggregateCommand
substrCP(val: any): DatabaseAggregateCommand
subtract(val: any): DatabaseAggregateCommand
sum(val: any): DatabaseAggregateCommand
switch(val: any): DatabaseAggregateCommand
toBool(val: any): DatabaseAggregateCommand
toDate(val: any): DatabaseAggregateCommand
toDecimal(val: any): DatabaseAggregateCommand
toDouble(val: any): DatabaseAggregateCommand
toInt(val: any): DatabaseAggregateCommand
toLong(val: any): DatabaseAggregateCommand
toObjectId(val: any): DatabaseAggregateCommand
toString(val: any): DatabaseAggregateCommand
toLower(val: any): DatabaseAggregateCommand
toUpper(val: any): DatabaseAggregateCommand
trim(val: any): DatabaseAggregateCommand
trunc(val: any): DatabaseAggregateCommand
type(val: any): DatabaseAggregateCommand
week(val: any): DatabaseAggregateCommand
year(val: any): DatabaseAggregateCommand
zip(val: any): DatabaseAggregateCommand
}
}
class DatabaseAggregateCommand {}
enum LOGIC_COMMANDS_LITERAL {
AND = 'and',
OR = 'or',
NOT = 'not',
NOR = 'nor'
}
class DatabaseLogicCommand {
and(...expressions: DatabaseLogicCommand[]): DatabaseLogicCommand
or(...expressions: DatabaseLogicCommand[]): DatabaseLogicCommand
nor(...expressions: DatabaseLogicCommand[]): DatabaseLogicCommand
not(expression: DatabaseLogicCommand): DatabaseLogicCommand
}
enum QUERY_COMMANDS_LITERAL {
// comparison
EQ = 'eq',
NEQ = 'neq',
GT = 'gt',
GTE = 'gte',
LT = 'lt',
LTE = 'lte',
IN = 'in',
NIN = 'nin',
// geo
GEO_NEAR = 'geoNear',
GEO_WITHIN = 'geoWithin',
GEO_INTERSECTS = 'geoIntersects',
// element
EXISTS = 'exists',
// evaluation
MOD = 'mod',
// array
ALL = 'all',
ELEM_MATCH = 'elemMatch',
SIZE = 'size'
}
class DatabaseQueryCommand extends DatabaseLogicCommand {
eq(val: any): DatabaseLogicCommand
neq(val: any): DatabaseLogicCommand
gt(val: any): DatabaseLogicCommand
gte(val: any): DatabaseLogicCommand
lt(val: any): DatabaseLogicCommand
lte(val: any): DatabaseLogicCommand
in(val: any[]): DatabaseLogicCommand
nin(val: any[]): DatabaseLogicCommand
exists(val: boolean): DatabaseLogicCommand
mod(divisor: number, remainder: number): DatabaseLogicCommand
all(val: any[]): DatabaseLogicCommand
elemMatch(val: any): DatabaseLogicCommand
size(val: number): DatabaseLogicCommand
geoNear(options: IGeoNearCommandOptions): DatabaseLogicCommand
geoWithin(options: IGeoWithinCommandOptions): DatabaseLogicCommand
geoIntersects(
options: IGeoIntersectsCommandOptions
): DatabaseLogicCommand
}
enum PROJECTION_COMMANDS_LITERAL {
SLICE = 'slice'
}
class DatabaseProjectionCommand {}
enum UPDATE_COMMANDS_LITERAL {
// field
SET = 'set',
REMOVE = 'remove',
INC = 'inc',
MUL = 'mul',
MIN = 'min',
MAX = 'max',
RENAME = 'rename',
// bitwise
BIT = 'bit',
// array
PUSH = 'push',
POP = 'pop',
SHIFT = 'shift',
UNSHIFT = 'unshift',
ADD_TO_SET = 'addToSet',
PULL = 'pull',
PULL_ALL = 'pullAll'
}
class DatabaseUpdateCommand {}
class Batch {}
/**
* A contract that all API provider must adhere to
*/
class APIBaseContract<
PromiseReturn,
CallbackReturn,
Param extends IAPIParam,
Context = any
> {
getContext(param: Param): Context
/**
* In case of callback-style invocation, this function will be called
*/
getCallbackReturn(param: Param, context: Context): CallbackReturn
getFinalParam<T extends Param>(param: Param, context: Context): T
run<T extends Param>(param: T): Promise<PromiseReturn>
}
interface IGeoPointConstructor {
new (longitude: number, latitide: number): GeoPoint
new (geojson: IGeoJSONPoint): GeoPoint
(longitude: number, latitide: number): GeoPoint
(geojson: IGeoJSONPoint): GeoPoint
}
interface IGeoMultiPointConstructor {
new (points: GeoPoint[] | IGeoJSONMultiPoint): GeoMultiPoint
(points: GeoPoint[] | IGeoJSONMultiPoint): GeoMultiPoint
}
interface IGeoLineStringConstructor {
new (points: GeoPoint[] | IGeoJSONLineString): GeoLineString
(points: GeoPoint[] | IGeoJSONLineString): GeoLineString
}
interface IGeoMultiLineStringConstructor {
new (
lineStrings: GeoLineString[] | IGeoJSONMultiLineString
): GeoMultiLineString
(
lineStrings: GeoLineString[] | IGeoJSONMultiLineString
): GeoMultiLineString
}
interface IGeoPolygonConstructor {
new (lineStrings: GeoLineString[] | IGeoJSONPolygon): GeoPolygon
(lineStrings: GeoLineString[] | IGeoJSONPolygon): GeoPolygon
}
interface IGeoMultiPolygonConstructor {
new (polygons: GeoPolygon[] | IGeoJSONMultiPolygon): GeoMultiPolygon
(polygons: GeoPolygon[] | IGeoJSONMultiPolygon): GeoMultiPolygon
}
interface IGeo {
Point: IGeoPointConstructor
MultiPoint: IGeoMultiPointConstructor
LineString: IGeoLineStringConstructor
MultiLineString: IGeoMultiLineStringConstructor
Polygon: IGeoPolygonConstructor
MultiPolygon: IGeoMultiPolygonConstructor
}
interface IGeoJSONPoint {
type: 'Point'
coordinates: [number, number]
}
interface IGeoJSONMultiPoint {
type: 'MultiPoint'
coordinates: Array<[number, number]>
}
interface IGeoJSONLineString {
type: 'LineString'
coordinates: Array<[number, number]>
}
interface IGeoJSONMultiLineString {
type: 'MultiLineString'
coordinates: Array<Array<[number, number]>>
}
interface IGeoJSONPolygon {
type: 'Polygon'
coordinates: Array<Array<[number, number]>>
}
interface IGeoJSONMultiPolygon {
type: 'MultiPolygon'
coordinates: Array<Array<Array<[number, number]>>>
}
type IGeoJSONObject =
| IGeoJSONPoint
| IGeoJSONMultiPoint
| IGeoJSONLineString
| IGeoJSONMultiLineString
| IGeoJSONPolygon
| IGeoJSONMultiPolygon
abstract class GeoPoint {
longitude: number
latitude: number
constructor(longitude: number, latitude: number)
toJSON(): Record<string, any>
toString(): string
}
abstract class GeoMultiPoint {
points: GeoPoint[]
constructor(points: GeoPoint[])
toJSON(): IGeoJSONMultiPoint
toString(): string
}
abstract class GeoLineString {
points: GeoPoint[]
constructor(points: GeoPoint[])
toJSON(): IGeoJSONLineString
toString(): string
}
abstract class GeoMultiLineString {
lines: GeoLineString[]
constructor(lines: GeoLineString[])
toJSON(): IGeoJSONMultiLineString
toString(): string
}
abstract class GeoPolygon {
lines: GeoLineString[]
constructor(lines: GeoLineString[])
toJSON(): IGeoJSONPolygon
toString(): string
}
abstract class GeoMultiPolygon {
polygons: GeoPolygon[]
constructor(polygons: GeoPolygon[])
toJSON(): IGeoJSONMultiPolygon
toString(): string
}
type GeoInstance =
| GeoPoint
| GeoMultiPoint
| GeoLineString
| GeoMultiLineString
| GeoPolygon
| GeoMultiPolygon
interface IGeoNearCommandOptions {
geometry: GeoPoint
maxDistance?: number
minDistance?: number
}
interface IGeoWithinCommandOptions {
geometry: GeoPolygon | GeoMultiPolygon
}
interface IGeoIntersectsCommandOptions {
geometry:
| GeoPoint
| GeoMultiPoint
| GeoLineString
| GeoMultiLineString
| GeoPolygon
| GeoMultiPolygon
}
interface IServerDateOptions {
offset: number
}
abstract class ServerDate {
readonly options: IServerDateOptions
constructor(options?: IServerDateOptions)
}
interface IRegExpOptions {
regexp: string
options?: string
}
interface IRegExpConstructor {
new (options: IRegExpOptions): RegExp
(options: IRegExpOptions): RegExp
}
abstract class RegExp {
readonly regexp: string
readonly options: string
constructor(options: IRegExpOptions)
}
type DocumentId = string | number
interface IDocumentData {
_id?: DocumentId
[key: string]: any
}
type IDBAPIParam = IAPIParam
interface IAddDocumentOptions extends IDBAPIParam {
data: IDocumentData
}
type IGetDocumentOptions = IDBAPIParam
type ICountDocumentOptions = IDBAPIParam
interface IUpdateDocumentOptions extends IDBAPIParam {
data: IUpdateCondition
}
interface IUpdateSingleDocumentOptions extends IDBAPIParam {
data: IUpdateCondition
}
interface ISetDocumentOptions extends IDBAPIParam {
data: IUpdateCondition
}
interface ISetSingleDocumentOptions extends IDBAPIParam {
data: IUpdateCondition
}
interface IRemoveDocumentOptions extends IDBAPIParam {
query: IQueryCondition
}
type IRemoveSingleDocumentOptions = IDBAPIParam
interface IWatchOptions {
// server realtime data init & change event
onChange: (snapshot: ISnapshot) => void
// error while connecting / listening
onError: (error: any) => void
}
interface ISnapshot {
id: number
docChanges: ISingleDBEvent[]
docs: Record<string, any>
type?: SnapshotType
}
type SnapshotType = 'init'
interface ISingleDBEvent {
id: number
dataType: DataType
queueType: QueueType
docId: string
doc: Record<string, any>
updatedFields?: Record<string, any>
removedFields?: string[]
}
type DataType = 'init' | 'update' | 'replace' | 'add' | 'remove' | 'limit'
type QueueType = 'init' | 'enqueue' | 'dequeue' | 'update'
interface IQueryCondition {
[key: string]: any
}
type IStringQueryCondition = string
interface IQueryResult extends IAPISuccessParam {
data: IDocumentData[]
}
interface IQuerySingleResult extends IAPISuccessParam {
data: IDocumentData
}
interface IUpdateCondition {
[key: string]: any
}
type IStringUpdateCondition = string
interface IAddResult extends IAPISuccessParam {
_id: DocumentId
}
interface IUpdateResult extends IAPISuccessParam {
stats: {
updated: number
// created: number,
}
}
interface ISetResult extends IAPISuccessParam {
_id: DocumentId
stats: {
updated: number
created: number
}
}
interface IRemoveResult extends IAPISuccessParam {
stats: {
removed: number
}
}
interface ICountResult extends IAPISuccessParam {
total: number
}
}
type Optional<T> = { [K in keyof T]+?: T[K] }
type OQ<
T extends Optional<
Record<'complete' | 'success' | 'fail', (...args: any[]) => any>
>
> =
| (RQ<T> & Required<Pick<T, 'success'>>)
| (RQ<T> & Required<Pick<T, 'fail'>>)
| (RQ<T> & Required<Pick<T, 'complete'>>)
| (RQ<T> & Required<Pick<T, 'success' | 'fail'>>)
| (RQ<T> & Required<Pick<T, 'success' | 'complete'>>)
| (RQ<T> & Required<Pick<T, 'fail' | 'complete'>>)
| (RQ<T> & Required<Pick<T, 'fail' | 'complete' | 'success'>>)
type RQ<
T extends Optional<
Record<'complete' | 'success' | 'fail', (...args: any[]) => any>
>
> = Pick<T, Exclude<keyof T, 'complete' | 'success' | 'fail'>>

@ -0,0 +1,636 @@
/*! *****************************************************************************
Copyright (c) 2021 Tencent, Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************** */
declare namespace WechatMiniprogram.Component {
type Instance<
TData extends DataOption,
TProperty extends PropertyOption,
TMethod extends Partial<MethodOption>,
TCustomInstanceProperty extends IAnyObject = {},
TIsPage extends boolean = false
> = InstanceProperties &
InstanceMethods<TData> &
TMethod &
(TIsPage extends true ? Page.ILifetime : {}) &
TCustomInstanceProperty & {
/** 组件数据,**包括内部数据和属性值** */
data: TData & PropertyOptionToData<TProperty>
/** 组件数据,**包括内部数据和属性值**(与 `data` 一致) */
properties: TData & PropertyOptionToData<TProperty>
}
type TrivialInstance = Instance<
IAnyObject,
IAnyObject,
IAnyObject,
IAnyObject
>
type TrivialOption = Options<IAnyObject, IAnyObject, IAnyObject, IAnyObject>
type Options<
TData extends DataOption,
TProperty extends PropertyOption,
TMethod extends MethodOption,
TCustomInstanceProperty extends IAnyObject = {},
TIsPage extends boolean = false
> = Partial<Data<TData>> &
Partial<Property<TProperty>> &
Partial<Method<TMethod, TIsPage>> &
Partial<OtherOption> &
Partial<Lifetimes> &
ThisType<
Instance<
TData,
TProperty,
TMethod,
TCustomInstanceProperty,
TIsPage
>
>
interface Constructor {
<
TData extends DataOption,
TProperty extends PropertyOption,
TMethod extends MethodOption,
TCustomInstanceProperty extends IAnyObject = {},
TIsPage extends boolean = false
>(
options: Options<
TData,
TProperty,
TMethod,
TCustomInstanceProperty,
TIsPage
>
): string
}
type DataOption = Record<string, any>
type PropertyOption = Record<string, AllProperty>
type MethodOption = Record<string, Function>
interface Data<D extends DataOption> {
/** 组件的内部数据,和 `properties` 一同用于组件的模板渲染 */
data?: D
}
interface Property<P extends PropertyOption> {
/** 组件的对外属性,是属性名到属性设置的映射表 */
properties: P
}
interface Method<M extends MethodOption, TIsPage extends boolean = false> {
/** 组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用,参见 [组件间通信与事件](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/events.html) */
methods: M & (TIsPage extends true ? Partial<Page.ILifetime> : {})
}
type PropertyType =
| StringConstructor
| NumberConstructor
| BooleanConstructor
| ArrayConstructor
| ObjectConstructor
| null
type ValueType<T extends PropertyType> = T extends null
? any
: T extends StringConstructor
? string
: T extends NumberConstructor
? number
: T extends BooleanConstructor
? boolean
: T extends ArrayConstructor
? any[]
: T extends ObjectConstructor
? IAnyObject
: never
type FullProperty<T extends PropertyType> = {
/** 属性类型 */
type: T
/** 属性初始值 */
value?: ValueType<T>
/** 属性值被更改时的响应函数 */
observer?:
| string
| ((
newVal: ValueType<T>,
oldVal: ValueType<T>,
changedPath: Array<string | number>
) => void)
/** 属性的类型(可以指定多个) */
optionalTypes?: ShortProperty[]
}
type AllFullProperty =
| FullProperty<StringConstructor>
| FullProperty<NumberConstructor>
| FullProperty<BooleanConstructor>
| FullProperty<ArrayConstructor>
| FullProperty<ObjectConstructor>
| FullProperty<null>
type ShortProperty =
| StringConstructor
| NumberConstructor
| BooleanConstructor
| ArrayConstructor
| ObjectConstructor
| null
type AllProperty = AllFullProperty | ShortProperty
type PropertyToData<T extends AllProperty> = T extends ShortProperty
? ValueType<T>
: FullPropertyToData<Exclude<T, ShortProperty>>
type FullPropertyToData<T extends AllFullProperty> = ValueType<T['type']>
type PropertyOptionToData<P extends PropertyOption> = {
[name in keyof P]: PropertyToData<P[name]>
}
interface InstanceProperties {
/** 组件的文件路径 */
is: string
/** 节点id */
id: string
/** 节点dataset */
dataset: Record<string, string>
}
interface InstanceMethods<D extends DataOption> {
/** `setData`
* `this.data`
*
* ****
*
* 1. ** this.data this.setData **
* 1. JSON
* 1. 1024kB
* 1. data value `undefined`
*/
setData(
/**
*
* `key: value` `this.data` `key` `value`
*
* `key` `array[2].message``a.b.c.d` this.data
*/
data: Partial<D> & IAnyObject,
/** setData引起的界面更新渲染完毕后的回调函数最低基础库 `1.5.0` */
callback?: () => void
): void
/** 检查组件是否具有 `behavior` 检查时会递归检查被直接或间接引入的所有behavior */
hasBehavior(behavior: Behavior.BehaviorIdentifier): void
/** 触发事件,参见组件事件 */
triggerEvent<DetailType = any>(
name: string,
detail?: DetailType,
options?: TriggerEventOption
): void
/** 创建一个 SelectorQuery 对象,选择器选取范围为这个组件实例内 */
createSelectorQuery(): SelectorQuery
/** 创建一个 IntersectionObserver 对象,选择器选取范围为这个组件实例内 */
createIntersectionObserver(
options: CreateIntersectionObserverOption
): IntersectionObserver
/** 使用选择器选择组件实例节点,返回匹配到的第一个组件实例对象(会被 `wx://component-export` 影响) */
selectComponent(selector: string): TrivialInstance
/** 使用选择器选择组件实例节点,返回匹配到的全部组件实例对象组成的数组 */
selectAllComponents(selector: string): TrivialInstance[]
/**
* `wx://component-export`
*
* [`2.8.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
selectOwnerComponent(): TrivialInstance
/** 获取这个关系所对应的所有关联节点,参见 组件间关系 */
getRelationNodes(relationKey: string): TrivialInstance[]
/**
* callback setData setData
*
* [`2.4.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
groupSetData(callback?: () => void): void
/**
* custom-tab-bar
*
* [`2.6.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
getTabBar(): TrivialInstance
/**
*
*
* [`2.7.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
getPageId(): string
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
*
* [`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
animate(
selector: string,
keyFrames: KeyFrame[],
duration: number,
callback?: () => void
): void
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
*
* [`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
animate(
selector: string,
keyFrames: ScrollTimelineKeyframe[],
duration: number,
scrollTimeline: ScrollTimelineOption
): void
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
*
* [`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
clearAnimation(selector: string, callback: () => void): void
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html)
*
* [`2.9.0`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
**/
clearAnimation(
selector: string,
options?: ClearAnimationOptions,
callback?: () => void
): void
getOpenerEventChannel(): EventChannel
}
interface ComponentOptions {
/**
* [slot](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件wxml的slot)
*/
multipleSlots?: boolean
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件样式隔离)
*/
addGlobalClass?: boolean
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#组件样式隔离)
*/
styleIsolation?:
| 'isolated'
| 'apply-shared'
| 'shared'
| 'page-isolated'
| 'page-apply-shared'
| 'page-shared'
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/pure-data.html) 是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能。从小程序基础库版本 2.8.2 开始支持。
*/
pureDataPattern?: RegExp
/**
* [](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html#%E8%99%9A%E6%8B%9F%E5%8C%96%E7%BB%84%E4%BB%B6%E8%8A%82%E7%82%B9) 使自定义组件内部的第一层节点由自定义组件本身完全决定。从小程序基础库版本 [`2.11.2`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) 开始支持 */
virtualHost?: boolean
}
interface TriggerEventOption {
/**
*
* `false`
*/
bubbles?: boolean
/** 穿false
*
* `false`
*/
composed?: boolean
/**
*
* `false`
*/
capturePhase?: boolean
}
interface RelationOption {
/** 目标组件的相对关系 */
type: 'parent' | 'child' | 'ancestor' | 'descendant'
/** 关系生命周期函数当关系被建立在页面节点树中时触发触发时机在组件attached生命周期之后 */
linked?(target: TrivialInstance): void
/** 关系生命周期函数当关系在页面节点树中发生改变时触发触发时机在组件moved生命周期之后 */
linkChanged?(target: TrivialInstance): void
/** 关系生命周期函数当关系脱离页面节点树时触发触发时机在组件detached生命周期之后 */
unlinked?(target: TrivialInstance): void
/** 如果这一项被设置则它表示关联的目标节点所应具有的behavior所有拥有这一behavior的组件节点都会被关联 */
target?: string
}
interface PageLifetimes {
/**
*
* /
*/
show(): void
/**
*
* / `navigateTo` `tab`
*/
hide(): void
/**
*
*
*/
resize(size: Page.IResizeOption): void
}
type DefinitionFilter = <T extends TrivialOption>(
/** 使用该 behavior 的 component/behavior 的定义对象 */
defFields: T,
/** 该 behavior 所使用的 behavior 的 definitionFilter 函数列表 */
definitionFilterArr?: DefinitionFilter[]
) => void
interface Lifetimes {
/** `created``attached``ready``moved``detached` `lifetimes` `lifetimes`
*
* `2.2.3` */
lifetimes: Partial<{
/**
* `setData`
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
created(): void
/**
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
attached(): void
/**
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
ready(): void
/**
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
moved(): void
/**
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
detached(): void
/**
*
*
* [`2.4.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
error(err: Error): void
}>
/**
* @deprecated `2.2.3` lifetimes
*
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
created(): void
/**
* @deprecated `2.2.3` lifetimes
*
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
attached(): void
/**
* @deprecated `2.2.3` lifetimes
*
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
ready(): void
/**
* @deprecated `2.2.3` lifetimes
*
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
moved(): void
/**
* @deprecated `2.2.3` lifetimes
*
*
*
* [`1.6.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
detached(): void
/**
* @deprecated `2.2.3` lifetimes
*
*
*
* [`2.4.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
error(err: Error): void
}
interface OtherOption {
/** 类似于mixins和traits的组件间代码复用机制参见 [behaviors](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/behaviors.html) */
behaviors: Behavior.BehaviorIdentifier[]
/**
* properties data [](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/observer.html)
*
* [`2.6.1`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html)
*/
observers: Record<string, (...args: any[]) => any>
/** 组件间关系定义,参见 [组件间关系](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html) */
relations: {
[componentName: string]: RelationOption
}
/** 组件接受的外部样式类,参见 [外部样式类](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/wxml-wxss.html) */
externalClasses?: string[]
/** 组件所在页面的生命周期声明对象,参见 [组件生命周期](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/lifetimes.html)
*
* [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
pageLifetimes?: Partial<PageLifetimes>
/** 一些选项(文档中介绍相关特性时会涉及具体的选项设置,这里暂不列举) */
options: ComponentOptions
/** 定义段过滤器,用于自定义组件扩展,参见 [自定义组件扩展](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/extend.html)
*
* [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
definitionFilter?: DefinitionFilter
/**
* 使 `behavior: wx://component-export` selectComponent [](https://developers.weixin.qq.com/miniprogram/dev/framework/custom-component/events.html)
* [`2.2.3`](https://developers.weixin.qq.com/miniprogram/dev/framework/compatibility.html) */
export: () => IAnyObject
}
interface KeyFrame {
/** 关键帧的偏移,范围[0-1] */
offset?: number
/** 动画缓动函数 */
ease?: string
/** 基点位置,即 CSS transform-origin */
transformOrigin?: string
/** 背景颜色,即 CSS background-color */
backgroundColor?: string
/** 底边位置,即 CSS bottom */
bottom?: number | string
/** 高度,即 CSS height */
height?: number | string
/** 左边位置,即 CSS left */
left?: number | string
/** 宽度,即 CSS width */
width?: number | string
/** 不透明度,即 CSS opacity */
opacity?: number | string
/** 右边位置,即 CSS right */
right?: number | string
/** 顶边位置,即 CSS top */
top?: number | string
/** 变换矩阵,即 CSS transform matrix */
matrix?: number[]
/** 三维变换矩阵,即 CSS transform matrix3d */
matrix3d?: number[]
/** 旋转,即 CSS transform rotate */
rotate?: number
/** 三维旋转,即 CSS transform rotate3d */
rotate3d?: number[]
/** X 方向旋转,即 CSS transform rotateX */
rotateX?: number
/** Y 方向旋转,即 CSS transform rotateY */
rotateY?: number
/** Z 方向旋转,即 CSS transform rotateZ */
rotateZ?: number
/** 缩放,即 CSS transform scale */
scale?: number[]
/** 三维缩放,即 CSS transform scale3d */
scale3d?: number[]
/** X 方向缩放,即 CSS transform scaleX */
scaleX?: number
/** Y 方向缩放,即 CSS transform scaleY */
scaleY?: number
/** Z 方向缩放,即 CSS transform scaleZ */
scaleZ?: number
/** 倾斜,即 CSS transform skew */
skew?: number[]
/** X 方向倾斜,即 CSS transform skewX */
skewX?: number
/** Y 方向倾斜,即 CSS transform skewY */
skewY?: number
/** 位移,即 CSS transform translate */
translate?: Array<number | string>
/** 三维位移,即 CSS transform translate3d */
translate3d?: Array<number | string>
/** X 方向位移,即 CSS transform translateX */
translateX?: number | string
/** Y 方向位移,即 CSS transform translateY */
translateY?: number | string
/** Z 方向位移,即 CSS transform translateZ */
translateZ?: number | string
}
interface ClearAnimationOptions {
/** 基点位置,即 CSS transform-origin */
transformOrigin?: boolean
/** 背景颜色,即 CSS background-color */
backgroundColor?: boolean
/** 底边位置,即 CSS bottom */
bottom?: boolean
/** 高度,即 CSS height */
height?: boolean
/** 左边位置,即 CSS left */
left?: boolean
/** 宽度,即 CSS width */
width?: boolean
/** 不透明度,即 CSS opacity */
opacity?: boolean
/** 右边位置,即 CSS right */
right?: boolean
/** 顶边位置,即 CSS top */
top?: boolean
/** 变换矩阵,即 CSS transform matrix */
matrix?: boolean
/** 三维变换矩阵,即 CSS transform matrix3d */
matrix3d?: boolean
/** 旋转,即 CSS transform rotate */
rotate?: boolean
/** 三维旋转,即 CSS transform rotate3d */
rotate3d?: boolean
/** X 方向旋转,即 CSS transform rotateX */
rotateX?: boolean
/** Y 方向旋转,即 CSS transform rotateY */
rotateY?: boolean
/** Z 方向旋转,即 CSS transform rotateZ */
rotateZ?: boolean
/** 缩放,即 CSS transform scale */
scale?: boolean
/** 三维缩放,即 CSS transform scale3d */
scale3d?: boolean
/** X 方向缩放,即 CSS transform scaleX */
scaleX?: boolean
/** Y 方向缩放,即 CSS transform scaleY */
scaleY?: boolean
/** Z 方向缩放,即 CSS transform scaleZ */
scaleZ?: boolean
/** 倾斜,即 CSS transform skew */
skew?: boolean
/** X 方向倾斜,即 CSS transform skewX */
skewX?: boolean
/** Y 方向倾斜,即 CSS transform skewY */
skewY?: boolean
/** 位移,即 CSS transform translate */
translate?: boolean
/** 三维位移,即 CSS transform translate3d */
translate3d?: boolean
/** X 方向位移,即 CSS transform translateX */
translateX?: boolean
/** Y 方向位移,即 CSS transform translateY */
translateY?: boolean
/** Z 方向位移,即 CSS transform translateZ */
translateZ?: boolean
}
interface ScrollTimelineKeyframe {
composite?: 'replace' | 'add' | 'accumulate' | 'auto'
easing?: string
offset?: number | null
[property: string]: string | number | null | undefined
}
interface ScrollTimelineOption {
/** 指定滚动元素的选择器(只支持 scroll-view该元素滚动时会驱动动画的进度 */
scrollSource: string
/** 指定滚动的方向。有效值为 horizontal 或 vertical */
orientation?: string
/** 指定开始驱动动画进度的滚动偏移量,单位 px */
startScrollOffset: number
/** 指定停止驱动动画进度的滚动偏移量,单位 px */
endScrollOffset: number
/** 起始和结束的滚动范围映射的时间长度,该时间可用于与关键帧动画里的时间 (duration) 相匹配,单位 ms */
timeRange: number
}
}
/** ComponentComponent
*
* * 使 `this.data` 使 `setData`
* * `this` 访
* * data `dataXyz` WXML `data-xyz=""` dataset
* * 使 data
* * `2.0.9` data
* * `bug` : type Object Array `this.setData` observer observer `newVal` `oldVal` `changedPath`
*/
declare let Component: WechatMiniprogram.Component.Constructor

File diff suppressed because it is too large Load Diff

@ -0,0 +1,259 @@
/*! *****************************************************************************
Copyright (c) 2021 Tencent, Inc. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
***************************************************************************** */
declare namespace WechatMiniprogram.Page {
type Instance<
TData extends DataOption,
TCustom extends CustomOption
> = OptionalInterface<ILifetime> &
InstanceProperties &
InstanceMethods<TData> &
Data<TData> &
TCustom
type Options<
TData extends DataOption,
TCustom extends CustomOption
> = (TCustom & Partial<Data<TData>> & Partial<ILifetime>) &
ThisType<Instance<TData, TCustom>>
type TrivialInstance = Instance<IAnyObject, IAnyObject>
interface Constructor {
<TData extends DataOption, TCustom extends CustomOption>(
options: Options<TData, TCustom>
): void
}
interface ILifetime {
/**
*
* onLoad
*/
onLoad(
/** 打开当前页面路径中的参数 */
query: Record<string, string | undefined>
): void | Promise<void>
/**
*
* /
*/
onShow(): void | Promise<void>
/**
*
*
*
* API `wx.setNavigationBarTitle``onReady`
*/
onReady(): void | Promise<void>
/**
*
* / `navigateTo` `tab`
*/
onHide(): void | Promise<void>
/**
*
* `redirectTo``navigateBack`
*/
onUnload(): void | Promise<void>
/**
*
*
* - `app.json``window``enablePullDownRefresh`
* - `wx.startPullDownRefresh`
* - `wx.stopPullDownRefresh`
*/
onPullDownRefresh(): void | Promise<void>
/**
*
*
* - `app.json``window``onReachBottomDistance`
* -
*/
onReachBottom(): void | Promise<void>
/**
*
* `<button>` `open-type="share"`
*
* ****
*
* return Object
*/
onShareAppMessage(
/** 分享发起来源参数 */
options: IShareAppMessageOption
): ICustomShareContent | void
/**
*
*
* Beta Android [ (Beta)](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/share-timeline.html)
*
* 2.11.3
*/
onShareTimeline(): ICustomTimelineContent | void
/**
*
*
*/
onPageScroll(
/** 页面滚动参数 */
options: IPageScrollOption
): void | Promise<void>
/** 当前是 tab 页时,点击 tab 时触发,最低基础库: `1.9.0` */
onTabItemTap(
/** tab 点击参数 */
options: ITabItemTapOption
): void | Promise<void>
/** 窗口尺寸改变时触发,最低基础库:`2.4.0` */
onResize(
/** 窗口尺寸参数 */
options: IResizeOption
): void | Promise<void>
/**
*
* 2.10.3 7.0.15 iOS
*/
onAddToFavorites(options: IAddToFavoritesOption): IAddToFavoritesContent
}
interface InstanceProperties {
/** 页面的文件路径 */
is: string
/** 到当前页面的路径 */
route: string
/** 打开当前页面路径中的参数 */
options: Record<string, string | undefined>
}
type DataOption = Record<string, any>
type CustomOption = Record<string, any>
type InstanceMethods<D extends DataOption> = Component.InstanceMethods<D>
interface Data<D extends DataOption> {
/**
*
* `data` 使****
*
* `data` `JSON``data``JSON`
*
* `WXML`
*/
data: D
}
interface ICustomShareContent {
/** 转发标题。默认值:当前小程序名称 */
title?: string
/** 转发路径,必须是以 / 开头的完整路径。默认值:当前页面 path */
path?: string
/** 自定义图片路径可以是本地文件路径、代码包文件路径或者网络图片路径。支持PNG及JPG。显示图片长宽比是 5:4最低基础库 `1.5.0`。默认值:使用默认截图 */
imageUrl?: string
}
interface ICustomTimelineContent {
/** 自定义标题,即朋友圈列表页上显示的标题。默认值:当前小程序名称 */
title?: string
/** 自定义页面路径中携带的参数,如 `path?a=1&b=2` 的 “?” 后面部分 默认值:当前页面路径携带的参数 */
query?: string
/** 自定义图片路径,可以是本地文件路径、代码包文件路径或者网络图片路径。支持 PNG 及 JPG。显示图片长宽比是 1:1。默认值默认使用小程序 Logo*/
imageUrl?: string
}
interface IPageScrollOption {
/** 页面在垂直方向已滚动的距离单位px */
scrollTop: number
}
interface IShareAppMessageOption {
/**
*
*
* - `button`
* - `menu`
*
* `1.2.4`
*/
from: 'button' | 'menu' | string
/** `from` `button` `target` `button` `undefined`
*
* `1.2.4` */
target: any
/** `<web-view>``<web-view>`url
*
* `1.6.4`
*/
webViewUrl?: string
}
interface ITabItemTapOption {
/** 被点击tabItem的序号从0开始最低基础库 `1.9.0` */
index: string
/** 被点击tabItem的页面路径最低基础库 `1.9.0` */
pagePath: string
/** 被点击tabItem的按钮文字最低基础库 `1.9.0` */
text: string
}
interface IResizeOption {
size: {
/** 变化后的窗口宽度,单位 px */
windowWidth: number
/** 变化后的窗口高度,单位 px */
windowHeight: number
}
}
interface IAddToFavoritesOption {
/** 页面中包含web-view组件时返回当前web-view的url */
webviewUrl?: string
}
interface IAddToFavoritesContent {
/** 自定义标题,默认值:页面标题或账号名称 */
title?: string
/** 自定义图片,显示图片长宽比为 11默认值页面截图 */
imageUrl?: string
/** 自定义query字段默认值当前页面的query */
query?: string
}
interface GetCurrentPages {
(): Array<Instance<IAnyObject, IAnyObject>>
}
}
/**
* `Object`
*/
declare let Page: WechatMiniprogram.Page.Constructor
/**
*
* ____
* - ____
* - `App.onLaunch` `getCurrentPages()` `page`
*/
declare let getCurrentPages: WechatMiniprogram.Page.GetCurrentPages

@ -0,0 +1,30 @@
{
"pages": [
"pages/index/index",
"pages/logs/logs",
"pages/report/form",
"pages/repairs/list",
"pages/repairs/detail",
"pages/tech/list",
"pages/tech/auth"
],
"window": {
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
},
"style": "v2",
"renderer": "skyline",
"rendererOptions": {
"skyline": {
"defaultDisplayBlock": true,
"defaultContentBox": true,
"tagNameStyleIsolation": "legacy",
"disableABTest": true,
"sdkVersionBegin": "3.0.0",
"sdkVersionEnd": "15.255.255"
}
},
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

@ -0,0 +1,18 @@
// app.ts
App<IAppOption>({
globalData: {},
onLaunch() {
// 展示本地存储能力
const logs = wx.getStorageSync('logs') || []
logs.unshift(Date.now())
wx.setStorageSync('logs', logs)
// 登录
wx.login({
success: res => {
console.log(res.code)
// 发送 res.code 到后台换取 openId, sessionKey, unionId
},
})
},
})

@ -0,0 +1,10 @@
/**app.wxss**/
.container {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 200rpx 0;
box-sizing: border-box;
}

@ -0,0 +1,5 @@
{
"component": true,
"styleIsolation": "apply-shared",
"usingComponents": {}
}

@ -0,0 +1,105 @@
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
/**
*
*/
properties: {
extClass: {
type: String,
value: ''
},
title: {
type: String,
value: ''
},
background: {
type: String,
value: ''
},
color: {
type: String,
value: ''
},
back: {
type: Boolean,
value: true
},
loading: {
type: Boolean,
value: false
},
homeButton: {
type: Boolean,
value: false,
},
animated: {
// 显示隐藏的时候opacity动画效果
type: Boolean,
value: true
},
show: {
// 显示隐藏导航隐藏的时候navigation-bar的高度占位还在
type: Boolean,
value: true,
observer: '_showChange'
},
// back为true的时候返回的页面深度
delta: {
type: Number,
value: 1
},
},
/**
*
*/
data: {
displayStyle: ''
},
lifetimes: {
attached() {
const rect = wx.getMenuButtonBoundingClientRect()
wx.getSystemInfo({
success: (res) => {
const isAndroid = res.platform === 'android'
const isDevtools = res.platform === 'devtools'
this.setData({
ios: !isAndroid,
innerPaddingRight: `padding-right: ${res.windowWidth - rect.left}px`,
leftWidth: `width: ${res.windowWidth - rect.left }px`,
safeAreaTop: isDevtools || isAndroid ? `height: calc(var(--height) + ${res.safeArea.top}px); padding-top: ${res.safeArea.top}px` : ``
})
}
})
},
},
/**
*
*/
methods: {
_showChange(show: boolean) {
const animated = this.data.animated
let displayStyle = ''
if (animated) {
displayStyle = `opacity: ${
show ? '1' : '0'
};transition:opacity 0.5s;`
} else {
displayStyle = `display: ${show ? '' : 'none'}`
}
this.setData({
displayStyle
})
},
back() {
const data = this.data
if (data.delta) {
wx.navigateBack({
delta: data.delta
})
}
this.triggerEvent('back', { delta: data.delta }, {})
}
},
})

@ -0,0 +1,64 @@
<view class="weui-navigation-bar {{extClass}}">
<view class="weui-navigation-bar__inner {{ios ? 'ios' : 'android'}}" style="color: {{color}}; background: {{background}}; {{displayStyle}}; {{innerPaddingRight}}; {{safeAreaTop}};">
<!-- 左侧按钮 -->
<view class='weui-navigation-bar__left' style="{{leftWidth}};">
<block wx:if="{{back || homeButton}}">
<!-- 返回上一页 -->
<block wx:if="{{back}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_goback">
<view
bindtap="back"
class="weui-navigation-bar__btn_goback_wrapper"
hover-class="weui-active"
hover-stay-time="100"
aria-role="button"
aria-label="返回"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_goback"></view>
</view>
</view>
</block>
<!-- 返回首页 -->
<block wx:if="{{homeButton}}">
<view class="weui-navigation-bar__buttons weui-navigation-bar__buttons_home">
<view
bindtap="home"
class="weui-navigation-bar__btn_home_wrapper"
hover-class="weui-active"
aria-role="button"
aria-label="首页"
>
<view class="weui-navigation-bar__button weui-navigation-bar__btn_home"></view>
</view>
</view>
</block>
</block>
<block wx:else>
<slot name="left"></slot>
</block>
</view>
<!-- 标题 -->
<view class='weui-navigation-bar__center'>
<view wx:if="{{loading}}" class="weui-navigation-bar__loading" aria-role="alert">
<view
class="weui-loading"
aria-role="img"
aria-label="加载中"
></view>
</view>
<block wx:if="{{title}}">
<text>{{title}}</text>
</block>
<block wx:else>
<slot name="center"></slot>
</block>
</view>
<!-- 右侧留空 -->
<view class='weui-navigation-bar__right'>
<slot name="right"></slot>
</view>
</view>
</view>

@ -0,0 +1,96 @@
.weui-navigation-bar {
--weui-FG-0:rgba(0,0,0,.9);
--height: 44px;
--left: 16px;
}
.weui-navigation-bar .android {
--height: 48px;
}
.weui-navigation-bar {
overflow: hidden;
color: var(--weui-FG-0);
flex: none;
}
.weui-navigation-bar__inner {
position: relative;
top: 0;
left: 0;
height: calc(var(--height) + env(safe-area-inset-top));
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding-top: env(safe-area-inset-top);
width: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__left {
position: relative;
padding-left: var(--left);
display: flex;
flex-direction: row;
align-items: flex-start;
height: 100%;
box-sizing: border-box;
}
.weui-navigation-bar__btn_goback_wrapper {
padding: 11px 18px 11px 16px;
margin: -11px -18px -11px -16px;
}
.weui-navigation-bar__btn_goback_wrapper.weui-active {
opacity: 0.5;
}
.weui-navigation-bar__btn_goback {
font-size: 12px;
width: 12px;
height: 24px;
-webkit-mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
mask: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='24' viewBox='0 0 12 24'%3E %3Cpath fill-opacity='.9' fill-rule='evenodd' d='M10 19.438L8.955 20.5l-7.666-7.79a1.02 1.02 0 0 1 0-1.42L8.955 3.5 10 4.563 2.682 12 10 19.438z'/%3E%3C/svg%3E") no-repeat 50% 50%;
-webkit-mask-size: cover;
mask-size: cover;
background-color: var(--weui-FG-0);
}
.weui-navigation-bar__center {
font-size: 17px;
text-align: center;
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
font-weight: bold;
flex: 1;
height: 100%;
}
.weui-navigation-bar__loading {
margin-right: 4px;
align-items: center;
}
.weui-loading {
font-size: 16px;
width: 16px;
height: 16px;
display: block;
background: transparent url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='80px' height='80px' viewBox='0 0 80 80' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3Ctitle%3Eloading%3C/title%3E%3Cdefs%3E%3ClinearGradient x1='94.0869141%25' y1='0%25' x2='94.0869141%25' y2='90.559082%25' id='linearGradient-1'%3E%3Cstop stop-color='%23606060' stop-opacity='0' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3ClinearGradient x1='100%25' y1='8.67370605%25' x2='100%25' y2='90.6286621%25' id='linearGradient-2'%3E%3Cstop stop-color='%23606060' offset='0%25'%3E%3C/stop%3E%3Cstop stop-color='%23606060' stop-opacity='0.3' offset='100%25'%3E%3C/stop%3E%3C/linearGradient%3E%3C/defs%3E%3Cg stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' opacity='0.9'%3E%3Cg%3E%3Cpath d='M40,0 C62.09139,0 80,17.90861 80,40 C80,62.09139 62.09139,80 40,80 L40,73 C58.2253967,73 73,58.2253967 73,40 C73,21.7746033 58.2253967,7 40,7 L40,0 Z' fill='url(%23linearGradient-1)'%3E%3C/path%3E%3Cpath d='M40,0 L40,7 C21.7746033,7 7,21.7746033 7,40 C7,58.2253967 21.7746033,73 40,73 L40,80 C17.90861,80 0,62.09139 0,40 C0,17.90861 17.90861,0 40,0 Z' fill='url(%23linearGradient-2)'%3E%3C/path%3E%3Ccircle id='Oval' fill='%23606060' cx='40.5' cy='3.5' r='3.5'%3E%3C/circle%3E%3C/g%3E%3C/g%3E%3C/svg%3E%0A") no-repeat;
background-size: 100%;
margin-left: 0;
animation: loading linear infinite 1s;
}
@keyframes loading {
from {
transform: rotate(0);
}
to {
transform: rotate(360deg);
}
}

@ -0,0 +1,5 @@
{
"usingComponents": {
"navigation-bar": "/components/navigation-bar/navigation-bar"
}
}

@ -0,0 +1,193 @@
// index.ts
// 获取应用实例
const app = getApp<IAppOption>()
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
import { getRole, setRole } from '../../utils/role'
import { isLoggedIn, logout, clearAllAccounts } from '../../utils/techAuth'
Component({
data: {
motto: '报修系统',
userInfo: {
avatarUrl: defaultAvatarUrl,
nickName: '',
},
hasUserInfo: false,
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
userRole: 'student' as 'student' | 'technician',
isTechLoggedIn: false,
},
lifetimes: {
attached() {
this.checkUserRole()
}
},
pageLifetimes: {
show() {
this.checkUserRole()
}
},
methods: {
checkUserRole() {
const role = getRole()
const isTechLoggedIn = isLoggedIn()
// 检查是否有保存的登录状态
const lastLoginRole = wx.getStorageSync('last_login_role')
const lastLoginTime = wx.getStorageSync('last_login_time')
// 如果当前是学生角色但有维修人员登录记录且登录时间在24小时内则自动恢复
if (role === 'student' && lastLoginRole === 'technician' && isTechLoggedIn) {
const now = Date.now()
const oneDay = 24 * 60 * 60 * 1000 // 24小时
if (lastLoginTime && (now - lastLoginTime) < oneDay) {
setRole('technician')
wx.showToast({ title: '欢迎回来,维修人员', icon: 'success' })
}
}
// 重新获取角色状态
const currentRole = getRole()
const currentTechLoggedIn = isLoggedIn()
this.setData({
userRole: currentRole,
isTechLoggedIn: currentTechLoggedIn
})
},
// 事件处理函数
bindViewTap() {
wx.navigateTo({
url: '../logs/logs',
})
},
goToReport() {
if (this.data.userRole === 'technician') {
wx.showToast({ title: '维修人员不能提交报修', icon: 'none' })
return
}
wx.navigateTo({ url: '/pages/report/form' })
},
goToMyRepairs() {
wx.navigateTo({ url: '/pages/repairs/list' })
},
goToTech() {
if (this.data.isTechLoggedIn) {
wx.navigateTo({ url: '/pages/tech/list' })
} else {
wx.navigateTo({ url: '/pages/tech/auth' })
}
},
switchToStudent() {
setRole('student')
// 保存学生角色状态
wx.setStorageSync('last_login_role', 'student')
wx.setStorageSync('last_login_time', Date.now())
wx.showToast({ title: '已切换到学生模式', icon: 'success' })
this.checkUserRole()
},
showIdentitySwitch() {
const currentRole = this.data.userRole
const isTechLoggedIn = this.data.isTechLoggedIn
if (currentRole === 'student') {
// 学生切换到维修人员 - 直接跳转到维修人员界面
wx.navigateTo({
url: '/pages/tech/auth',
success: () => {
console.log('跳转到维修人员界面')
},
fail: (err) => {
console.error('跳转失败:', err)
}
})
} else {
// 维修人员切换回学生
if (isTechLoggedIn) {
wx.showActionSheet({
itemList: ['切换回学生身份', '退出维修人员登录'],
success: (res) => {
if (res.tapIndex === 0) {
this.switchToStudent()
} else if (res.tapIndex === 1) {
this.showLogoutConfirm()
}
}
})
} else {
this.switchToStudent()
}
}
},
showLogoutConfirm() {
wx.showModal({
title: '退出登录',
content: '确定要退出维修人员账号吗?',
confirmText: '确定退出',
cancelText: '取消',
confirmColor: '#dc3545',
success: (res) => {
if (res.confirm) {
this.doLogout()
}
}
})
},
doLogout() {
logout()
setRole('student')
// 清除登录状态
wx.removeStorageSync('last_login_role')
wx.removeStorageSync('last_login_time')
wx.showToast({ title: '已退出登录', icon: 'success' })
this.checkUserRole()
},
onChooseAvatar(e: any) {
const { avatarUrl } = e.detail
const { nickName } = this.data.userInfo
this.setData({
"userInfo.avatarUrl": avatarUrl,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
onInputChange(e: any) {
const nickName = e.detail.value
const { avatarUrl } = this.data.userInfo
this.setData({
"userInfo.nickName": nickName,
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
})
},
getUserProfile() {
// 推荐使用wx.getUserProfile获取用户信息开发者每次通过该接口获取用户个人信息均需用户确认开发者妥善保管用户快速填写的头像昵称避免重复弹窗
wx.getUserProfile({
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
success: (res) => {
console.log(res)
this.setData({
userInfo: res.userInfo,
hasUserInfo: true
})
}
})
},
clearAllTechAccounts() {
wx.showModal({
title: '清除所有维修人员账号',
content: '确定要删除所有已注册的维修人员账号吗?此操作不可恢复!',
confirmText: '确定删除',
cancelText: '取消',
confirmColor: '#dc3545',
success: (res) => {
if (res.confirm) {
clearAllAccounts()
wx.showToast({ title: '所有账号已清除', icon: 'success' })
// 刷新页面状态
this.checkUserRole()
}
}
})
},
},
})

@ -0,0 +1,54 @@
<!--index.wxml-->
<navigation-bar title="Weixin" back="{{false}}" color="black" background="#FFF"></navigation-bar>
<scroll-view class="scrollarea" scroll-y type="list">
<view class="container">
<view class="userinfo">
<block wx:if="{{canIUseNicknameComp && !hasUserInfo}}">
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
</button>
<view class="nickname-wrapper">
<text class="nickname-label">昵称</text>
<input type="text" class="nickname-input" placeholder="请输入昵称(支持中文)" bindinput="onInputChange" confirm-type="done" hold-keyboard="{{true}}" adjust-position="{{true}}" />
</view>
</block>
<block wx:elif="{{!hasUserInfo}}">
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
<view wx:else> 请使用2.10.4及以上版本基础库 </view>
</block>
<block wx:else>
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
</block>
</view>
<view class="usermotto">
<text class="user-motto">{{motto}}</text>
</view>
<view class="entry">
<block wx:if="{{userRole === 'student'}}">
<button class="primary" size="mini" type="primary" bindtap="goToReport">提交报修</button>
<button size="mini" bindtap="goToMyRepairs">我的报修</button>
</block>
<block wx:elif="{{userRole === 'technician'}}">
<button size="mini" bindtap="goToMyRepairs">报修列表</button>
<button size="mini" type="primary" bindtap="goToTech">维修管理</button>
</block>
</view>
<!-- 身份切换区域 -->
<view class="identity-section">
<view class="current-identity">
<text class="identity-label">当前身份:</text>
<text class="identity-value">{{userRole === 'student' ? '学生' : '维修人员'}}</text>
<text class="login-status" wx:if="{{userRole === 'technician' && isTechLoggedIn}}">(已登录)</text>
</view>
<view class="identity-actions">
<button class="switch-btn" size="mini" bindtap="showIdentitySwitch">切换身份</button>
</view>
<view class="identity-actions">
<button class="clear-btn" size="mini" bindtap="clearAllTechAccounts">清除维修人员账号</button>
</view>
</view>
</view>
</scroll-view>

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save