<route lang="jsonc" type="page">
|
{
|
"layout": "default",
|
"style": {
|
"navigationBarTitleText": "学习任务",
|
"navigationStyle": "custom"
|
}
|
}
|
</route>
|
|
<script lang="ts" setup>
|
import type { Device } from '@/api/device'
|
import type { ParentTask } from '@/api/parent'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import { computed, onMounted, ref, watch } from 'vue'
|
import { useMessage } from 'wot-design-uni/components/wd-message-box'
|
import { getBindDevices } from '@/api/device'
|
import { deleteParentTask, getParentDeviceStatus, getParentTasks, parentTaskError, runParentTaskNow, stopParentTask, updateParentTask } from '@/api/parent'
|
import { t } from '@/i18n'
|
import { toast } from '@/utils/toast'
|
import { formatLesson, formatOnComplete, formatSchedule } from './scheduleText'
|
|
defineOptions({
|
name: 'ParentTaskList',
|
})
|
|
interface Props {
|
agentId?: string
|
macAddress?: string
|
embedded?: boolean
|
}
|
|
const props = withDefaults(defineProps<Props>(), {
|
agentId: '',
|
macAddress: '',
|
embedded: false,
|
})
|
|
const message = useMessage()
|
const pageAgentId = ref('')
|
const pageMac = ref('')
|
const devices = ref<Device[]>([])
|
const selectedMac = ref('')
|
const tasks = ref<ParentTask[]>([])
|
const loading = ref(false)
|
const busyId = ref('')
|
const deviceOnline = ref<boolean | null>(null)
|
const statusLoading = ref(false)
|
let statusSeq = 0
|
|
const currentAgentId = computed(() => props.agentId || pageAgentId.value)
|
const canDispatch = computed(() => deviceOnline.value === true)
|
|
const runningTasks = computed(() =>
|
tasks.value.filter(item => item.runMode === 'immediate' && (item.status === 'queued' || item.status === 'running')),
|
)
|
const scheduledTasks = computed(() => tasks.value.filter(item => item.runMode === 'scheduled'))
|
|
const deviceColumns = computed(() =>
|
devices.value.map(device => ({
|
value: device.macAddress,
|
label: device.alias || device.macAddress,
|
})),
|
)
|
|
const statusTitle = computed(() => {
|
if (!selectedMac.value)
|
return ''
|
if (statusLoading.value || deviceOnline.value === null)
|
return t('parentTask.checkingOnline')
|
return deviceOnline.value ? t('parentTask.online') : t('parentTask.offline')
|
})
|
|
const statusHint = computed(() => {
|
if (!selectedMac.value || statusLoading.value || deviceOnline.value === null)
|
return ''
|
return deviceOnline.value ? t('parentTask.onlineHint') : t('parentTask.offlineHint')
|
})
|
|
function goBack() {
|
uni.navigateBack()
|
}
|
|
function deviceLabel(mac: string) {
|
const device = devices.value.find(item => item.macAddress === mac)
|
return device?.alias || mac
|
}
|
|
async function loadDevices() {
|
if (!currentAgentId.value)
|
return
|
devices.value = (await getBindDevices(currentAgentId.value)) || []
|
const preferred = props.macAddress || pageMac.value
|
if (preferred && devices.value.some(item => item.macAddress === preferred)) {
|
selectedMac.value = preferred
|
}
|
else if (!selectedMac.value && devices.value.length > 0) {
|
selectedMac.value = devices.value[0].macAddress
|
}
|
else if (selectedMac.value && !devices.value.some(item => item.macAddress === selectedMac.value)) {
|
selectedMac.value = devices.value[0]?.macAddress || ''
|
}
|
}
|
|
async function loadTasks() {
|
if (!currentAgentId.value || !selectedMac.value) {
|
tasks.value = []
|
return
|
}
|
loading.value = true
|
try {
|
const result = await getParentTasks(selectedMac.value, currentAgentId.value)
|
tasks.value = result?.list || []
|
}
|
catch (error) {
|
console.error('load parent tasks failed', error)
|
tasks.value = []
|
}
|
finally {
|
loading.value = false
|
}
|
}
|
|
function selectedDeviceOnline(): boolean | null {
|
const device = devices.value.find(item => item.macAddress === selectedMac.value)
|
if (!device || typeof device.online !== 'boolean')
|
return null
|
return device.online
|
}
|
|
async function loadDeviceStatus() {
|
if (!currentAgentId.value || !selectedMac.value) {
|
deviceOnline.value = null
|
statusLoading.value = false
|
return
|
}
|
const fromList = selectedDeviceOnline()
|
if (fromList !== null)
|
deviceOnline.value = fromList
|
const seq = ++statusSeq
|
statusLoading.value = fromList === null
|
try {
|
const result = await getParentDeviceStatus(selectedMac.value, currentAgentId.value)
|
if (seq !== statusSeq)
|
return
|
deviceOnline.value = !!result?.online
|
}
|
catch (error) {
|
console.error('load device status failed', error)
|
if (seq !== statusSeq)
|
return
|
const fallback = selectedDeviceOnline()
|
if (fallback !== null)
|
deviceOnline.value = fallback
|
}
|
finally {
|
if (seq === statusSeq)
|
statusLoading.value = false
|
}
|
}
|
|
async function refresh() {
|
await loadDevices()
|
await Promise.all([loadTasks(), loadDeviceStatus()])
|
}
|
|
function goCreate() {
|
if (!currentAgentId.value || !selectedMac.value) {
|
toast.warning(t('parentTask.bindDeviceFirst'))
|
return
|
}
|
if (!canDispatch.value) {
|
toast.warning(t('parentTask.deviceOffline'))
|
return
|
}
|
uni.navigateTo({
|
url: `/pages/parent-task/edit?agentId=${encodeURIComponent(currentAgentId.value)}&macAddress=${encodeURIComponent(selectedMac.value)}`,
|
})
|
}
|
|
function goHistory() {
|
if (!currentAgentId.value || !selectedMac.value) {
|
toast.warning(t('parentTask.bindDeviceFirst'))
|
return
|
}
|
uni.navigateTo({
|
url: `/pages/parent-task/history?agentId=${encodeURIComponent(currentAgentId.value)}&macAddress=${encodeURIComponent(selectedMac.value)}`,
|
})
|
}
|
|
function goEdit(task: ParentTask) {
|
uni.navigateTo({
|
url: `/pages/parent-task/edit?agentId=${encodeURIComponent(task.agentId)}&macAddress=${encodeURIComponent(task.macAddress)}&taskId=${encodeURIComponent(task.id)}`,
|
})
|
}
|
|
async function toggleEnabled(task: ParentTask) {
|
if (task.runMode !== 'scheduled' || busyId.value)
|
return
|
if (!canDispatch.value) {
|
toast.warning(t('parentTask.deviceOffline'))
|
return
|
}
|
busyId.value = task.id
|
try {
|
await updateParentTask(task.id, {
|
macAddress: task.macAddress,
|
agentId: task.agentId,
|
runMode: task.runMode,
|
scene: task.scene,
|
params: task.params,
|
schedule: task.schedule,
|
enabled: !task.enabled,
|
})
|
await loadTasks()
|
}
|
catch (error: any) {
|
toast.error(parentTaskError(error, t('parentTask.saveFailed')))
|
}
|
finally {
|
busyId.value = ''
|
}
|
}
|
|
async function runNow(task: ParentTask) {
|
if (busyId.value)
|
return
|
if (!canDispatch.value) {
|
toast.warning(t('parentTask.deviceOffline'))
|
return
|
}
|
busyId.value = task.id
|
try {
|
await runParentTaskNow(task.id)
|
toast.success(t('parentTask.started'))
|
await loadTasks()
|
}
|
catch (error: any) {
|
toast.error(parentTaskError(error, t('parentTask.startFailed')))
|
}
|
finally {
|
busyId.value = ''
|
}
|
}
|
|
function confirmStop(task: ParentTask) {
|
message.confirm({
|
title: t('parentTask.stopTitle'),
|
msg: t('parentTask.stopConfirm'),
|
confirmButtonText: t('parentTask.stop'),
|
cancelButtonText: t('common.cancel'),
|
}).then(async () => {
|
if (busyId.value)
|
return
|
busyId.value = task.id
|
try {
|
await stopParentTask(task.id)
|
toast.success(t('parentTask.stopped'))
|
await loadTasks()
|
}
|
catch (error: any) {
|
toast.error(parentTaskError(error, t('parentTask.stopFailed')))
|
}
|
finally {
|
busyId.value = ''
|
}
|
}).catch(() => {})
|
}
|
|
function confirmDelete(task: ParentTask) {
|
message.confirm({
|
title: t('parentTask.deleteTitle'),
|
msg: t('parentTask.deleteConfirm'),
|
confirmButtonText: t('common.delete'),
|
cancelButtonText: t('common.cancel'),
|
}).then(async () => {
|
busyId.value = task.id
|
try {
|
await deleteParentTask(task.id)
|
toast.success(t('parentTask.deleted'))
|
await loadTasks()
|
}
|
catch (error: any) {
|
toast.error(parentTaskError(error, t('parentTask.deleteFailed')))
|
}
|
finally {
|
busyId.value = ''
|
}
|
}).catch(() => {})
|
}
|
|
function taskSubtitle(task: ParentTask) {
|
const bits = [
|
formatLesson(task.params),
|
task.runMode === 'scheduled' ? formatSchedule(task.schedule, t) : t('parentTask.immediate'),
|
formatOnComplete(task.params, t),
|
].filter(Boolean)
|
return bits.join(' · ')
|
}
|
|
onLoad((options) => {
|
if (options?.agentId)
|
pageAgentId.value = String(options.agentId)
|
if (options?.macAddress)
|
pageMac.value = decodeURIComponent(String(options.macAddress))
|
})
|
|
onShow(() => {
|
refresh()
|
})
|
|
onMounted(() => {
|
refresh()
|
})
|
|
watch(() => selectedMac.value, () => {
|
loadTasks()
|
loadDeviceStatus()
|
})
|
|
defineExpose({
|
refresh,
|
})
|
</script>
|
|
<template>
|
<view class="parent-task-page" :class="{ embedded }">
|
<wd-navbar v-if="!embedded" :title="t('parentTask.pageTitle')" safe-area-inset-top>
|
<template #left>
|
<wd-icon name="arrow-left" size="18" @click="goBack" />
|
</template>
|
<template #right>
|
<text class="px-[8rpx] text-[28rpx] text-[#336cff]" @click="goHistory">
|
{{ t('parentHistory.entry') }}
|
</text>
|
</template>
|
</wd-navbar>
|
|
<view class="px-[20rpx] py-[16rpx]">
|
<view v-if="devices.length > 0 && selectedMac" class="mb-[16rpx] rounded-[16rpx] bg-white p-[24rpx]">
|
<view class="mb-[12rpx] flex items-center justify-between">
|
<text class="text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.device') }}
|
</text>
|
<text v-if="embedded" class="text-[26rpx] text-[#336cff]" @click="goHistory">
|
{{ t('parentHistory.entry') }}
|
</text>
|
</view>
|
<wd-picker
|
v-if="devices.length > 1"
|
v-model="selectedMac"
|
:columns="deviceColumns"
|
:placeholder="t('parentTask.selectDevice')"
|
:cancel-button-text="t('common.cancel')"
|
:confirm-button-text="t('common.confirm')"
|
/>
|
<text v-else class="mb-[8rpx] block text-[32rpx] text-[#232338] font-semibold">
|
{{ deviceLabel(selectedMac) }}
|
</text>
|
<view
|
class="mt-[16rpx] flex items-start gap-[16rpx] rounded-[16rpx] p-[20rpx]"
|
:class="deviceOnline ? 'bg-[#e8f8ef]' : (statusLoading || deviceOnline === null ? 'bg-[#f5f7fb]' : 'bg-[#fff1f0]')"
|
>
|
<view
|
class="mt-[10rpx] h-[20rpx] w-[20rpx] shrink-0 rounded-full"
|
:class="deviceOnline ? 'bg-[#1aa34a]' : (statusLoading || deviceOnline === null ? 'bg-[#c0c4cc]' : 'bg-[#d4380d]')"
|
/>
|
<view class="min-w-0 flex-1">
|
<text
|
class="block text-[34rpx] font-bold leading-[1.3]"
|
:class="deviceOnline ? 'text-[#146c36]' : (statusLoading || deviceOnline === null ? 'text-[#65686f]' : 'text-[#d4380d]')"
|
>
|
{{ statusTitle }}
|
</text>
|
<text
|
v-if="statusHint"
|
class="mt-[6rpx] block text-[24rpx] leading-[1.45]"
|
:class="deviceOnline ? 'text-[#2f6f45]' : 'text-[#a8071a]'"
|
>
|
{{ statusHint }}
|
</text>
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view v-if="loading && tasks.length === 0" class="flex flex-col items-center py-[80rpx]">
|
<wd-loading color="#336cff" />
|
<text class="mt-[16rpx] text-[28rpx] text-[#666]">
|
{{ t('common.loading') }}
|
</text>
|
</view>
|
|
<view v-else-if="devices.length === 0" class="px-[40rpx] py-[80rpx] text-center">
|
<text class="text-[30rpx] text-[#666]">
|
{{ t('parentTask.bindDeviceFirst') }}
|
</text>
|
</view>
|
|
<view v-else class="px-[20rpx] pb-[160rpx]">
|
<view v-if="runningTasks.length > 0" class="mb-[24rpx]">
|
<text class="mb-[12rpx] block px-[8rpx] text-[28rpx] text-[#232338] font-semibold">
|
{{ t('parentTask.runningSection') }}
|
</text>
|
<view
|
v-for="task in runningTasks"
|
:key="task.id"
|
class="mb-[16rpx] rounded-[20rpx] border-[1rpx] border-[#d6e4ff] bg-white p-[28rpx]"
|
>
|
<view class="mb-[8rpx] flex items-center justify-between">
|
<text class="text-[32rpx] text-[#232338] font-semibold">
|
{{ task.sceneName || task.scene }}
|
</text>
|
<text class="rounded-[8rpx] bg-[#336cff] px-[12rpx] py-[4rpx] text-[22rpx] text-white">
|
{{ t('parentTask.running') }}
|
</text>
|
</view>
|
<text class="mb-[16rpx] block text-[26rpx] text-[#65686f]">
|
{{ taskSubtitle(task) }}
|
</text>
|
<wd-button size="small" type="error" plain :loading="busyId === task.id" @click="confirmStop(task)">
|
{{ t('parentTask.stop') }}
|
</wd-button>
|
</view>
|
</view>
|
|
<text class="mb-[12rpx] block px-[8rpx] text-[28rpx] text-[#232338] font-semibold">
|
{{ t('parentTask.scheduledSection') }}
|
</text>
|
<view v-if="scheduledTasks.length === 0" class="rounded-[20rpx] bg-white p-[48rpx] text-center text-[26rpx] text-[#999]">
|
{{ t('parentTask.emptySchedule') }}
|
</view>
|
<view
|
v-for="task in scheduledTasks"
|
:key="task.id"
|
class="mb-[16rpx] rounded-[20rpx] border-[1rpx] border-[#eeeeee] bg-white p-[28rpx]"
|
@click="goEdit(task)"
|
>
|
<view class="mb-[8rpx] flex items-center justify-between">
|
<text class="text-[32rpx] text-[#232338] font-semibold">
|
{{ task.sceneName || task.scene }}
|
</text>
|
<wd-switch
|
:model-value="task.enabled"
|
size="22"
|
:disabled="busyId === task.id || !canDispatch"
|
@click.stop
|
@change="toggleEnabled(task)"
|
/>
|
</view>
|
<text class="mb-[16rpx] block text-[26rpx] text-[#65686f]">
|
{{ taskSubtitle(task) }}
|
</text>
|
<view class="flex gap-[16rpx]" @click.stop>
|
<wd-button size="small" type="primary" plain :disabled="!canDispatch" :loading="busyId === task.id" @click="runNow(task)">
|
{{ t('parentTask.startNow') }}
|
</wd-button>
|
<wd-button size="small" type="error" plain :loading="busyId === task.id" @click="confirmDelete(task)">
|
{{ t('common.delete') }}
|
</wd-button>
|
</view>
|
</view>
|
</view>
|
|
<wd-fab
|
v-if="devices.length > 0"
|
type="primary"
|
size="small"
|
icon="add"
|
:disabled="!canDispatch"
|
:draggable="true"
|
:expandable="false"
|
@click="goCreate"
|
/>
|
<wd-message-box />
|
</view>
|
</template>
|
|
<style scoped>
|
.parent-task-page {
|
min-height: 100%;
|
background: #f5f7fb;
|
}
|
|
.parent-task-page.embedded {
|
min-height: auto;
|
}
|
</style>
|