feat: add rider h5 MVP

This commit is contained in:
王鹏
2026-07-07 16:44:48 +08:00
parent 576c54ec9d
commit 9ad43005c4
17 changed files with 3195 additions and 0 deletions

12
rider-h5/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>邻小帮配送员</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

2185
rider-h5/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

3
rider-h5/src/App.vue Normal file
View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

45
rider-h5/src/api/http.ts Normal file
View File

@@ -0,0 +1,45 @@
import axios, { AxiosHeaders } from 'axios'
export interface ApiEnvelope<T> {
code: number
message: string
data: T
}
export const TOKEN_KEY = 'rider_token'
export const USER_KEY = 'rider_user'
export const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || ''
})
http.interceptors.request.use((config) => {
const token = localStorage.getItem(TOKEN_KEY)
if (token) {
const headers = AxiosHeaders.from(config.headers)
headers.set('satoken', token)
headers.set('Authorization', token)
config.headers = headers
}
return config
})
async function unwrap<T>(request: Promise<{ data: ApiEnvelope<T> }>): Promise<T> {
const response = await request
if (response.data.code !== 0) {
throw new Error(response.data.message || '请求失败')
}
return response.data.data
}
export function apiGet<T>(url: string, params?: Record<string, unknown>) {
return unwrap<T>(http.get(url, { params }))
}
export function apiPost<T>(url: string, data?: unknown) {
return unwrap<T>(http.post(url, data))
}
export function apiPut<T>(url: string, data?: unknown) {
return unwrap<T>(http.put(url, data))
}

View File

@@ -0,0 +1,18 @@
import { describe, expect, it } from 'vitest'
import { groupTasksByStatus } from './riderTaskApi'
describe('groupTasksByStatus', () => {
it('groups assigned delivering completed and exception tasks', () => {
const grouped = groupTasksByStatus([
{ id: 1, status: 'ASSIGNED' },
{ id: 2, status: 'DELIVERING' },
{ id: 3, status: 'DELIVERED' },
{ id: 4, status: 'EXCEPTION' }
])
expect(grouped.assigned).toHaveLength(1)
expect(grouped.delivering).toHaveLength(1)
expect(grouped.completed).toHaveLength(1)
expect(grouped.exception).toHaveLength(1)
})
})

View File

@@ -0,0 +1,58 @@
import type { CurrentUser, DeliveryTask, FileUploadResult, RiderIncomeSummary } from '../types'
import { apiGet, apiPost, apiPut, http } from './http'
export interface TaskGroups<T extends { status: string }> {
assigned: T[]
delivering: T[]
completed: T[]
exception: T[]
canceled: T[]
}
export function groupTasksByStatus<T extends { status: string }>(tasks: T[]): TaskGroups<T> {
return {
assigned: tasks.filter((task) => task.status === 'ASSIGNED' || task.status === 'ACCEPTED'),
delivering: tasks.filter((task) => task.status === 'DELIVERING'),
completed: tasks.filter((task) => task.status === 'DELIVERED' || task.status === 'COMPLETED'),
exception: tasks.filter((task) => task.status === 'EXCEPTION'),
canceled: tasks.filter((task) => task.status === 'CANCELED')
}
}
export function riderLogin(username: string, password: string) {
return apiPost<CurrentUser>('/api/auth/admin-login', { username, password })
}
export function listRiderTasks() {
return apiGet<DeliveryTask[]>('/api/rider/tasks')
}
export function getRiderTask(id: number) {
return apiGet<DeliveryTask>(`/api/rider/tasks/${id}`)
}
export function startRiderTask(id: number) {
return apiPut<DeliveryTask>(`/api/rider/tasks/${id}/start`)
}
export function deliverRiderTask(id: number, photoUrl?: string) {
return apiPut<DeliveryTask>(`/api/rider/tasks/${id}/delivered`, { photoUrl })
}
export function markRiderTaskException(id: number, exceptionType: string, exceptionNote: string) {
return apiPut<DeliveryTask>(`/api/rider/tasks/${id}/exception`, { exceptionType, exceptionNote })
}
export function getIncomeSummary() {
return apiGet<RiderIncomeSummary>('/api/rider/income-summary')
}
export async function uploadDeliveredPhoto(file: File) {
const data = new FormData()
data.append('file', file)
const response = await http.post<{ code: number; message: string; data: FileUploadResult }>('/api/files/upload', data)
if (response.data.code !== 0) {
throw new Error(response.data.message || '上传失败')
}
return response.data.data
}

