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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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,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" />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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'>>
|
||||
File diff suppressed because it is too large
Load Diff
@ -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,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,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…
Reference in new issue