feat: 初始化居家上门服务系统完整项目代码
- Spring Boot 后端服务 (hss-home-service) - delivery-miniapp 配送小程序 - website 官网 (Nuxt) - docs 架构设计文档 - Docker 容器化部署配置 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
26
hss-home-service/delivery-miniapp/common/api.js
Normal file
26
hss-home-service/delivery-miniapp/common/api.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const BASE_URL = 'http://localhost:18080/api/hss';
|
||||
|
||||
function getHeaders() {
|
||||
const token = uni.getStorageSync('token');
|
||||
return {
|
||||
'Authorization': token ? 'Bearer ' + token : '',
|
||||
'X-User-Role': 'STAFF',
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
}
|
||||
|
||||
function generateIdempotencyKey() {
|
||||
return 'idem-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
function apiGet(path, params = {}) {
|
||||
return uni.request({ url: BASE_URL + path, method: 'GET', data: params, header: getHeaders() });
|
||||
}
|
||||
|
||||
function apiPost(path, data = {}) {
|
||||
const headers = getHeaders();
|
||||
headers['Idempotency-Key'] = generateIdempotencyKey();
|
||||
return uni.request({ url: BASE_URL + path, method: 'POST', data, header: headers });
|
||||
}
|
||||
|
||||
module.exports = { BASE_URL, apiGet, apiPost, generateIdempotencyKey };
|
||||
49
hss-home-service/delivery-miniapp/manifest.json
Normal file
49
hss-home-service/delivery-miniapp/manifest.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "居家上门服务-delivery",
|
||||
"appid": "__UNI__HSS_DELIVERY",
|
||||
"description": "服务人员移动作业端",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"transformPx": false,
|
||||
"app-plus": {
|
||||
"usingComponents": true,
|
||||
"nvueStyleCompiler": "uni-app",
|
||||
"compilerVersion": 3,
|
||||
"splashscreen": {
|
||||
"alwaysShowBeforeRender": true,
|
||||
"waiting": true,
|
||||
"autoclose": true,
|
||||
"delay": 0
|
||||
},
|
||||
"modules": {
|
||||
"Maps": {},
|
||||
"Geolocation": {},
|
||||
"Camera": {}
|
||||
},
|
||||
"distribute": {
|
||||
"android": {
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
|
||||
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
|
||||
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
|
||||
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>"
|
||||
]
|
||||
},
|
||||
"ios": {}
|
||||
}
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
},
|
||||
"usingComponents": true,
|
||||
"permission": {
|
||||
"scope.userLocation": {
|
||||
"desc": "需要获取位置进行GPS签到"
|
||||
}
|
||||
},
|
||||
"requiredPrivateInfos": ["getLocation", "chooseLocation"]
|
||||
}
|
||||
}
|
||||
58
hss-home-service/delivery-miniapp/pages.json
Normal file
58
hss-home-service/delivery-miniapp/pages.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/delivery/login/login",
|
||||
"style": { "navigationBarTitleText": "服务人员登录" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/index/index",
|
||||
"style": { "navigationBarTitleText": "工作台" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/orders/orders",
|
||||
"style": { "navigationBarTitleText": "工单列表" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/order-detail/order-detail",
|
||||
"style": { "navigationBarTitleText": "工单详情" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/accept/accept",
|
||||
"style": { "navigationBarTitleText": "接单确认" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/checkin/checkin",
|
||||
"style": { "navigationBarTitleText": "GPS签到" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/execute/execute",
|
||||
"style": { "navigationBarTitleText": "服务执行" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/exception/exception",
|
||||
"style": { "navigationBarTitleText": "异常上报" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/finish/finish",
|
||||
"style": { "navigationBarTitleText": "签退完成" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/offline-sync/offline-sync",
|
||||
"style": { "navigationBarTitleText": "离线补传" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/messages/messages",
|
||||
"style": { "navigationBarTitleText": "消息通知" }
|
||||
},
|
||||
{
|
||||
"path": "pages/delivery/profile/profile",
|
||||
"style": { "navigationBarTitleText": "我的资质" }
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "居家上门服务",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<view class="workbench">
|
||||
<view class="stats-row">
|
||||
<view class="stat-item"><text class="stat-num">{{ stats.todayTotal }}</text><text>今日工单</text></view>
|
||||
<view class="stat-item"><text class="stat-num">{{ stats.completedToday }}</text><text>已完成</text></view>
|
||||
<view class="stat-item warn"><text class="stat-num">{{ stats.exceptionCount }}</text><text>异常</text></view>
|
||||
</view>
|
||||
<view class="quick-actions">
|
||||
<view class="action-card" @click="navTo('/pages/delivery/orders/orders?status=ORDER_ASSIGNED')">
|
||||
<text class="action-num">{{ stats.pendingAccept }}</text><text>待接单</text>
|
||||
</view>
|
||||
<view class="action-card" @click="navTo('/pages/delivery/orders/orders?status=ORDER_ACCEPTED')">
|
||||
<text class="action-num">{{ stats.pendingCheckin }}</text><text>待签到</text>
|
||||
</view>
|
||||
<view class="action-card" @click="navTo('/pages/delivery/orders/orders?status=ORDER_IN_SERVICE')">
|
||||
<text class="action-num">{{ stats.inService }}</text><text>服务中</text>
|
||||
</view>
|
||||
<view class="action-card" @click="navTo('/pages/delivery/offline-sync/offline-sync')">
|
||||
<text>离线补传</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="order-list">
|
||||
<text class="section-title">今日任务</text>
|
||||
<view v-for="o in todayOrders" :key="o.id" class="order-item" @click="navTo('/pages/delivery/order-detail/order-detail?id='+o.id)">
|
||||
<text class="patient-name">{{ o.patient_name }}</text>
|
||||
<text class="time">{{ o.time_window_start }} - {{ o.time_window_end }}</text>
|
||||
<text :class="'status status-'+o.status">{{ statusText(o.status) }}</text>
|
||||
</view>
|
||||
<view v-if="todayOrders.length === 0" class="empty">暂无今日任务</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { apiGet } from '@/common/api.js';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
stats: { todayTotal: 0, completedToday: 0, exceptionCount: 0, pendingAccept: 0, pendingCheckin: 0, inService: 0 },
|
||||
todayOrders: []
|
||||
};
|
||||
},
|
||||
onShow() { this.loadData(); },
|
||||
methods: {
|
||||
async loadData() {
|
||||
try {
|
||||
const [wbRes, ordersRes] = await Promise.all([
|
||||
apiGet('/delivery/workbench'),
|
||||
apiGet('/delivery/work-orders/today')
|
||||
]);
|
||||
if (wbRes.data.code === 200) this.stats = wbRes.data.data;
|
||||
if (ordersRes.data.code === 200) this.todayOrders = ordersRes.data.data;
|
||||
} catch (e) {}
|
||||
},
|
||||
navTo(url) { uni.navigateTo({ url }); },
|
||||
statusText(s) {
|
||||
const m = { ORDER_ASSIGNED: '待接单', ORDER_ACCEPTED: '待签到', ORDER_CHECKED_IN: '待服务', ORDER_IN_SERVICE: '服务中', ORDER_COMPLETED: '已完成', ORDER_EXCEPTION: '异常' };
|
||||
return m[s] || s;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.workbench { padding: 20rpx; }
|
||||
.stats-row { display: flex; justify-content: space-around; padding: 30rpx; background: #fff; border-radius: 16rpx; margin-bottom: 20rpx; }
|
||||
.stat-item { text-align: center; }
|
||||
.stat-num { font-size: 48rpx; font-weight: bold; display: block; }
|
||||
.stat-item.warn .stat-num { color: #ff4d4f; }
|
||||
.quick-actions { display: flex; flex-wrap: wrap; gap: 20rpx; margin-bottom: 20rpx; }
|
||||
.action-card { flex: 1; min-width: 150rpx; background: #fff; padding: 30rpx; border-radius: 16rpx; text-align: center; }
|
||||
.action-num { font-size: 40rpx; font-weight: bold; display: block; color: #1677ff; }
|
||||
.section-title { font-size: 32rpx; font-weight: bold; margin: 20rpx 0; display: block; }
|
||||
.order-item { background: #fff; padding: 24rpx; border-radius: 12rpx; margin-bottom: 12rpx; display: flex; justify-content: space-between; }
|
||||
.empty { text-align: center; color: #999; padding: 60rpx; }
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<view class="login-container">
|
||||
<view class="logo-area">
|
||||
<text class="title">居家上门服务</text>
|
||||
<text class="subtitle">服务人员端</text>
|
||||
</view>
|
||||
<view class="form-area">
|
||||
<input class="input" v-model="username" placeholder="请输入账号" />
|
||||
<input class="input" v-model="password" type="password" placeholder="请输入密码" />
|
||||
<button class="login-btn" @click="handleLogin" :disabled="loading">
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BASE_URL } from '@/common/api.js';
|
||||
export default {
|
||||
data() {
|
||||
return { username: '', password: '', loading: false };
|
||||
},
|
||||
methods: {
|
||||
async handleLogin() {
|
||||
if (!this.username || !this.password) {
|
||||
uni.showToast({ title: '请输入账号密码', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await uni.request({
|
||||
url: BASE_URL + '/auth/login',
|
||||
method: 'POST',
|
||||
data: { username: this.username, password: this.password, role: 'STAFF' }
|
||||
});
|
||||
if (res.data.code === 200) {
|
||||
uni.setStorageSync('token', res.data.data.token);
|
||||
uni.setStorageSync('userInfo', res.data.data);
|
||||
uni.switchTab({ url: '/pages/delivery/index/index' });
|
||||
} else {
|
||||
uni.showToast({ title: res.data.message, icon: 'none' });
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '网络错误', icon: 'none' });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.login-container { padding: 80rpx 60rpx; }
|
||||
.logo-area { text-align: center; margin-bottom: 80rpx; }
|
||||
.title { font-size: 48rpx; font-weight: bold; display: block; }
|
||||
.subtitle { font-size: 28rpx; color: #999; margin-top: 10rpx; }
|
||||
.input { border: 1px solid #ddd; border-radius: 12rpx; padding: 24rpx; margin-bottom: 24rpx; font-size: 30rpx; }
|
||||
.login-btn { background: #1677ff; color: #fff; border-radius: 12rpx; height: 88rpx; line-height: 88rpx; font-size: 32rpx; margin-top: 40rpx; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user