1
rider-h5/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

6
rider-h5/src/main.ts Normal file
View File

@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import { router } from './router'
import './styles.css'
createApp(App).use(router).mount('#app')

View File

@@ -0,0 +1,26 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import LoginView from '../views/LoginView.vue'
import TaskListView from '../views/TaskListView.vue'
import TaskDetailView from '../views/TaskDetailView.vue'
import IncomeSummaryView from '../views/IncomeSummaryView.vue'
export const router = createRouter({
history: createWebHashHistory(),
routes: [
{ path: '/login', component: LoginView },
{ path: '/', component: TaskListView },
{ path: '/tasks/:id', component: TaskDetailView },
{ path: '/income', component: IncomeSummaryView }
]
})
router.beforeEach((to) => {
const loggedIn = Boolean(localStorage.getItem('rider_token'))
if (to.path !== '/login' && !loggedIn) {
return '/login'
}
if (to.path === '/login' && loggedIn) {
return '/'
}
return true
})

323
rider-h5/src/styles.css Normal file
View File

@@ -0,0 +1,323 @@
* {
box-sizing: border-box;
}
html,
body,
#app {
min-height: 100%;
}
body {
margin: 0;
color: #182026;
background: #eef2f5;
font-family: Inter, "Microsoft YaHei", "PingFang SC", Arial, sans-serif;
}
button,
input,
select,
textarea {
font: inherit;
}
.mobile-page {
min-height: 100vh;
max-width: 520px;
margin: 0 auto;
background: #f7f9fb;
}
.page-header {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
gap: 12px;
min-height: 56px;
padding: 12px 16px;
color: #fff;
background: #176b5d;
}
.page-title {
flex: 1;
margin: 0;
font-size: 18px;
font-weight: 700;
}
.page-subtitle {
margin: 2px 0 0;
color: rgba(255, 255, 255, 0.78);
font-size: 12px;
}
.content {
padding: 14px;
}
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: #e8eef2;
}
.login-panel {
width: 100%;
max-width: 380px;
padding: 26px 22px;
border: 1px solid #dbe3e9;
border-radius: 8px;
background: #fff;
}
.login-title {
margin: 0 0 6px;
font-size: 24px;
}
.login-subtitle {
margin: 0 0 22px;
color: #64717d;
}
.field {
display: block;
margin-bottom: 14px;
}
.field-label {
display: block;
margin-bottom: 7px;
color: #586774;
font-size: 13px;
}
.input,
.textarea,
.select {
width: 100%;
min-height: 44px;
padding: 10px 12px;
border: 1px solid #ced8df;
border-radius: 6px;
color: #182026;
background: #fff;
outline: none;
}
.textarea {
min-height: 88px;
resize: vertical;
}
.input:focus,
.textarea:focus,
.select:focus {
border-color: #176b5d;
box-shadow: 0 0 0 3px rgba(23, 107, 93, 0.12);
}
.button {
min-height: 42px;
padding: 0 14px;
border: 1px solid transparent;
border-radius: 6px;
font-weight: 700;
cursor: pointer;
}
.button:disabled {
cursor: not-allowed;
opacity: 0.58;
}
.button.primary {
color: #fff;
background: #176b5d;
}
.button.secondary {
color: #176b5d;
border-color: #b8cec8;
background: #fff;
}
.button.danger {
color: #fff;
background: #b43b38;
}
.button.text {
min-height: 36px;
padding: 0 8px;
color: #fff;
background: transparent;
}
.button.full {
width: 100%;
}
.tab-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
padding: 12px 14px 0;
}
.tab {
min-height: 38px;
border: 1px solid #d6e0e6;
border-radius: 6px;
color: #4c5d68;
background: #fff;
}
.tab.active {
color: #176b5d;
border-color: #176b5d;
background: #e8f3f0;
font-weight: 700;
}
.task-card,
.panel,
.stat-card {
border: 1px solid #dfe7ed;
border-radius: 8px;
background: #fff;
}
.task-card {
display: block;
width: 100%;
margin-bottom: 10px;
padding: 14px;
text-align: left;
cursor: pointer;
}
.task-card:active {
transform: translateY(1px);
}
.panel {
padding: 14px;
margin-bottom: 12px;
}
.row {
display: flex;
align-items: center;
gap: 10px;
}
.between {
justify-content: space-between;
}
.stack {
display: grid;
gap: 10px;
}
.actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.muted {
color: #687985;
font-size: 13px;
}
.strong {
font-weight: 700;
}
.price {
color: #b45118;
font-weight: 800;
}
.tag {
display: inline-flex;
align-items: center;
min-height: 26px;
padding: 0 9px;
border-radius: 999px;
color: #176b5d;
background: #e8f3f0;
font-size: 12px;
font-weight: 700;
}
.tag.danger {
color: #9c2d2a;
background: #fdecea;
}
.empty {
padding: 36px 12px;
text-align: center;
color: #7a8a95;
}
.detail-line {
display: grid;
grid-template-columns: 74px 1fr;
gap: 10px;
padding: 9px 0;
border-bottom: 1px solid #edf1f4;
}
.detail-line:last-child {
border-bottom: none;
}
.photo-preview {
width: 100%;
max-height: 260px;
object-fit: cover;
border-radius: 8px;
border: 1px solid #dfe7ed;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.stat-card {
padding: 14px;
}
.stat-value {
margin-top: 8px;
font-size: 28px;
font-weight: 800;
}
.error {
margin: 0 0 12px;
padding: 10px 12px;
border-radius: 6px;
color: #9c2d2a;
background: #fdecea;
font-size: 13px;
}
@media (max-width: 360px) {
.actions {
grid-template-columns: 1fr;
}
}

