<route lang="jsonc" type="page">
|
{
|
"layout": "default",
|
"style": {
|
"navigationBarTitleText": "创建学习任务",
|
"navigationStyle": "custom"
|
}
|
}
|
</route>
|
|
<script lang="ts" setup>
|
import type {
|
IdiomCatalog,
|
IdiomEnPass,
|
IdiomNamed,
|
IdiomStory,
|
ParentLanguagePair,
|
ParentScene,
|
ParentTask,
|
ParentTaskParams,
|
ParentSchedule,
|
} from '@/api/parent'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import { computed, ref, watch } from 'vue'
|
import {
|
createParentTask,
|
getParentDeviceStatus,
|
getParentIdiomCatalog,
|
getParentLanguagePairs,
|
getParentScenes,
|
getParentTask,
|
parentTaskError,
|
runParentTaskNow,
|
updateParentTask,
|
} from '@/api/parent'
|
import { t } from '@/i18n'
|
import { toast } from '@/utils/toast'
|
import { normalizeTime, pad2 } from './scheduleText'
|
|
defineOptions({
|
name: 'ParentTaskEdit',
|
})
|
|
const FALLBACK_SCENES: ParentScene[] = [
|
{ id: 'extensive', name: '泛听', hint: '按课文循环播放,不对话。', needsLesson: true, needsEnPasses: true, needsShadowingSettings: false, needsLanguagePair: false, needsDuration: false, needsOnComplete: true, appId: 'english_learning', taskId: 'repeater' },
|
{ id: 'shadowing', name: '跟读', hint: '播一句、跟读一句。', needsLesson: true, needsEnPasses: false, needsShadowingSettings: true, needsLanguagePair: false, needsDuration: false, needsOnComplete: true, appId: 'english_learning', taskId: 'shadowing' },
|
{ id: 'chat', name: '默认聊天', hint: '回到日常对话。', needsLesson: false, needsEnPasses: false, needsShadowingSettings: false, needsLanguagePair: false, needsDuration: true, needsOnComplete: true, appId: 'chat', taskId: 'conversation' },
|
{ id: 'translation', name: '翻译', hint: '立即或到点进入中英互译,无需口头切换。', needsLesson: false, needsEnPasses: false, needsShadowingSettings: false, needsLanguagePair: true, needsDuration: true, needsOnComplete: true, appId: 'english_translation', taskId: 'live_translate' },
|
{ id: 'bilingual', name: '中英对话', hint: '立即或到点进入中英对话,无需口头切换。', needsLesson: false, needsEnPasses: false, needsShadowingSettings: false, needsLanguagePair: true, needsDuration: true, needsOnComplete: true, appId: 'english_conversation', taskId: 'bilingual_chat' },
|
]
|
|
const FALLBACK_LEVELS: IdiomNamed[] = [
|
{ id: 'l1', name: '一级' },
|
{ id: 'l2', name: '二级' },
|
{ id: 'l3', name: '三级' },
|
{ id: 'l4', name: '四级' },
|
{ id: 'l5', name: '五级' },
|
{ id: 'l6', name: '六级' },
|
]
|
const FALLBACK_SYSTEMS: IdiomNamed[] = [
|
{ id: 'origin', name: '起源与存在理解', nameEn: 'Understanding Origins and Existence' },
|
{ id: 'growth', name: '学习与成长机制', nameEn: 'Learning & Growth Mechanisms' },
|
{ id: 'language', name: '语言与意义建构', nameEn: 'Construction of Language & Meaning' },
|
{ id: 'cognition', name: '认知与思维偏差', nameEn: 'Cognitive & Thinking Biases' },
|
{ id: 'values', name: '价值与行为选择', nameEn: 'Values & Behavioral Choices' },
|
{ id: 'society', name: '社会互动与结构关系', nameEn: 'Social Interaction & Structural Relationships' },
|
{ id: 'destiny', name: '命运与自我超越', nameEn: 'Destiny & Self-Transcendence' },
|
]
|
const DEFAULT_EN_PASSES: IdiomEnPass[] = [
|
{ speed: 0.8, repeat: 1 },
|
{ speed: 0.9, repeat: 2 },
|
{ speed: 1.0, repeat: 5 },
|
]
|
const SPEED_OPTIONS = [0.7, 0.8, 0.9, 1.0, 1.1, 1.2]
|
|
const WEEKDAYS = [
|
{ value: 1, key: 'mon' },
|
{ value: 2, key: 'tue' },
|
{ value: 3, key: 'wed' },
|
{ value: 4, key: 'thu' },
|
{ value: 5, key: 'fri' },
|
{ value: 6, key: 'sat' },
|
{ value: 7, key: 'sun' },
|
]
|
const SCHEDULE_TYPES: { id: 'daily' | 'hourly' | 'interval', key: string }[] = [
|
{ id: 'daily', key: 'parentTask.schedule.daily' },
|
{ id: 'hourly', key: 'parentTask.schedule.hourly' },
|
{ id: 'interval', key: 'parentTask.schedule.interval' },
|
]
|
|
|
const agentId = ref('')
|
const macAddress = ref('')
|
const taskId = ref('')
|
const loading = ref(false)
|
const submitting = ref('')
|
const deviceOnline = ref<boolean | null>(null)
|
const statusLoading = ref(false)
|
let statusSeq = 0
|
const scenes = ref<ParentScene[]>(FALLBACK_SCENES)
|
const languagePairs = ref<ParentLanguagePair[]>([])
|
const levels = ref<IdiomNamed[]>(FALLBACK_LEVELS)
|
const systems = ref<IdiomNamed[]>(FALLBACK_SYSTEMS)
|
const stories = ref<IdiomStory[]>([{
|
id: '001',
|
slug: 'pangu',
|
systemId: 'origin',
|
name: '开天辟地',
|
nameEn: 'Pangu splits the chaos to create the world',
|
sentenceCount: 58,
|
}])
|
|
const sceneId = ref('shadowing')
|
const runMode = ref<'immediate' | 'scheduled'>('immediate')
|
const levelId = ref('l1')
|
const systemId = ref('origin')
|
const lessonId = ref('l1.001')
|
const enPasses = ref<IdiomEnPass[]>(DEFAULT_EN_PASSES.map(item => ({ ...item })))
|
const shadowingLanguage = ref<'cn' | 'en'>('en')
|
const shadowingSpeed = ref(0.8)
|
const languagePair = ref('zh-en')
|
const durationMinutes = ref(0)
|
const onComplete = ref<'chat' | 'power_off'>('chat')
|
const scheduleType = ref<'daily' | 'hourly' | 'interval'>('daily')
|
const dailyHour = ref(7)
|
const dailyMinute = ref(30)
|
const hourlyMinute = ref(0)
|
const intervalMinutes = ref(60)
|
const weekdays = ref<number[]>([1, 2, 3, 4, 5, 6, 7])
|
|
const isEdit = computed(() => !!taskId.value)
|
const currentScene = computed(() => scenes.value.find(item => item.id === sceneId.value) || FALLBACK_SCENES[1])
|
const pageTitle = computed(() => isEdit.value ? t('parentTask.editTitle') : t('parentTask.createTitle'))
|
const canDispatch = computed(() => deviceOnline.value === true)
|
const statusTitle = computed(() => {
|
if (statusLoading.value || deviceOnline.value === null)
|
return t('parentTask.checkingOnline')
|
return deviceOnline.value ? t('parentTask.online') : t('parentTask.offline')
|
})
|
const statusHint = computed(() => {
|
if (statusLoading.value || deviceOnline.value === null)
|
return ''
|
return deviceOnline.value ? t('parentTask.onlineHint') : t('parentTask.offlineHint')
|
})
|
const pairColumns = computed(() => languagePairs.value.map(item => ({
|
value: item.id,
|
label: item.ready ? item.name : `${item.name}(${t('parentTask.pairSoon')})`,
|
})))
|
const selectedPair = computed(() => languagePairs.value.find(item => item.id === languagePair.value))
|
const filteredStories = computed(() => stories.value.filter(item => item.systemId === systemId.value))
|
const selectedStory = computed(() => {
|
const parts = lessonId.value.split('.')
|
const sid = parts.length >= 2 ? parts[parts.length - 1] : lessonId.value
|
return filteredStories.value.find(item => item.id === sid)
|
|| stories.value.find(item => item.id === sid)
|
})
|
|
function composeLessonId(story?: IdiomStory, level = levelId.value) {
|
if (!story)
|
return ''
|
return `${level}.${story.id}`
|
}
|
|
function goBack() {
|
uni.navigateBack()
|
}
|
|
async function loadDeviceStatus() {
|
if (!agentId.value || !macAddress.value) {
|
deviceOnline.value = null
|
statusLoading.value = false
|
return
|
}
|
const seq = ++statusSeq
|
statusLoading.value = true
|
try {
|
const result = await getParentDeviceStatus(macAddress.value, agentId.value)
|
if (seq !== statusSeq)
|
return
|
deviceOnline.value = !!result?.online
|
}
|
catch (error) {
|
console.error('load device status failed', error)
|
if (seq !== statusSeq)
|
return
|
}
|
finally {
|
if (seq === statusSeq)
|
statusLoading.value = false
|
}
|
}
|
|
function normalizeScene(raw: ParentScene): ParentScene {
|
const fallback = FALLBACK_SCENES.find(item => item.id === raw.id)
|
return {
|
...fallback,
|
...raw,
|
name: raw.name || fallback?.name || raw.id,
|
needsLesson: !!raw.needsLesson,
|
needsEnPasses: raw.needsEnPasses ?? fallback?.needsEnPasses ?? raw.id === 'extensive',
|
needsShadowingSettings: raw.needsShadowingSettings ?? fallback?.needsShadowingSettings ?? raw.id === 'shadowing',
|
needsLanguagePair: !!raw.needsLanguagePair,
|
needsDuration: !!raw.needsDuration,
|
needsOnComplete: raw.needsOnComplete !== false,
|
appId: raw.appId || fallback?.appId || '',
|
taskId: raw.taskId || fallback?.taskId || '',
|
}
|
}
|
|
function selectScene(id: string) {
|
sceneId.value = id
|
}
|
|
function selectLevel(id: string) {
|
levelId.value = id
|
const story = selectedStory.value || filteredStories.value[0]
|
if (story)
|
lessonId.value = composeLessonId(story, id)
|
}
|
|
function selectSystem(id: string) {
|
systemId.value = id
|
ensureStorySelection()
|
}
|
|
function selectStory(story: IdiomStory) {
|
lessonId.value = composeLessonId(story)
|
}
|
|
function ensureStorySelection() {
|
const current = selectedStory.value
|
if (current && current.systemId === systemId.value) {
|
lessonId.value = composeLessonId(current)
|
return
|
}
|
const firstInSystem = filteredStories.value[0]
|
if (firstInSystem) {
|
lessonId.value = composeLessonId(firstInSystem)
|
return
|
}
|
const first = stories.value[0]
|
if (first) {
|
systemId.value = first.systemId
|
lessonId.value = composeLessonId(first)
|
}
|
}
|
|
function parseLessonId(raw?: string) {
|
const value = String(raw || '').trim()
|
if (!value) {
|
return { levelId: 'l1', systemId: 'origin', lessonId: 'l1.001' }
|
}
|
const parts = value.split('.')
|
if (parts.length === 2 && /^l[1-6]$/i.test(parts[0]) && /^\d{3}$/.test(parts[1])) {
|
const matched = stories.value.find(item => item.id === parts[1])
|
return {
|
levelId: parts[0],
|
systemId: matched?.systemId || systemId.value,
|
lessonId: `${parts[0]}.${parts[1]}`,
|
}
|
}
|
return { levelId: levelId.value, systemId: systemId.value, lessonId: value }
|
}
|
|
function applyCatalog(catalog: IdiomCatalog) {
|
if (catalog.levels?.length)
|
levels.value = catalog.levels
|
if (catalog.systems?.length)
|
systems.value = catalog.systems
|
if (catalog.stories?.length)
|
stories.value = catalog.stories
|
if (catalog.defaults?.enPasses?.length && !taskId.value)
|
enPasses.value = catalog.defaults.enPasses.map(item => ({ speed: Number(item.speed), repeat: Number(item.repeat) }))
|
if (catalog.defaults?.shadowingSpeed && !taskId.value)
|
shadowingSpeed.value = Number(catalog.defaults.shadowingSpeed)
|
}
|
|
function normalizePasses(raw: unknown): IdiomEnPass[] {
|
if (!Array.isArray(raw) || raw.length === 0)
|
return DEFAULT_EN_PASSES.map(item => ({ ...item }))
|
return raw.slice(0, 8).map((item: any) => ({
|
speed: Math.min(2, Math.max(0.5, Number(item?.speed) || 1)),
|
repeat: Math.min(10, Math.max(1, Number(item?.repeat) || 1)),
|
}))
|
}
|
|
function addEnPass() {
|
if (enPasses.value.length >= 8)
|
return
|
enPasses.value = [...enPasses.value, { speed: 1.0, repeat: 1 }]
|
}
|
|
function removeEnPass(index: number) {
|
if (enPasses.value.length <= 1)
|
return
|
enPasses.value = enPasses.value.filter((_, i) => i !== index)
|
}
|
|
function setPassSpeed(index: number, speed: number) {
|
enPasses.value = enPasses.value.map((item, i) => i === index ? { ...item, speed } : item)
|
}
|
|
function toggleWeekday(day: number) {
|
if (weekdays.value.includes(day)) {
|
if (weekdays.value.length === 1)
|
return
|
weekdays.value = weekdays.value.filter(item => item !== day)
|
}
|
else {
|
weekdays.value = [...weekdays.value, day].sort()
|
}
|
}
|
|
function savedDailyTime(): string {
|
const hour = Math.min(23, Math.max(0, Number(dailyHour.value) || 0))
|
const minute = Math.min(59, Math.max(0, Number(dailyMinute.value) || 0))
|
return `${pad2(hour)}:${pad2(minute)}`
|
}
|
|
function applyDailyTime(raw?: string) {
|
const time = normalizeTime(raw)
|
const [hour, minute] = time.split(':')
|
dailyHour.value = Number(hour)
|
dailyMinute.value = Number(minute)
|
}
|
|
async function loadCatalog() {
|
try {
|
const [sceneList, pairList, catalog] = await Promise.all([
|
getParentScenes(),
|
getParentLanguagePairs(),
|
getParentIdiomCatalog(),
|
])
|
if (sceneList?.length)
|
scenes.value = sceneList.map(normalizeScene)
|
if (pairList?.length)
|
languagePairs.value = pairList
|
if (catalog?.stories?.length)
|
applyCatalog(catalog)
|
}
|
catch (error) {
|
console.error('load parent catalog failed', error)
|
languagePairs.value = [
|
{ id: 'zh-en', name: '中英', ready: true },
|
{ id: 'zh-fr', name: '中法', ready: false },
|
{ id: 'zh-ja', name: '中日', ready: false },
|
]
|
}
|
}
|
|
async function refreshScenes() {
|
try {
|
const sceneList = await getParentScenes()
|
if (sceneList?.length)
|
scenes.value = sceneList.map(normalizeScene)
|
}
|
catch (error) {
|
console.error('refresh parent scenes failed', error)
|
}
|
}
|
|
function applyTask(task: ParentTask) {
|
sceneId.value = task.scene || 'shadowing'
|
runMode.value = task.runMode === 'scheduled' ? 'scheduled' : 'immediate'
|
const parsed = parseLessonId(task.params?.lesson_id)
|
levelId.value = parsed.levelId
|
systemId.value = parsed.systemId
|
lessonId.value = parsed.lessonId
|
enPasses.value = normalizePasses(task.params?.en_passes)
|
shadowingLanguage.value = task.params?.language === 'cn' ? 'cn' : 'en'
|
shadowingSpeed.value = Number(task.params?.speed || 0.8)
|
languagePair.value = task.params?.language_pair || 'zh-en'
|
durationMinutes.value = Number(task.params?.duration_minutes || 0)
|
onComplete.value = task.params?.on_complete === 'power_off' ? 'power_off' : 'chat'
|
const schedule = task.schedule || {}
|
scheduleType.value = schedule.type || 'daily'
|
applyDailyTime(schedule.time)
|
hourlyMinute.value = Number(schedule.minute || 0)
|
intervalMinutes.value = Number(schedule.every_minutes || 60)
|
weekdays.value = (schedule.weekdays && schedule.weekdays.length > 0)
|
? [...schedule.weekdays]
|
: [1, 2, 3, 4, 5, 6, 7]
|
}
|
|
function buildParams(): ParentTaskParams {
|
const params: ParentTaskParams = {}
|
if (currentScene.value.needsLesson)
|
params.lesson_id = lessonId.value
|
if (sceneId.value === 'extensive')
|
params.en_passes = enPasses.value.map(item => ({ speed: item.speed, repeat: item.repeat }))
|
if (sceneId.value === 'shadowing') {
|
params.language = shadowingLanguage.value
|
params.speed = shadowingSpeed.value
|
}
|
if (currentScene.value.needsLanguagePair)
|
params.language_pair = languagePair.value || 'zh-en'
|
if (currentScene.value.needsDuration && Number(durationMinutes.value) > 0)
|
params.duration_minutes = Math.min(24 * 60, Math.max(1, Number(durationMinutes.value)))
|
if (currentScene.value.needsOnComplete)
|
params.on_complete = onComplete.value
|
return params
|
}
|
|
function buildSchedule(): ParentSchedule | undefined {
|
if (runMode.value !== 'scheduled')
|
return undefined
|
if (scheduleType.value === 'hourly') {
|
return { type: 'hourly', minute: Math.min(59, Math.max(0, Number(hourlyMinute.value) || 0)) }
|
}
|
if (scheduleType.value === 'interval') {
|
return { type: 'interval', every_minutes: Math.min(24 * 60, Math.max(1, Number(intervalMinutes.value) || 60)) }
|
}
|
const schedule: ParentSchedule = {
|
type: 'daily',
|
time: savedDailyTime(),
|
}
|
if (weekdays.value.length > 0 && weekdays.value.length < 7)
|
schedule.weekdays = [...weekdays.value]
|
return schedule
|
}
|
|
function validate(): boolean {
|
if (!macAddress.value || !agentId.value) {
|
toast.warning(t('parentTask.bindDeviceFirst'))
|
return false
|
}
|
if (!sceneId.value) {
|
toast.warning(t('parentTask.pickScene'))
|
return false
|
}
|
if (currentScene.value.needsLesson && !lessonId.value) {
|
toast.warning(t('parentTask.pickStory'))
|
return false
|
}
|
return true
|
}
|
|
async function submit(mode: 'immediate' | 'scheduled') {
|
if (submitting.value || !validate())
|
return
|
if (!canDispatch.value) {
|
toast.warning(t('parentTask.deviceOffline'))
|
return
|
}
|
runMode.value = mode
|
submitting.value = mode
|
const payload = {
|
macAddress: macAddress.value,
|
agentId: agentId.value,
|
runMode: mode,
|
scene: sceneId.value,
|
params: buildParams(),
|
schedule: mode === 'scheduled' ? buildSchedule() : undefined,
|
enabled: true,
|
}
|
try {
|
if (isEdit.value) {
|
await updateParentTask(taskId.value, payload)
|
toast.success(payload.schedule?.time ? `${t('parentTask.saved')} ${payload.schedule.time}` : t('parentTask.saved'))
|
}
|
else {
|
await createParentTask(payload)
|
toast.success(mode === 'immediate'
|
? t('parentTask.started')
|
: (payload.schedule?.time ? `${t('parentTask.saved')} ${payload.schedule.time}` : t('parentTask.saved')))
|
}
|
setTimeout(() => uni.navigateBack(), 400)
|
}
|
catch (error: any) {
|
toast.error(parentTaskError(error, t('parentTask.saveFailed')))
|
}
|
finally {
|
submitting.value = ''
|
}
|
}
|
|
async function startExisting() {
|
if (!taskId.value || submitting.value)
|
return
|
if (!canDispatch.value) {
|
toast.warning(t('parentTask.deviceOffline'))
|
return
|
}
|
submitting.value = 'run'
|
try {
|
await runParentTaskNow(taskId.value)
|
toast.success(t('parentTask.started'))
|
setTimeout(() => uni.navigateBack(), 400)
|
}
|
catch (error: any) {
|
toast.error(parentTaskError(error, t('parentTask.startFailed')))
|
}
|
finally {
|
submitting.value = ''
|
}
|
}
|
|
watch(sceneId, () => {
|
if (currentScene.value.needsOnComplete && !onComplete.value)
|
onComplete.value = 'chat'
|
})
|
|
onLoad(async (options) => {
|
agentId.value = String(options?.agentId || '')
|
macAddress.value = decodeURIComponent(String(options?.macAddress || ''))
|
taskId.value = String(options?.taskId || '')
|
loading.value = true
|
loadDeviceStatus()
|
try {
|
await loadCatalog()
|
if (taskId.value) {
|
const task = await getParentTask(taskId.value)
|
applyTask(task)
|
}
|
ensureStorySelection()
|
}
|
finally {
|
loading.value = false
|
}
|
})
|
|
onShow(() => {
|
if (agentId.value && macAddress.value)
|
loadDeviceStatus()
|
refreshScenes()
|
})
|
</script>
|
|
<template>
|
<view class="min-h-screen bg-[#f5f7fb]">
|
<wd-navbar :title="pageTitle" safe-area-inset-top>
|
<template #left>
|
<wd-icon name="arrow-left" size="18" @click="goBack" />
|
</template>
|
</wd-navbar>
|
|
<view v-if="loading" class="flex flex-col items-center py-[80rpx]">
|
<wd-loading color="#336cff" />
|
</view>
|
|
<view v-else class="box-border px-[24rpx] py-[20rpx] pb-[200rpx]">
|
<view
|
class="mb-[20rpx] flex items-start gap-[16rpx] rounded-[20rpx] p-[24rpx]"
|
:class="deviceOnline ? 'bg-[#e8f8ef]' : (statusLoading || deviceOnline === null ? 'bg-white' : '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 class="mb-[20rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-semibold">
|
{{ t('parentTask.scene') }}
|
</text>
|
<view class="flex flex-wrap gap-[16rpx]">
|
<view
|
v-for="scene in scenes"
|
:key="scene.id"
|
class="rounded-[32rpx] px-[28rpx] py-[12rpx] text-[26rpx]"
|
:class="sceneId === scene.id ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="selectScene(scene.id)"
|
>
|
{{ scene.name }}
|
</view>
|
</view>
|
<text class="mt-[16rpx] block text-[22rpx] text-[#8a8d96]">
|
{{ t('parentTask.dispatchHint') }}
|
</text>
|
<text v-if="currentScene.hint" class="mt-[8rpx] block text-[22rpx] text-[#8a8d96]">
|
{{ currentScene.hint }}
|
</text>
|
</view>
|
|
<view v-if="currentScene.needsLesson" class="mb-[20rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-semibold">
|
{{ t('parentTask.story') }}
|
</text>
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.level') }}
|
</text>
|
<view class="mb-[20rpx] flex flex-wrap gap-[12rpx]">
|
<view
|
v-for="level in levels"
|
:key="level.id"
|
class="rounded-[24rpx] px-[20rpx] py-[8rpx] text-[24rpx]"
|
:class="levelId === level.id ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="selectLevel(level.id)"
|
>
|
{{ level.name }}
|
</view>
|
</view>
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.system') }}
|
</text>
|
<view class="mb-[20rpx] flex flex-col gap-[12rpx]">
|
<view
|
v-for="item in systems"
|
:key="item.id"
|
class="rounded-[16rpx] px-[20rpx] py-[14rpx]"
|
:class="systemId === item.id ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="selectSystem(item.id)"
|
>
|
<text class="block text-[26rpx] font-medium">
|
{{ item.name }}
|
</text>
|
<text
|
class="mt-[4rpx] block text-[20rpx] leading-[1.4]"
|
:class="systemId === item.id ? 'text-white/80' : 'text-[#8a8d96]'"
|
>
|
{{ item.nameEn }}
|
</text>
|
</view>
|
</view>
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.pickStory') }}
|
</text>
|
<view v-if="filteredStories.length" class="flex flex-col gap-[12rpx]">
|
<view
|
v-for="story in filteredStories"
|
:key="story.id"
|
class="rounded-[16rpx] px-[20rpx] py-[16rpx]"
|
:class="selectedStory && selectedStory.id === story.id ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="selectStory(story)"
|
>
|
<text class="block text-[28rpx] font-medium">
|
{{ story.name }}
|
</text>
|
<text
|
class="mt-[6rpx] block text-[22rpx] leading-[1.4]"
|
:class="selectedStory && selectedStory.id === story.id ? 'text-white/85' : 'text-[#65686f]'"
|
>
|
{{ story.nameEn }}
|
</text>
|
</view>
|
</view>
|
<text v-else class="block text-[24rpx] text-[#fa8c16]">
|
{{ t('parentTask.noStories') }}
|
</text>
|
<text v-if="selectedStory" class="mt-[16rpx] block text-[22rpx] text-[#8a8d96]">
|
{{ t('parentTask.sentenceCount', { n: selectedStory.sentenceCount || 0 }) }}
|
</text>
|
</view>
|
|
<view v-if="currentScene.needsEnPasses" class="mb-[20rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<text class="mb-[8rpx] block text-[28rpx] text-[#232338] font-semibold">
|
{{ t('parentTask.enPassesTitle') }}
|
</text>
|
<text class="mb-[16rpx] block text-[22rpx] text-[#8a8d96]">
|
{{ t('parentTask.cnFixed') }}
|
</text>
|
<view
|
v-for="(pass, index) in enPasses"
|
:key="index"
|
class="mb-[16rpx] rounded-[16rpx] bg-[#f5f7fb] p-[16rpx]"
|
>
|
<view class="mb-[12rpx] flex items-center justify-between">
|
<text class="text-[24rpx] text-[#232338]">
|
{{ t('parentTask.enPassIndex', { n: index + 1 }) }}
|
</text>
|
<text
|
v-if="enPasses.length > 1"
|
class="text-[24rpx] text-[#fa5151]"
|
@click="removeEnPass(index)"
|
>
|
{{ t('parentTask.removePass') }}
|
</text>
|
</view>
|
<view class="mb-[12rpx] flex flex-wrap gap-[12rpx]">
|
<view
|
v-for="speed in SPEED_OPTIONS"
|
:key="speed"
|
class="rounded-[24rpx] px-[16rpx] py-[6rpx] text-[22rpx]"
|
:class="pass.speed === speed ? 'bg-[#336cff] text-white' : 'bg-white text-[#232338]'"
|
@click="setPassSpeed(index, speed)"
|
>
|
{{ speed.toFixed(1) }}×
|
</view>
|
</view>
|
<view class="flex items-center gap-[12rpx]">
|
<text class="text-[24rpx] text-[#65686f]">
|
{{ t('parentTask.repeat') }}
|
</text>
|
<wd-input-number v-model="enPasses[index].repeat" :min="1" :max="10" />
|
</view>
|
</view>
|
<view
|
v-if="enPasses.length < 8"
|
class="rounded-[16rpx] border border-dashed border-[#336cff] py-[12rpx] text-center text-[24rpx] text-[#336cff]"
|
@click="addEnPass"
|
>
|
{{ t('parentTask.addEnPass') }}
|
</view>
|
</view>
|
|
<view v-if="currentScene.needsShadowingSettings" class="mb-[20rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-semibold">
|
{{ t('parentTask.shadowingSettings') }}
|
</text>
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.shadowingLang') }}
|
</text>
|
<view class="mb-[20rpx] flex gap-[16rpx]">
|
<view
|
class="flex-1 rounded-[16rpx] py-[16rpx] text-center text-[26rpx]"
|
:class="shadowingLanguage === 'cn' ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="shadowingLanguage = 'cn'"
|
>
|
{{ t('parentTask.shadowingLang.cn') }}
|
</view>
|
<view
|
class="flex-1 rounded-[16rpx] py-[16rpx] text-center text-[26rpx]"
|
:class="shadowingLanguage === 'en' ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="shadowingLanguage = 'en'"
|
>
|
{{ t('parentTask.shadowingLang.en') }}
|
</view>
|
</view>
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.shadowingSpeed') }}
|
</text>
|
<view class="mb-[16rpx] flex flex-wrap gap-[12rpx]">
|
<view
|
v-for="speed in SPEED_OPTIONS"
|
:key="speed"
|
class="rounded-[24rpx] px-[20rpx] py-[8rpx] text-[24rpx]"
|
:class="shadowingSpeed === speed ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="shadowingSpeed = speed"
|
>
|
{{ speed.toFixed(1) }}×
|
</view>
|
</view>
|
<text class="block text-[22rpx] text-[#8a8d96]">
|
{{ t('parentTask.shadowingHint', { speed: shadowingSpeed.toFixed(1) }) }}
|
</text>
|
</view>
|
|
<view v-if="currentScene.needsLanguagePair" class="mb-[20rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.languagePair') }}
|
</text>
|
<wd-picker
|
v-model="languagePair"
|
:columns="pairColumns"
|
:placeholder="t('parentTask.pickPair')"
|
:cancel-button-text="t('common.cancel')"
|
:confirm-button-text="t('common.confirm')"
|
/>
|
<text v-if="selectedPair && !selectedPair.ready" class="mt-[12rpx] block text-[24rpx] text-[#fa8c16]">
|
{{ t('parentTask.pairFallbackHint') }}
|
</text>
|
</view>
|
|
<view v-if="currentScene.needsDuration || currentScene.needsOnComplete" class="mb-[20rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<view v-if="currentScene.needsDuration" class="mb-[24rpx]">
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.duration') }}
|
</text>
|
<view class="mb-[16rpx] flex flex-wrap gap-[12rpx]">
|
<view
|
v-for="item in [0, 10, 30]"
|
:key="item"
|
class="rounded-[24rpx] px-[20rpx] py-[8rpx] text-[24rpx]"
|
:class="Number(durationMinutes) === item ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="durationMinutes = item"
|
>
|
{{ item === 0 ? t('parentTask.noDuration') : t('parentTask.minutesValue', { n: item }) }}
|
</view>
|
</view>
|
<wd-input-number v-model="durationMinutes" :min="0" :max="240" />
|
</view>
|
<view v-if="currentScene.needsOnComplete">
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.onComplete') }}
|
</text>
|
<view class="flex gap-[16rpx]">
|
<view
|
class="flex-1 rounded-[16rpx] py-[16rpx] text-center text-[26rpx]"
|
:class="onComplete === 'chat' ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="onComplete = 'chat'"
|
>
|
{{ t('parentTask.completeChat') }}
|
</view>
|
<view
|
class="flex-1 rounded-[16rpx] py-[16rpx] text-center text-[26rpx]"
|
:class="onComplete === 'power_off' ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="onComplete = 'power_off'"
|
>
|
{{ t('parentTask.completePowerOff') }}
|
</view>
|
</view>
|
</view>
|
</view>
|
|
<view class="mb-[20rpx] rounded-[20rpx] bg-white p-[28rpx]">
|
<text class="mb-[16rpx] block text-[28rpx] text-[#232338] font-semibold">
|
{{ t('parentTask.schedule') }}
|
</text>
|
<view class="mb-[20rpx] flex flex-wrap gap-[12rpx]">
|
<view
|
v-for="item in SCHEDULE_TYPES"
|
:key="item.id"
|
class="rounded-[24rpx] px-[20rpx] py-[8rpx] text-[24rpx]"
|
:class="scheduleType === item.id ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="scheduleType = item.id"
|
>
|
{{ t(item.key) }}
|
</view>
|
</view>
|
|
<view v-if="scheduleType === 'daily'">
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.time') }}
|
</text>
|
<view class="flex items-center gap-[16rpx]">
|
<wd-input-number v-model="dailyHour" :min="0" :max="23" />
|
<text class="text-[32rpx] text-[#232338]">:</text>
|
<wd-input-number v-model="dailyMinute" :min="0" :max="59" />
|
</view>
|
<text class="mb-[12rpx] mt-[12rpx] block text-[22rpx] text-[#8a8d96]">
|
{{ t('parentTask.timeHint') }} {{ savedDailyTime() }}
|
</text>
|
<text class="mb-[12rpx] mt-[20rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.repeatDays') }}
|
</text>
|
<view class="flex flex-wrap gap-[12rpx]">
|
<view
|
v-for="day in WEEKDAYS"
|
:key="day.value"
|
class="rounded-[24rpx] px-[20rpx] py-[8rpx] text-[24rpx]"
|
:class="weekdays.includes(day.value) ? 'bg-[#336cff] text-white' : 'bg-[#f5f7fb] text-[#232338]'"
|
@click="toggleWeekday(day.value)"
|
>
|
{{ t(`parentTask.weekday.${day.key}`) }}
|
</view>
|
</view>
|
</view>
|
|
<view v-else-if="scheduleType === 'hourly'">
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.hourlyMinute') }}
|
</text>
|
<wd-input-number v-model="hourlyMinute" :min="0" :max="59" />
|
</view>
|
|
<view v-else>
|
<text class="mb-[12rpx] block text-[26rpx] text-[#65686f]">
|
{{ t('parentTask.intervalMinutes') }}
|
</text>
|
<wd-input-number v-model="intervalMinutes" :min="1" :max="1440" />
|
</view>
|
</view>
|
</view>
|
|
<view class="action-bar">
|
<wd-button
|
class="flex-1"
|
type="primary"
|
plain
|
:disabled="!canDispatch"
|
:loading="submitting === 'immediate' || submitting === 'run'"
|
@click="isEdit ? startExisting() : submit('immediate')"
|
>
|
{{ t('parentTask.startNow') }}
|
</wd-button>
|
<wd-button
|
class="flex-1"
|
type="primary"
|
:disabled="!canDispatch"
|
:loading="submitting === 'scheduled'"
|
@click="submit('scheduled')"
|
>
|
{{ t('parentTask.saveSchedule') }}
|
</wd-button>
|
</view>
|
</view>
|
</template>
|
|
<style scoped>
|
.action-bar {
|
position: fixed;
|
left: 0;
|
right: 0;
|
bottom: 0;
|
display: flex;
|
gap: 16rpx;
|
padding: 16rpx 24rpx calc(16rpx + env(safe-area-inset-bottom));
|
background: #ffffff;
|
border-top: 1rpx solid #eeeeee;
|
z-index: 20;
|
}
|
|
</style>
|