52
rider-h5/src/types.ts Normal file
View File

@@ -0,0 +1,52 @@
export interface CurrentUser {
token: string
userId: number
tenantId: number
communityId: number
roleCode: string
}
export type DeliveryTaskStatus =
| 'ASSIGNED'
| 'ACCEPTED'
| 'DELIVERING'
| 'DELIVERED'
| 'COMPLETED'
| 'EXCEPTION'
| 'CANCELED'
| string
export interface DeliveryTask {
id: number
bizType: string
bizOrderId: number
bizOrderNo: string
riderId: number
riderName: string
contactName?: string
contactPhone?: string
address?: string
amountCent: number
remark?: string
status: DeliveryTaskStatus
deliveredPhotoUrl?: string
exceptionType?: string
exceptionNote?: string
createdAt?: string
updatedAt?: string
}
export interface RiderIncomeSummary {
assignedCount: number
deliveringCount: number
deliveredCount: number
estimatedIncomeCent: number
}
export interface FileUploadResult {
originalFilename: string
contentType: string
size: number
objectKey: string
url: string
}

View File

@@ -0,0 +1,30 @@
export function yuan(cent?: number) {
return ((cent || 0) / 100).toFixed(2)
}
export function statusLabel(status?: string) {
const labels: Record<string, string> = {
ASSIGNED: '待配送',
ACCEPTED: '待配送',
DELIVERING: '配送中',
DELIVERED: '已送达',
COMPLETED: '已完成',
EXCEPTION: '异常',
CANCELED: '已取消'
}
return status ? labels[status] || status : '-'
}
export function bizTypeLabel(type?: string) {
const labels: Record<string, string> = {
GOODS_ORDER: '商品订单',
EXPRESS_ORDER: '快递代取',
GROUP_BUY_ORDER: '团购订单'
}
return type ? labels[type] || type : '-'
}
export function shortTime(value?: string) {
if (!value) return '-'
return value.replace('T', ' ').slice(5, 16)
}

View File

@@ -0,0 +1,68 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { getIncomeSummary } from '../api/riderTaskApi'
import type { RiderIncomeSummary } from '../types'
import { yuan } from '../utils/format'
const router = useRouter()
const summary = ref<RiderIncomeSummary | null>(null)
const loading = ref(false)
const errorMessage = ref('')
async function load() {
loading.value = true
errorMessage.value = ''
try {
summary.value = await getIncomeSummary()
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '收入加载失败'
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<main class="mobile-page">
<header class="page-header">
<button class="button text" type="button" @click="router.back()">返回</button>
<div>
<h1 class="page-title">收入统计</h1>
<p class="page-subtitle">按已送达任务预估</p>
</div>
</header>
<section class="content">
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
<div v-if="loading" class="panel muted">加载中</div>
<template v-if="summary">
<section class="panel">
<div class="muted">预计收入</div>
<div class="stat-value price">¥{{ yuan(summary.estimatedIncomeCent) }}</div>
</section>
<section class="stat-grid">
<div class="stat-card">
<div class="muted">待配送</div>
<div class="stat-value">{{ summary.assignedCount }}</div>
</div>
<div class="stat-card">
<div class="muted">配送中</div>
<div class="stat-value">{{ summary.deliveringCount }}</div>
</div>
<div class="stat-card">
<div class="muted">已送达</div>
<div class="stat-value">{{ summary.deliveredCount }}</div>
</div>
<div class="stat-card">
<div class="muted">单均补贴</div>
<div class="stat-value">3</div>
</div>
</section>
</template>
</section>
</main>
</template>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { TOKEN_KEY, USER_KEY } from '../api/http'
import { riderLogin } from '../api/riderTaskApi'
const router = useRouter()
const loading = ref(false)
const errorMessage = ref('')
const form = reactive({
username: 'rider',
password: 'rider123'
})
async function submit() {
loading.value = true
errorMessage.value = ''
try {
const user = await riderLogin(form.username, form.password)
localStorage.setItem(TOKEN_KEY, user.token)
localStorage.setItem(USER_KEY, JSON.stringify(user))
router.replace('/')
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '登录失败'
} finally {
loading.value = false
}
}
</script>
<template>
<main class="login-page">
<section class="login-panel">
<h1 class="login-title">邻小帮配送员</h1>
<p class="login-subtitle">查看任务更新配送状态上传送达照片</p>
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
<form @submit.prevent="submit">
<label class="field">
<span class="field-label">账号</span>
<input v-model="form.username" class="input" autocomplete="username" />
</label>
<label class="field">
<span class="field-label">密码</span>
<input v-model="form.password" class="input" type="password" autocomplete="current-password" />
</label>
<button class="button primary full" :disabled="loading" type="submit">
{{ loading ? '登录中' : '登录' }}
</button>
</form>
</section>
</main>
</template>

View File

@@ -0,0 +1,196 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import {
deliverRiderTask,
getRiderTask,
markRiderTaskException,
startRiderTask,
uploadDeliveredPhoto
} from '../api/riderTaskApi'
import type { DeliveryTask } from '../types'
import { bizTypeLabel, shortTime, statusLabel, yuan } from '../utils/format'
const route = useRoute()
const router = useRouter()
const taskId = Number(route.params.id)
const task = ref<DeliveryTask | null>(null)
const loading = ref(false)
const actionLoading = ref(false)
const uploading = ref(false)
const errorMessage = ref('')
const photoUrl = ref('')
const exceptionType = ref('CONTACT_FAILED')
const exceptionNote = ref('')
const canStart = computed(() => task.value?.status === 'ASSIGNED' || task.value?.status === 'ACCEPTED')
const canDeliver = computed(() => task.value?.status === 'DELIVERING')
const canMarkException = computed(() => !['DELIVERED', 'COMPLETED', 'CANCELED'].includes(task.value?.status || ''))
async function load() {
loading.value = true
errorMessage.value = ''
try {
task.value = await getRiderTask(taskId)
photoUrl.value = task.value.deliveredPhotoUrl || ''
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '任务加载失败'
} finally {
loading.value = false
}
}
async function startTask() {
await runAction(async () => {
task.value = await startRiderTask(taskId)
})
}
async function deliverTask() {
if (!photoUrl.value) {
errorMessage.value = '请先上传送达照片'
return
}
await runAction(async () => {
task.value = await deliverRiderTask(taskId, photoUrl.value)
})
}
async function markException() {
if (!exceptionNote.value.trim()) {
errorMessage.value = '请填写异常说明'
return
}
await runAction(async () => {
task.value = await markRiderTaskException(taskId, exceptionType.value, exceptionNote.value.trim())
})
}
async function runAction(action: () => Promise<void>) {
actionLoading.value = true
errorMessage.value = ''
try {
await action()
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '操作失败'
} finally {
actionLoading.value = false
}
}
async function handlePhotoChange(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
uploading.value = true
errorMessage.value = ''
try {
const result = await uploadDeliveredPhoto(file)
photoUrl.value = result.url
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '上传失败'
} finally {
uploading.value = false
}
}
function callUser() {
if (task.value?.contactPhone) {
window.location.href = `tel:${task.value.contactPhone}`
}
}
onMounted(load)
</script>
<template>
<main class="mobile-page">
<header class="page-header">
<button class="button text" type="button" @click="router.back()">返回</button>
<div>
<h1 class="page-title">任务详情</h1>
<p class="page-subtitle">{{ task ? bizTypeLabel(task.bizType) : '配送履约' }}</p>
</div>
</header>
<section class="content">
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
<div v-if="loading" class="panel muted">加载中</div>
<template v-if="task">
<section class="panel">
<div class="row between">
<div>
<div class="strong">{{ bizTypeLabel(task.bizType) }}</div>
<div class="muted">{{ task.bizOrderNo }}</div>
</div>
<span class="tag" :class="{ danger: task.status === 'EXCEPTION' }">{{ statusLabel(task.status) }}</span>
</div>
</section>
<section class="panel">
<div class="detail-line">
<span class="muted">联系人</span>
<span>{{ task.contactName || '居民' }}</span>
</div>
<div class="detail-line">
<span class="muted">手机号</span>
<span>{{ task.contactPhone || '-' }}</span>
</div>
<div class="detail-line">
<span class="muted">地址</span>
<span>{{ task.address || '-' }}</span>
</div>
<div class="detail-line">
<span class="muted">金额</span>
<span class="price">¥{{ yuan(task.amountCent) }}</span>
</div>
<div class="detail-line">
<span class="muted">备注</span>
<span>{{ task.remark || '无' }}</span>
</div>
<div class="detail-line">
<span class="muted">创建</span>
<span>{{ shortTime(task.createdAt) }}</span>
</div>
</section>
<section v-if="canStart || canDeliver" class="panel stack">
<label v-if="canDeliver" class="field">
<span class="field-label">送达照片</span>
<input class="input" type="file" accept="image/*" :disabled="uploading" @change="handlePhotoChange" />
</label>
<img v-if="photoUrl" class="photo-preview" :src="photoUrl" alt="送达照片" />
<div class="actions">
<button v-if="canStart" class="button primary" :disabled="actionLoading" type="button" @click="startTask">
开始配送
</button>
<button v-if="canDeliver" class="button primary" :disabled="actionLoading || uploading" type="button" @click="deliverTask">
{{ uploading ? '上传中' : '标记送达' }}
</button>
<button class="button secondary" type="button" @click="callUser">拨打电话</button>
</div>
</section>
<section v-if="canMarkException" class="panel stack">
<label class="field">
<span class="field-label">异常类型</span>
<select v-model="exceptionType" class="select">
<option value="CONTACT_FAILED">联系不上</option>
<option value="ADDRESS_ERROR">地址错误</option>
<option value="PICKUP_FAILED">取件失败</option>
<option value="OTHER">其他</option>
</select>
</label>
<label class="field">
<span class="field-label">异常说明</span>
<textarea v-model="exceptionNote" class="textarea" placeholder="说明现场情况,便于后台处理" />
</label>
<button class="button danger full" :disabled="actionLoading" type="button" @click="markException">
标记异常
</button>
</section>
</template>
</section>
</main>
</template>

View File

@@ -0,0 +1,102 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { TOKEN_KEY, USER_KEY } from '../api/http'
import { groupTasksByStatus, listRiderTasks } from '../api/riderTaskApi'
import type { DeliveryTask } from '../types'
import { bizTypeLabel, shortTime, statusLabel, yuan } from '../utils/format'
type TabKey = 'assigned' | 'delivering' | 'completed' | 'exception'
const router = useRouter()
const tasks = ref<DeliveryTask[]>([])
const activeTab = ref<TabKey>('assigned')
const loading = ref(false)
const errorMessage = ref('')
const grouped = computed(() => groupTasksByStatus(tasks.value))
const tabs = computed(() => [
{ key: 'assigned' as const, label: '待配送', count: grouped.value.assigned.length },
{ key: 'delivering' as const, label: '配送中', count: grouped.value.delivering.length },
{ key: 'completed' as const, label: '已完成', count: grouped.value.completed.length },
{ key: 'exception' as const, label: '异常', count: grouped.value.exception.length }
])
const visibleTasks = computed(() => grouped.value[activeTab.value])
async function load() {
loading.value = true
errorMessage.value = ''
try {
tasks.value = await listRiderTasks()
} catch (error) {
errorMessage.value = error instanceof Error ? error.message : '任务加载失败'
} finally {
loading.value = false
}
}
function logout() {
localStorage.removeItem(TOKEN_KEY)
localStorage.removeItem(USER_KEY)
router.replace('/login')
}
onMounted(load)
</script>
<template>
<main class="mobile-page">
<header class="page-header">
<div>
<h1 class="page-title">今日任务</h1>
<p class="page-subtitle">按状态处理配送履约</p>
</div>
<button class="button text" type="button" @click="router.push('/income')">收入</button>
<button class="button text" type="button" @click="logout">退出</button>
</header>
<nav class="tab-row">
<button
v-for="tab in tabs"
:key="tab.key"
class="tab"
:class="{ active: activeTab === tab.key }"
type="button"
@click="activeTab = tab.key"
>
{{ tab.label }} {{ tab.count }}
</button>
</nav>
<section class="content">
<p v-if="errorMessage" class="error">{{ errorMessage }}</p>
<div v-if="loading" class="panel muted">加载中</div>
<button
v-for="task in visibleTasks"
:key="task.id"
class="task-card"
type="button"
@click="router.push(`/tasks/${task.id}`)"
>
<div class="row between">
<div>
<div class="strong">{{ bizTypeLabel(task.bizType) }} {{ task.bizOrderNo }}</div>
<div class="muted">{{ shortTime(task.createdAt) }}</div>
</div>
<span class="tag" :class="{ danger: task.status === 'EXCEPTION' }">{{ statusLabel(task.status) }}</span>
</div>
<div class="stack" style="margin-top: 10px">
<div>{{ task.contactName || '居民' }} {{ task.contactPhone || '' }}</div>
<div class="muted">{{ task.address || '暂无地址' }}</div>
<div class="row between">
<span class="muted">{{ task.remark || '无备注' }}</span>
<span class="price">¥{{ yuan(task.amountCent) }}</span>
</div>
</div>
</button>
<div v-if="!loading && !visibleTasks.length" class="empty">当前没有{{ tabs.find((tab) => tab.key === activeTab)?.label }}任务</div>
</section>
</main>
</template>

17
rider-h5/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
"strict": true
},
"include": ["src/**/*.ts", "src/**/*.vue"]
}