1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/guo-tai-0602-english-practice

В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
Внести вклад в разработку кода
Синхронизировать код
Отмена
Подсказка: Поскольку Git не поддерживает пустые директории, создание директории приведёт к созданию пустого файла .keep.
Loading...
README.md

Проект по созданию приложения для изучения английских слов на HarmonyOS 4.0

Клонирование проекта

git clone https://gitee.com/guo-tai-0602/english-price.git

После загрузки проекта на локальную машину, используйте DevEco Studio для открытия проекта. Приложение автоматически загрузит зависимости из директории oh_modules. В данном проекте используется плагин axios для выполнения HTTP-запросов.

Описание серверных интерфейсов

В проекте включены собственные серверные службы, расположенные в директории HttpServer. В этой директории есть версии для MacOS и Windows. Выберите соответствующую версию в зависимости от вашей операционной системы. В данном случае используется версия для Windows. После установки и запуска сервера, откроется окно, как показано на следующем рисунке:

Документация по API

Когда отображаются адреса интерфейсов, это означает, что серверные службы успешно запущены.

Интерфейсы

Внимание: В зависимости от используемой сети, адреса интерфейсов могут отличаться. Убедитесь, что вы изменяете значение baseURL в фронтенде в соответствии с вашими реальными адресами интерфейсов.

Место изменения в фронтенде: entry/src/main/ets/http/Axios.ets, как показано на следующем рисунке:

baseURL в фронтендеВсе используемые API в данном проекте описаны в файле entry/src/main/ets/http/Api.ets. После экспорта, эти API можно импортировать и использовать на нужных страницах, что облегчает управление и изменение.

1. Описание проекта

Основная функция данного приложения - помощь в запоминании английских слов. Оно состоит из трех модулей, как показано на следующем рисунке:

image-20240510141010757

2. Страница с вопросами

2.1. Описание функций

Страница с вопросами имеет три состояния: ответы, пауза и остановка. Начальное состояние - остановка, в котором нельзя отвечать на вопросы. При нажатии на варианты ответов, необходимо отображать подсказки, как показано на следующем рисунке:

image-20240510141303942

В состоянии остановка можно изменять количество слов для тестирования (в других состояниях это невозможно). После изменения количества слов, необходимо заново выбрать соответствующее количество вопросов из базы данных.

image-20240510141336121

Нажатие кнопки начать тестирование переводит страницу в состояние ответы, где начинается отсчет времени:

image-20240510141401217

Ответственный процесс имеет логику, представленную ниже:

image-20240510141433261Во время ответа на вопросы требуется в реальном времени обновлять статистическую информацию, которая включает в себя прогресс и точность, как показано ниже:

image-20240510141507429

Во время ответа на вопросы, нажатие кнопки Приостановить тест переводит процесс в приостановленное состояние, в котором таймер останавливает отсчет времени.

Повторное нажатие кнопки Начать тест возвращает процесс в состояние ответа на вопросы, при этом таймер возобновляет отсчет времени.

Когда все вопросы текущего теста будут завершены или нажата кнопка Завершить тест, процесс переходит в остановленное состояние, и появляется окно с результатами статистики, как показано ниже:

image-20240510141525843

В этом случае:

  • Нажатие кнопки Закрыть в правом верхнем углу закрывает окно, при этом тестовые вопросы и статистическая информация сбрасываются, и страница ответов возвращается в исходное состояние.
  • Нажатие кнопки Сыграть еще раз закрывает окно, при этом тестовые вопросы и статистическая информация сбрасываются, и затем процесс переходит в состояние ответа на вопросы.
  • Нажатие кнопки Войти закрывает окно, при этом тестовые вопросы и статистическая информация сбрасываются, и затем процесс переходит на страницу входа.####

2.2. Реализация

2.2.1. Требуемые навыки

1. Использование стандартных компонентов разметки: Column, Row и т.д.
2. Использование стандартных компонентов: Progress, Button, Image, Text, TextTimer (таймер) и т.д.
3. Создание пользовательских компонентов.
4. Создание пользовательских диалоговых окон.
5. Управление состоянием компонентов: @State, @Prop, @Link, @Watch и т.д.

Вышеуказанный контент можно найти в главах OnClickListener, 4 и 5 учебника по разработке приложений для HarmonyOS 4.0.

2.2.2. Процесс реализации

Основная разметка страницы ответов представлена ниже:

image-20240510141859362Стили компонентов представлены ниже| Компонент | Стиль | Эффект | | ---------------- | :----------------------------------------------------------- | ------------------------------------------------------------ | | Фон страницы | @Extend(Column) function practiceBgStyle() {
.width('100%')
.height('100%')
.backgroundImage($r('app.media.img_practice_bg'))
.backgroundImageSize({ width: '100%', height: '100%' })
.justifyContent(FlexAlign.SpaceEvenly)
} | ! image-20240510142638214 | | Фон панели статистики | @Styles function statBgStyle() {
.backgroundColor(Color.White)
.width('90%')
.borderRadius(10)
.padding(20)
} | ! image-20240510143301916 | | Слово | @Extend(Text) function wordStyle() {
.fontSize(50)
.fontWeight(FontWeight.Bold)
} | ! image-20240510143325045 | | Пример предложения | @Extend(Text) function sentenceStyle() {
.height(40)
.fontSize(16)
.fontColor('#9BA1A5')
.fontWeight(FontWeight.Medium)
.width('80%')
.textAlign(TextAlign.Center)
} | ! image-20240510144130894 | | Кнопка выбора | @Extend(Button) function optionButtonStyle(color: {
bg: ResourceColor,
font: ResourceColor
}) {
.width(240)
.height(48)
.fontSize(16)
.type(ButtonType.Normal)
.fontWeight(FontWeight.Medium)
.borderRadius(8)
.backgroundColor(color.bg)
.fontColor(color.font)
} | ! image-20240510144154737 | | Кнопка управления | @Extend(Button) function controlButtonStyle(color: {
bg: ResourceColor,
border: ResourceColor,
font: ResourceColor
}) {
.fontSize(16)
.borderWidth(1)
.backgroundColor(color.bg)
.borderColor(color.border)
.fontColor(color.font)
} | ! image-20240510144209988 |imgdb.cn/item/663dd9240ea9cb140393ff10.png) |##### 2. 2. 2. 2. Практическое состояниеПрактическое состояние имеет три возможных значения: ответственный режим, пауза и остановка. Мы можем определить перечисление для представления этих состояний, как показано ниже:

export enum PracticeStatus {
  Running, // Ответственный режим
  Paused, // Пауза
  Stopped // Остановка
}

Для отслеживания текущего состояния можно определить переменную типа перечисления, как показано ниже:

@State practiceStatus: PracticeStatus = PracticeStatus.Stopped

Управление практическим состоянием осуществляется с помощью двух кнопок в нижней части интерфейса. Важно отметить, что стили кнопок также должны изменяться в зависимости от текущего состояния, как показано на следующих изображениях:

Практическое состояние Остановка Ответственный режим Пауза
Эффект image-20240510145227187 image-20240510145256726 image-20240510145305424

Для более подробного понимания можно обратиться к следующему коду:

Button('Остановить тест')
  .controlButtonStyle({
    bg: Color.Transparent,
    border: this.practiceStatus === PracticeStatus.Stopped ? Color.Gray : Color.Black,
    font: this.practiceStatus === PracticeStatus.Stopped ? Color.Gray : Color.Black
  })
  .enabled(this.practiceStatus !== PracticeStatus.Stopped)
``````Button(this.practiceStatus === PracticeStatus.Running ? 'Приостановить тест' : 'Начать тест')
  .controlButtonStyle({
    bg: this.practiceStatus === PracticeStatus.Running ? '#555555' : Color.Black,
    border: this.practiceStatus === PracticeStatus.Running ? '#555555' : Color.Black,
    font: Color.White
  })
  .stateEffect(false)

Кроме того, необходимо привязать обработчики событий нажатия для этих двух кнопок, чтобы управлять изменениями практического состояния.

##### 2.2.2.3. Логика переключения вопросов

Эффект переключения вопросов реализуется с помощью двух переменных состояния: массива вопросов и индекса массива. Массив содержит все вопросы текущего теста, а индекс указывает на текущий вопрос. Как показано на следующем рисунке, изменение значения `currentIndex` позволяет переключаться между вопросами.

Тип данных для вопросов определен следующим образом:

```arkts
export interface Question {
  word: string; // слово
  sentence: string; // примерное предложение
  options: string[]; // варианты ответа
  answer: string; // правильный ответ
}
``````javascript
export const questionData: Question[] = [
    {
      word: "book",
      options: ["книга", "ручка", "ластик", "рюкзак"],
      answer: "книга",
      sentence: "Я люблю читать хорошую книгу каждый вечер."
    },
    {
      word: "computer",
      options: ["телевизор", "компьютер", "смартфон", "камера"],
      answer: "компьютер",
      sentence: "Я использую компьютер для работы и развлечений."
    },
    {
      word: "apple",
      options: ["банан", "слива", "яблоко", "груша"],
      answer: "яблоко",
      sentence: "Она любит есть хрустящее яблоко днём."
    },
    {
      word: "sun",
      options: ["луна", "солнце", "звёзды", "земля"],
      answer: "солнце",
      sentence: "Солнце предоставляет тепло и свет нашей планете."
    },
    {
      word: "water",
      options: ["огонь", "земля", "ветер", "вода"],
      answer: "вода",
      sentence: "Я всегда ношу с собой бутылку воды."
    },
    {
      word: "mountain",
      options: ["пустыня", "море", "плоскогорье", "гора"],
      answer: "гора",
      sentence: "Горная цепь покрыта снегом зимой."
    },
    {
      word: "flower",
      options: ["дерево", "трава", "цветок", "куст"],
      answer: "цветок",
      sentence: "Сад полон ярких цветов."
    },
    {
      word: "car",
      options: ["велосипед", "самолёт", "яхта", "автомобиль"],
      answer: "автомобиль",
      sentence: "Я езжу на автомобиле на работу каждый день."
    },
    {
      word: "time",
      options: ["пространство", "часы", "календарь", "время"],
      answer: "время",
      sentence: "Время летит, когда ты веселишься."
    },
    {
      word: "music",
      options: ["картинка", "танец", "музыка", "театр"],
      answer: "музыка",
      sentence: "Слушание музыки помогает мне расслабиться."
    },
    {
      word: "rain",
```     options: ["снег", "гроза", "солнце", "дождь"],
      answer: "дождь",
      sentence: "Мне нравится звук дождя, стучащего по окну."
    },
    {
      word: "fire",
      options: ["лёд", "пламя", "дым", "молния"],
      answer: "пламя",
      sentence: "Костер согревал нас в холодный вечер."
    },
    {
      word: "friend",
      options: ["незнакомец", "сосед", "родственник", "друг"],
      answer: "друг",
      sentence: "Настоящий друг всегда рядом."
    },
    {
      word: "food",
      options: ["фрукты", "овощи", "мясо", "еда"],
      answer: "еда",
      sentence: "Здоровая еда необходима для сбалансированного питания."
    },
    {
      word: "color",
      options: ["чёрный", "белый", "красный", "цвет"],
      answer: "цвет",
      sentence: "Цветное изображение выглядит ярче."
    },
    {
      word: "color",
      options: ["чёрный", "белый", "красный", "цвет"],
      answer: "цвет",
      sentence: "Художник использовал яркую цветовую палитру."
    },
    {
      word: "bookshelf",
      options: ["стул", "стол", "полка для книг", "кровать"],
      answer: "полка для книг",
      sentence: "Полка для книг заполнена романами и справочными книгами."
    },
    {
      word: "moon",
      options: ["солнце", "звезда", "луна", "земля"],
      answer: "луна",
      sentence: "Лунный свет освещал ночное небо."
    },
    {
      word: "school",
      options: ["парк", "магазин", "больница", "школа"],
      answer: "школа",
      sentence: "Ученики ходят в школу, чтобы учиться и расти."
    },
    {
      word: "shoes",
      options: ["шляпа", "одежда", "брюки", "туфли"],
      answer: "туфли",
      sentence: "Она купила новую пару стильных туфель."
    },
    {
      word: "camera",
      options: ["телевизор", "компьютер", "камера", "телефон"],
      answer: "камера",
      sentence: "Фотограф запечатлел момент с помощью своей камеры."
    }
  ] // Случайным образом выбираем n вопросов из базы вопросов
 ```javascript
 export function getRandomQuestions(count: number) {
   let length = questionData.length;
 ```  let indexes: number[] = [];
  while (indexes.length < count) {
    let index = Math.floor(Math.random() * length);
    if (!indexes.includes(index)) {
      indexes.push(index);
    }
  }
  return indexes.map(index => questionData[index]);
}
```

**Примечание:** При смене вопроса необходимо учитывать задержку и в это время кнопки выбора должны быть недоступны.

##### 2.2.2.4. Оценка правильности ответа

Логика оценки правильности ответа является относительно сложной, и её следует поэтапно реализовать.

**Шаг 1: Создание компонента кнопки выбора**

Оценка правильности ответа требует изменения стиля кнопок выбора, которые имеют три различных стиля, как показано на следующем рисунке:

![image-20240510145613011](https://pic.imgdb.cn/item/663dd9830ea9cb140394b3b6.png)

Учитывая вышеупомянутые стили, можно извлечь кнопку выбора в отдельный компонент и определить переменную состояния для управления стилем кнопки. Тип переменной состояния можно определить следующим перечислением:

```arkts
export enum OptionStatus {
  Default, // По умолчанию
  Right, // Правильный
  Wrong // Неправильный
}
```

Таким образом, после ответа на вопрос, нам нужно будет изменить только переменную состояния, чтобы кнопка отображала соответствующий стиль.

**Шаг 2: Реализация логики изменения состояния кнопки**

В нормальных условиях, при каждом изменении вопроса, компоненты кнопок выбора, отрендеренные с помощью ForEach, будут пересозданы. Поэтому нам нужно учитывать только переход кнопок из состояния Default в Right или Wrong.<div style="background-color: #f0f9eb; padding: 10px;">
<b>Примечание:</b> <br>
    Если массив <b>options</b> двух последовательных вопросов имеет пересечение, то согласно принципу ForEach, который стремится к повторному использованию существующих компонентов, некоторые <b>OptionButton</b> могут не пересоздаваться. В этом случае нам нужно будет восстановить состояние этих <b>OptionButton</b> из состояния Right или Wrong предыдущего вопроса до состояния Default. Для упрощения логики можно установить генератор ключей ForEach следующим образом: <br>
option => this.questions[this.currentIndex].word + '-' + option, чтобы гарантировать пересоздание <b>OptionButton</b> для каждого вопроса.
</div>

Переход кнопок выбора из состояния Default в Right или Wrong требует учитывать следующие два вопроса:<div style="background-color: #DFEEFD; padding: 10px;">
1. Как активировать действие, которое изменяет состояние каждого кнопки
2. Как каждый кнопка определяет, в какое состояние он должен перейти
</div>

Конкретная логика представлена на следующей схеме

![image-20240510150316988](https://pic.imgdb.cn/item/663dd9990ea9cb140394e3d6.png)<div style="background-color: #DFEEFD; padding: 10px;">
<b>Объяснение:</b> <br>
1. Внутри компонента-потомка переменные <b>option</b> и <b>answer</b> представляют опции и правильные ответы соответственно, поэтому компонент-потомок может определить, является ли он правильным ответом, на основе этих двух переменных.<br>
2. Переменная @State <b>selectedOption</b> в компоненте-родителе используется для отслеживания выбранного опции, а переменная @Prop <b>selectedOption</b> в компоненте-потомке синхронизирует изменения в компоненте-родителе, поэтому компонент-потомок может определить, является ли он выбранным ответом, на основе <b>option</b> и <b>selectedOption</b>.<br>
3. Переменная <b>answerStatus</b> представляет статус ответа на вопрос, который может быть одним из двух значений: <b>AnswerStatus.Answering</b> или <b>AnswerStatus.Answered</b>. Начальный статус ответа для каждого вопроса — <b>AnswerStatus.Answering</b>, после ответа он становится <b>AnswerStatus.Answered</b>. Компонент-родитель использует переменную <b>answerStatus</b> для управления доступностью кнопок-опций, а компонент-потомок активирует изменение переменной <b>optionStatus</b> при изменении <b>answerStatus</b>.
</div>##### 2.2.2.5. Статистические данные

В связи с схожей структурой статистических данных, можно рассмотреть возможность выделения статистических данных в отдельный пользовательский компонент, который должен иметь три параметра: **иконка**, **название** и **UI-компонент**

![image-20240510150733938](https://pic.imgdb.cn/item/663dd9a80ea9cb140394fe44.png)

**Внимание:** параметр **UI-компонента** должен быть декорирован с помощью @BuilderParam

Учитывая, что в будущем для круга отметок потребуется статистическая информация с разными цветами шрифтов, можно добавить еще один параметр — **цвет шрифта**

![image-20240510150756604](D:\Huawei\EnglishPractice\reamde_img\image-20240510150756604.png)

Стили компонента можно использовать в соответствии с таблицей ниже

![image-20240510150809664](https://pic.imgdb.cn/item/663dd9ba0ea9cb1403951d38.png)

###### 2.2.2.5.1. Точность

Для статистики **точности** необходимо определить два состояния: `answeredCount` и `rightCount`. Переменная `answeredCount` представляет количество ответов в текущем тесте, а `rightCount` — количество правильных ответов. Эти переменные обновляются после каждого ответа.

###### 2.2.2.5.2. Прогресс

Для статистики прогресса используются два состояния: `totalCount` и `answeredCount`, которые отображаются с помощью компонента **Progress**.

###### 2.2.2.5.3. Количество словКоличество слов отображается с помощью компонента кнопки **Button**. При нажатии на кнопку должен появляться текстовый выборщик, позволяющий выбрать количество слов для следующего теста. После выбора необходимо перезагрузить задания с указанным количеством слов. Стили кнопки можно увидеть в таблице ниже.![image-20240510150844797](https://pic.imgdb.cn/item/663dd9ca0ea9cb1403953c48.png)

<div style="background-color: #f0f9eb; padding: 10px;">
<b>Внимание</b> : Количество слов можно изменять только в состоянии <b>Стоп</b>.
</div>

###### 2.2.2.5.4. Время

Для использования таймера необходимо использовать компонент **TextTimer**. Пример использования компонента приведен ниже.

**1. Параметры**

Компонент **TextTimer** требует передачи параметра `controller`, который используется для управления запуском, остановкой и сбросом таймера. Пример использования приведен ниже.

```arkts
// Объявление контроллера
timerController: TextTimerController = new TextTimerController();

// Объявление компонента
TextTimer({ controller: this.timerController })

// Запуск таймера
this.timerController.start()
// Остановка таймера
this.timerController.pause()
// Сброс таймера
this.timerController.reset()
```

**2. События**

Часто используемое событие компонента **TextTimer** — это `onTimer`, которое срабатывает при изменении таймера. Это событие можно использовать для записи времени. Функция обратного вызова принимает следующие параметры.

```arkts
(utc: number, elapsedTime: number) => void
```

В параметре `utc` указывается текущий таймстамп, а `elapsedTime` — время, прошедшее с момента запуска таймера, измеренное в миллисекундах.

##### 2.2.2.6. Модальные окна

Модальные окна используются для отображения статистической информации. Для этого необходимо определить три параметра для модального окна: `answeredCount`, `rightCount`, `timeUsed`.Структура модального окна представлена на следующем рисунке.

![image-20240510151136116](https://pic.imgdb.cn/item/663dd9dd0ea9cb1403955e57.png)

Стили компонентов внутри модального окна можно увидеть в таблице ниже.

![image-20240510151219960](https://pic.imgdb.cn/item/663dd9f60ea9cb1403958c03.png)

<div style="background-color: #f0f9eb; padding: 10px;">
<b>Внимание</b>: По умолчанию все модальные окна используют стандартные стили. Для использования пользовательских стилей необходимо указать параметр <b>customStyle:true</b> для <b>CustomDialogController</b>.
</div>

Логика преобразования времени в миллисекунды представлена в следующем коде.

```arkts
export function convertMillisecondsToTime(timeUsed: number): string {
  // Вычисление часов, минут и секунд
  const hours = Math.floor(timeUsed / 3600000); // 1 час = 3600000 миллисекунд
  const minutes = Math.floor((timeUsed % 3600000) / 60000); // 1 минута = 60000 миллисекунд
  const seconds = Math.floor((timeUsed % 60000) / 1000); // 1 секунда = 1000 миллисекунд
}
```

## 3. Таб-разметка

### 3.1. Обзор

В этом разделе необходимо реализовать таб-разметку. Конкретный результат представлен ниже:

![image-20240510151428062](https://pic.imgdb.cn/item/663dda0e0ea9cb140395c006.png)

### 3.2. Реализация

#### 3.2.1. Необходимые навыки

**Tabs** компонент

#### 3.2.2. Процесс реализации

##### 3.2.2.1. Стили для табов

Стили для табов представлены в таблице ниже:

![image-20240510151536341](https://pic.imgdb.cn/item/663dda270ea9cb140395f82a.png)

## 4. Страница приветствия

### 4.1. Обзор

Страница приветствия имеет относительно простую функциональность. Конкретный результат представлен ниже:![recording](https://pic.imgdb.cn/item/663dd77f0ea9cb140390a754.gif)

### 4.2. Реализация

#### 4.2.1. Необходимые навыки

Необходимые навыки для модуля ответов представлены ниже:

<div style="background-color: #f0f9eb; padding: 10px;">
1. Эффекты анимации компонентов<br>
2. Маршрутизация страниц<br>
3. Жизненные циклы компонентов<br>
</div>

Данные навыки можно найти в главах bk8, bk9 и bk10 учебника "HarmonyOS 4.0 Разработка приложений".

#### 4.2.2. Процесс реализации

##### 4.2.2.1. Базовая разметка и стили

Базовая разметка страницы приветствия представлена на следующем рисунке:

![image-20240510151849110](https://pic.imgdb.cn/item/663dda570ea9cb1403964d30.png)

Стили для компонентов представлены в таблице ниже:

![image-20240510151948576](https://pic.imgdb.cn/item/663dda670ea9cb1403966bb1.png)

##### 4.2.2.2. Реализация анимации

Необходимые анимации представлены на следующем рисунке:

![recording](https://pic.imgdb.cn/item/663ddaac0ea9cb1403972c33.gif)

Как видно, данные анимации относятся к анимации перехода компонентов. Поэтому можно использовать метод `transition()` для настройки анимации. Важно отметить, что данные анимации включают два эффекта перехода: **перемещение** и **прозрачность**.

##### 4.2.2.3. Триггер анимации

Требуется, чтобы анимация автоматически запускалась при появлении страницы. Для этого можно использовать жизненный цикл компонента `onPageShow()`.

```##### 4.2.2.4. Переход на страницу
```Требуется, чтобы после завершения анимации интерфейса было выполнено ожидание в 200 мс перед переходом на страницу с вопросами. Для этого используется функционал маршрутизации страниц. Важно отметить, что обычно страница приветствия **не может быть возвращена**.##### 4.2.2.5. Указание начальной страницы приложения

Измените следующий фрагмент в файле `entry/src/main/ets/entryability/EntryAbility.ts`, чтобы указать страницу приветствия как начальную страницу приложения.

```arkts
onWindowStageCreate(windowStage: window.WindowStage) {
  // Основное окно создано, задаем начальную страницу для этого способа
  hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

  // Место для изменения
  windowStage.loadContent('pages/SplashPage', (err, data) => {
    if (err.code) {
      hilog.error(0x0000, 'testTag', 'Не удалось загрузить контент. Причина: %{public}s', JSON.stringify(err) ?? '');
      return;
    }
    hilog.info(0x0000, 'testTag', 'Успешная загрузка контента. Данные: %{public}s', JSON.stringify(data) ?? '');
  });
}
```

**Описание:** Содержание данного файла можно найти в главе 15 книги "HarmonyOS 4.0 Разработка приложений".

## 5. Функция входа

### 5.1. Обзор

Метод входа в систему осуществляется через SMS-код, как показано на следующем рисунке.

![image-20240510152227382](https://pic.imgdb.cn/item/663ddac50ea9cb1403977288.png)

### 5.2. План реализации

#### 5.2.1. Необходимые навыки

Необходимые навыки для реализации функции входа в систему следующие:

<div style="background-color: #f0f9eb; padding: 10px;">
1. Запросы по сети
2. Управление состоянием приложения
</div>

Данный раздел можно найти в главах 12 и 13 книги "HarmonyOS 4.0 Разработка приложений".

#### 5.2.2. Процесс реализации

##### 5.2.2.1. Основные элементы и стили

Основные элементы и стили страницы входа можно найти в следующем коде.```arkts
import router from '@ohos.router'
@Entry
@Component
struct LoginPage {
  @State phone: string = ''
  @State code: string = ''
  build() {
    Column() {
      Image($r('app.media.ic_back'))
        .backStyle()
        .alignSelf(ItemAlign.Start)
        .onClick(() => {
          //todo: вернуться на предыдущую страницу
        })

      Blank()
      Column() {
        Text('Добро пожаловать')
          .titleStyle()

        Row() {
          Image($r("app.media.ic_phone"))
            .iconStyle()
          TextInput({ placeholder: 'Введите номер телефона', text: this.phone })
            .inputStyle()
            .onChange((value) => {
              this.phone = value;
            })
        }.margin({ top: 30 })

        Divider()
          .color(Color.Black)

        Row() {
          Image($r("app.media.ic_code"))
            .iconStyle()
          TextInput({ placeholder: 'Введите код', text: this.code })
            .inputStyle()
            .onChange((value) => {
              this.code = value;
            })
          Button('Получить код')
            .buttonStyle(Color.White, Color.Black)
            .onClick(() => {
              //todo: получить код
            })
        }.margin({ top: 20 })

        Divider()
          .margin({ right: 120 })
          .color(Color.Black)

        Button('Войти')
          .buttonStyle(Color.Black, Color.White)
          .width('100%')
          .margin({ top: 50 })
          .onClick(() => {
            //todo: войти
          })

        Row() {
          Text('Вход означает согласие с')
            .fontSize(10)
            .color('#546B9D')
          Text('«Пользовательским соглашением»')
            .fontSize(10)
            .color('#00B3FF')
        }.margin({ top: 20 })
      }.formStyle()
    }
  }
}
```      Строка({пространство: 10}) {
        Изображение($r('app.media.ic_logo'))
          .ширина(36)
          .высота(36)
        Текст('Быстрый способ запоминания слов')
          .цветШрифта('#546B9D')
          .жирныйШрифт()
          .размерШрифта(20)
      }.отступ({ верх: 70 })      Текст('Разработано Atguigu')
        .размерШрифта(12)
        .цветШрифта('#546B9D')
        .отступ(10)

    }
    .стильВхода()

  @Styles функция стилевхода() {
    .ширина('100%')
    .высота('100%')
    .фоновоеИзображение($r("app.media.img_login_bg"))
    .размерФоновогоИзображения({ширина: '100%', высота: '100%'})
    .отступ({ верх: 30, низ: 30, левый: 20, правый: 20 })
  }

  @Styles функция стилевозврата() {
    .ширина(25)
    .высота(25)
  }

  @Styles функция стилеФормы() {
    .цветФона(Color.White)
    .отступ(30)
    .круглостьКраев(20)
  }

  @Extend(Текст) функция стильЗаголовка() {
    .жирныйШрифт()
    .размерШрифта(22)
  }

  @Styles функция стильИконки() {
    .ширина(24)
    .высота(24)
  }

  @Extend(ТекстовоеПоле) функция стильПоля() {
    .высота(40)
    .размещениеВеса(1)
    .размерШрифта(14)
    .цветФона(Color.Transparent)
  }

  @Extend(Кнопка) функция стильКнопки(цветФона: ResourceColor, цветШрифта: ResourceColor) {
    .тип(Кнопка.Тип.обычный)
    .размерШрифта(14)
    .жирныйШрифт()
    .ширинаКраев(1)
    .круглостьКраев(5)
    .цветФона(цветФона)
    .цветШрифта(цветШрифта)
  }
}##### 5.2.2.2. Интеграция с backend-интерфейсами

**Шаг 1: Добавление зависимости axios**

Выполните следующую команду в терминале:

```powershell
npm install @ohos/axios
```

**Шаг 2: Создание экземпляра axios**

```arkts
import axios, { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from '@ohos/axios'
import promptAction from '@ohos.promptAction';

// Создание экземпляра axios
export const instance = axios.create({
  baseURL: 'http://xxx.xxx.xxx.xxx:3000',
  timeout: 2000
})
```// Добавление запроса-интерцептора
instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
  // Получение токена из AppStorage
  const token = AppStorage.Get('token');
  if (token) {
    // Если токен существует, добавляем его в заголовок запроса
    config.headers['token'] = token;
  }
  return config;
}, (error: AxiosError) => {
  // Если произошла ошибка, выводим сообщение об ошибке
  promptAction.showToast({ message: error.message });
  return Promise.reject(error);
});

// Дополнение ответа-интерцептора
instance.interceptors.response.use((response: AxiosResponse) => {
  // Если сервер вернул нормальные данные, ничего не делаем
  if (response.data.code === 200) {
    return response;
  } else {
    // Если сервер вернул ошибочные данные, выводим сообщение об ошибке
    promptAction.showToast({ message: response.data.message });
    return Promise.reject(response.data.message);
  }
}, (error: AxiosError) => {
  // Если произошла ошибка, выводим сообщение об ошибке
  promptAction.showToast({ message: error.message });
  return Promise.reject(error);
});
```

**Шаг 3: Интеграция с backend-интерфейсами**

Для функции входа в систему необходимо интегрироваться с двумя backend-интерфейсами: получение кода подтверждения и вход в систему.

```arkts
// Получение кода подтверждения
export function sendCode(phone: string) {
  return instance.get('/word/user/code', { params: { phone: phone } });
}

// Вход в систему
export function login(phone: string, code: string) {
  return instance.post('/word/user/login', { phone: phone, code: code });
}
```

<div style="background-color: #f0f9eb; padding: 10px;">
<b>Внимание:</b> Необходимо настроить <b>разрешение на доступ к сети</b>
</div>

##### 5.2.2.3. Реализация логики входа в системуЛогика входа в систему довольно проста, важно сохранить токен в **PersistentStorage** после успешного входа и вернуться на предыдущую страницу.

## 6. Функция отметки присутствия

### 6.1. Описание

После реализации функции входа в систему, можно реализовать функцию отметки присутствия после завершения тестирования.![image-20240510152710315](https://pic.imgdb.cn/item/663ddadc0ea9cb140397a480.png)

В зависимости от текущего состояния авторизации, в окне результатов должны отображаться разные кнопки для отметки присутствия. Если пользователь авторизован, должна отображаться кнопка **Immediate Presence Marking**, в противном случае — кнопка **Authorization for Presence Marking**.

При нажатии на кнопку **Immediate Presence Marking** должна быть отправлена заявка на отметку присутствия, после чего происходит переход на страницу отметки присутствия. Конкретный процесс представлен на следующем рисунке.

![image-20240510152729470](https://pic.imgdb.cn/item/663ddaec0ea9cb140397c374.png)

При нажатии на кнопку **Authorization for Presence Marking** сначала происходит переход на страницу авторизации. После успешной авторизации отправляется заявка на отметку присутствия, после чего происходит переход на страницу отметки присутствия. Конкретный процесс представлен на следующем рисунке.

![image-20240510152743130](https://pic.imgdb.cn/item/663ddafb0ea9cb140397e23f.png)### 6.2. Реализация

#### 6.2.1. Логика перехода между страницами

Сначала реализуем логику перехода между страницами в соответствии с вышеуказанными требованиями.

#### 6.2.2. Интеграция с сервером

```arkts
export function createPost(post: {
  rightCount: number,
  answeredCount: number,
  timeUsed: number
}) {
  return instance.post('/word/post/create', post)
}
```

## 7. Страница отметки присутствия

### 7.1. Обзор

Страница отметки присутствия используется для отображения всех записей отметки присутствия пользователей и предоставляет функцию лайка.

![image-20240510152838700](https://pic.imgdb.cn/item/663ddb0a0ea9cb140398015c.png)

### 7.2. Реализация

#### 7.2.1. Определение состояния списка отметок присутствия

Структура возвращаемых сервером данных по отметке присутствия выглядит следующим образом:

```arkts
{
  "id": 0,
  "postText": "string", //Текст отметки присутствия
  "rightCount": 0, //Количество правильных ответов
  "answeredCount": 0, //Количество ответов
  "timeUsed": 0, //Время затраченное на ответы
  "createTime": "string", //Время создания отметки присутствия
  "likeCount": 0, //Количество лайков
  "nickname": "string", //Никнейм пользователя
  "avatarUrl": "string", //URL аватара пользователя
  "isLike": true //Указывает, лайкнул ли текущий пользователь
}
```

Свойство `isLike` указывает, лайкнул ли текущий пользователь. В зависимости от этого свойства должны отображаться иконки лайка в разных цветах, как показано ниже.

![image-20240510152922864](https://pic.imgdb.cn/item/663ddb1a0ea9cb140398210e.png)При выполнении пользователем действия лайка или отмены лайка необходимо изменить значение `isLike`, чтобы переключить цвет иконки. Важно отметить, что мы будем использовать массив для хранения списка отметок присутствия, а `isLike` — свойство элемента массива. В предыдущем разделе было упомянуто, что прямое изменение свойств элемента массива не будет отслеживаться фреймворком. Поэтому нам нужно использовать подкомпонент, передавая отметку присутствия как свойство этого компонента и используя декоратор `@ObjectLink`. Также тип отметки присутствия должен быть классом, который декорирован с помощью `@Observed`. Определение этого класса представлено ниже.

```arkts
@Observed
export class PostInfo {
  id: число;
  postText: строка;
  rightCount: число;
  answeredCount: число;
  timeUsed: число;
  createTime: строка;
  likeCount: число;
  nickname: строка;
  avatarUrl: строка;
  isLike: логический;

  конструктор(post: {id: число, postText: строка, rightCount: число, answeredCount: число, timeUsed: число, createTime: строка, likeCount: число, nickname: строка, avatarUrl: строка, isLike: логический}) {
    это.id = post.id;
    это.postText = post.postText;
    это.rightCount = post.rightCount;
    это.answeredCount = post.answeredCount;
    это.timeUsed = post.timeUsed;
    это.createTime = post.createTime;
    это.likeCount = post.likeCount;
    это.nickname = post.nickname;
    это.avatarUrl = post.avatarUrl;
    это.isLike = post.isLike;
  }
}
```

Массив определений данных о посте:```arkts
@Состояние postInfoList: PostInfo[] = []
```

#### 7.2.2. Основной макет и стили

Для удобства разработки макета и стилей можно добавить тестовый элемент в массив `postInfoList`, как показано ниже:

```arkts
@Состояние postInfoList: PostInfo[] = [новый PostInfo({
  id: 1,
  postText: "Если выбрали путь вперед, не подводи свою молодость, иди вперед с упорством",
  rightCount: 3,
  answeredCount: 4,
  timeUsed: 5747,
  createTime: "2024-03-19 18:54:33",
  likeCount: 1,
  nickname: "138****8888",
  avatarUrl: "https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg",
  isLike: ложь
})]
```

Круг постов требует, чтобы пользователь был авторизован для доступа, поэтому необходимо отображать разные данные в зависимости от состояния авторизации, как показано ниже:

![image-20240510153108198](https://pic.imgdb.cn/item/663ddb2e0ea9cb14039841cb.png)

Состояние авторизации можно определить по **токену**

```arkts
@СвойствоХранилища('токен') токен: строка = ''
```

Основной фрейм страницы можно использовать следующий код в качестве примера:

![image-20240510153138420](https://pic.imgdb.cn/item/663ddb3e0ea9cb1403986138.png)

**Неавторизованный** пользователь видит следующий контент:

![image-20240510153158228](https://pic.imgdb.cn/item/663ddb4d0ea9cb1403987c65.png)

**Авторизованный** пользователь видит следующий контент:

![image-20240510153257857](https://pic.imgdb.cn/item/663ddb610ea9cb140398a34b.png)```arkts
// PostList
@Строитель
listBuilder() {
  Stack() {
    List() {
      ДляКаждого(это.postInfoList, (post) => {
        ListItem() {
          PostItem({ post: post })
        }
      })
    }.ширина('100%')
    .высота('100%')
    .выравнивание(ListItemAlign.Center)
}
```    
Column({ space: 20 }) {
  Button({ type: ButtonType.Circle }) {
    Image($r('app.media.ic_top'))
      .height(14)
      .width(14)
  }
  .height(40)
  .width(40)
  .backgroundColor(Color.Black)
  .opacity(0.5)
  .onClick(() => {
    //todo: вернуться вверх
  })
}      Button({ type: ButtonType.Circle }) {
        Image($r('app.media.ic_refresh'))
          .height(14)
          .width(14)
      }
      .height(40)
      .width(40)
      .backgroundColor(Color.Black)
      .opacity(0.5)
      .onClick(() => {
        //todo: обновить
      })
    }
    .offset({ x: -20, y: -50 })
  }.width('100%')
  .layoutWeight(1)
  .alignContent(Alignment.BottomEnd)
}


// PostItem
@Component
struct PostItem {
  @ObjectLink post: PostInfo;

  build() {
    Column({ space: 10 }) {
      Row({ space: 10 }) {
        Image(this.post.avatarUrl)
          .height(40)
          .width(40)
          .borderRadius(20)
        Text(this.post.nickname)
          .height(40)
          .fontSize(14)
          .fontWeight(FontWeight.Bold)
        Blank()
        Text(this.post.createTime)
          .height(40)
          .fontSize(14)
          .fontColor('#999999')
          .fontWeight(FontWeight.Medium)
      }.width('100%')

      Text(this.post.postText)
        .width('100%')

      Row() {
        Column() {
          StatItem({
            icon: $r('app.media.ic_timer_white'),
            name: 'Время',
            fontColor: Color.White }) {
            Text(convertMillisecondsToTime(this.post.timeUsed))
              .statTextStyle()
          }

          StatItem({
            icon: $r('app.media.ic_accuracy_white'),
            name: 'Точность',
            fontColor: Color.White
          }) {
            Text((this.post.answeredCount === 0 ? 0 : this.post.rightCount / this.post.answeredCount * 100).toFixed(0) + '%')
              .statTextStyle()
          }
        }
      }
    }
  }
}```markdown
          StatItem({
            icon: $r('app.media.ic_count_white'),
            name: 'Количество',
            fontColor: Color.White
          }) {
            Text(this.post.answeredCount.toString())
              .statTextStyle()
          }
        }
        .padding(10)
        .borderRadius(10)
        .layoutWeight(1)
        .backgroundImage($r('app.media.img_post_bg'))
        .backgroundImageSize(ImageSize.Cover)
        ```markdown
        Column() {
          Text(this.post.likeCount.toString())
            .fontSize(12)
            .fontWeight(FontWeight.Medium)
            .fontColor(this.post.isLike ? '#3ECBA1' : '#000000')
          Image(this.post.isLike ? $r('app.media.ic_post_like_selected') : $r('app.media.ic_post_like'))
            .width(26)
            .height(26)
            .onClick(() => {
              //todo:лайк/отменить лайк
            })
        }.width(50)
      }.width('100%')
      .alignItems(VerticalAlign.Bottom)
    }
    .padding(10)
    .width('90%')
    .margin({ top: 10 })
    .borderRadius(10)
    .shadow({ radius: 20 })
  }
```@Extend(Text) function statTextStyle() {
  .width(100)
  .fontSize(16)
  .textAlign(TextAlign.End)
  .fontWeight(FontWeight.Medium)
  .fontColor(Color.White)
}
```

#### 7.2.3. Интеграция с серверными API

Для получения информации о постах, лайках и отмене лайков необходимо интегрироваться с тремя серверными API. Конкретные детали представлены ниже:

```arkts
// Получение списка всех постов
export function getAllPost(page: number, size: number) {
  return instance.get('/word/post/getAll', { params: { page: page, size: size } })
}

// Лайк
export function like(postId: number) {
  return instance.get('/word/like/create', { params: { postId: postId } })
}

// Отмена лайка
export function cancelLike(postId: number) {
  return instance.get('/word/like/cancel', { params: { postId: postId } })
}
```

#### 7.2.4. Реализация логики загрузки данных

Загрузка данных для списка постов осуществляется с использованием ленивой загрузки (ленивая загрузка). Вначале загружается только одна страница данных, а затем при достижении конца списка загружается следующая страница. После загрузки всех данных необходимо вывести уведомление, как показано на следующем рисунке:

![image-20240510153444447](https://pic.imgdb.cn/item/663ddb840ea9cb140398e14c.png)

Загрузка первой страницы данных зависит от состояния пользователя при запуске приложения. Если пользователь уже авторизован при запуске, то первая страница данных должна быть загружена до отображения компонента **CirclePage**. Если пользователь не авторизован при запуске, то загрузка первой страницы данных должна произойти после авторизации пользователя.Для реализации логики загрузки при достижении конца списка необходимо использовать событие `onReachEnd()` компонента **List**. Также необходимо определить два переменных: `page`, который представляет собой номер следующей страницы для загрузки, и `total`, который представляет собой общее количество записей, чтобы определить, завершена ли загрузка.

#### 7.2.5. Логика автоматического обновления после завершения

После завершения обновления страница автоматически перезагружается для отображения последних данных. Конкретные эффекты представлены ниже:

![recording](https://files.superbed.cn/static/images/a7/54/663dd77f0ea9cb140390a754.gif)

Для реализации этой функции необходимо, чтобы страница обновления знала о событии завершения обновления и могла запустить логику обновления. Событие можно уведомить с помощью **emitter**, который используется следующим образом:

**Импорт модуля emitter**

```arkts
import emitter from '@ohos.events.emitter';
```

**Отправка пользовательского события**

```arkts
let event = {
    eventId: 1, // Идентификатор события, определяется бизнес-логикой
    priority: emitter.EventPriority.LOW // Приоритет события
};

let eventData = {
    data: {
        "content": "c",
        "id": 1,
        "isEmpty": false,
    }
};

// Отправка события с идентификатором eventId равным 1 и данными eventData
emitter.emit(event, eventData);
```

**Подписка на пользовательское событие**

```arkts
// Определение события с идентификатором eventId равным 1
let event = {
    eventId: 1
};
```// Выполнение обратного вызова при получении события с идентификатором eventId равным 1
let callback = (eventData) => {
    console.info('event callback');
};

// Подписка на событие с идентификатором eventId равным 1
emitter.on(event, callback);

---

Обновление представления можно реализовать следующим образом:

![изображение-20240510153922292](https://pic.imgdb.cn/item/663ddbad0ea9cb14039932ba.png)

#### 7.2.6. Логика добавления/удаления лайков

Логика добавления и удаления лайков довольно проста. При выполнении действия необходимо изменить атрибуты `isLike` и `likeCount` и отправить запрос на добавление или удаление лайка на сервер.

#### 7.2.7. Логика возврата к верхней части страницы

Логика возврата к верхней части страницы также довольно проста. Необходимо привязать компонент List к Scroller и затем вызвать метод `scrollToIndex`.

#### 7.2.8. Логика ручного обновления

Ручное обновление можно использовать логику автоматического обновления, описанную выше.

## 8. Страница личного кабинета

### 8.1. Обзор

Функции личного кабинета включают вход/выход и просмотр личных записей. Ниже приведены состояния страницы при входе и выходе:

![изображение-20240510154055804](https://pic.imgdb.cn/item/663ddbc00ea9cb1403995454.png)

Ниже приведена страница личных записей. Важно отметить, что доступ к личным записям возможен только при входе в систему:

![изображение-20240510154112854](https://pic.imgdb.cn/item/663ddbd00ea9cb140399705c.png)### 8.2. Реализация

#### 8.2.1. Интеграция с сервером

Для личного кабинета требуется два интерфейса, представленные ниже:

```arkts
// Получение информации о залогиненном пользователе
export function info() {
  return instance.get('/word/user/info')
}

// Получение моих записей о входе
export function getMyPost(page: number, size: number) {
  return instance.get('/word/post/getMine', { params: { page: page, size: size } })
}
```

#### 8.2.2. Полный код

**Личный кабинет**

```arkts
import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
import { info } from '../http/Api';

@Component
export struct MinePage {
  @StorageLink('token') @Watch('onTokenChange') token: string = ''
  @State userInfo: {
    nickname?: string,
    avatarUrl?: string
  } = {};

  async onTokenChange() {
    if (this.token) {
      let response = await info()
      this.userInfo = response.data.data;
    } else {
      this.userInfo = {}
    }
  }

  async aboutToAppear() {
    if (this.token) {
      let response = await info()
      this.userInfo = response.data.data;
    }
  }

  build() {
    Stack() {
      Column() {
        Image(this.token ? this.userInfo.avatarUrl : $r('app.media.img_avatar'))
          .width(100)
          .height(100)
          .borderRadius(50)
          .margin({ top: 120 })
          .onClick(() => {
            router.pushUrl({ url: 'pages/LoginPage' })
          })

        Text(this.token ? this.userInfo.nickname : 'Не авторизован')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.Black)
          .margin({ top: 20 })
      }
    }
  }
}
```        if (!this.token) {
          Text('Пожалуйста, нажмите на аватар для авторизации')
            .fontSize(12)
            .fontWeight(FontWeight.Medium)
            .fontColor(Color.Black)
            .margin({ top: 4 })
        }
      }
      .width('100%')
      .height('50%')
      .backgroundImage(this.token ? this.userInfo.avatarUrl : $r('app.media.img_avatar'))
      .backgroundImageSize({ height: '100%', width: '100%' })
      .backgroundBlurStyle(BlurStyle.Regular)
```      Column({ space: 10 }) {
        this.mineItemBuilder($r('app.media.ic_mine_card'), 'Записи о входе', () => {
          if (this.token) {
            router.pushUrl({ url: 'pages/PostHistoryPage' })
          } else {
            promptAction.showToast({ message: 'Пожалуйста, войдите, нажав на аватар' })
          }
        })
        Divider()
        this.mineItemBuilder($r('app.media.ic_mine_update'), 'Проверить обновления', () => {
          promptAction.showToast({ message: 'Вы уже используете последнюю версию' })
        })
        Divider()
        this.mineItemBuilder($r('app.media.ic_mine_about'), 'О приложении', () => {
          promptAction.showToast({ message: 'Нет информации о приложении' })
        })```markdown
        Blank()```markdown
        если (this.token) {
          Button('Выйти из системы')
            .width('100%')
            .fontSize(18)
            .backgroundColor(Color.Gray)
            .fontColor(Color.White)
            .onClick(() => {
              this.token = ''
            })
        }

      }
      .width('100%')
      .height('60%')
      .offset({ y: '40%' })
      .borderRadius({ topLeft: 50, topRight: 50 })
      .backgroundColor(Color.White)
      .padding(30)

    }.width('100%')
    .height('100%')
    .alignContent(Alignment.Top)
  }

  @Builder
  mineItemBuilder(icon: Resource, title: string, callback?: () => void) {
    Row({ space: 10 }) {
      Image(icon)
        .width(24)
        .height(24)
      Text(title)
        .fontSize(16)
        .height(24)
        .fontWeight(FontWeight.Medium)
      Blank()
      Image($r('app.media.ic_arrow_right'))
        .width(24)
        .height(24)
    }.width('100%')
    .height(40)
    .onClick(() => {
      callback();
    })
  }
}
```

**История посещений**

```arkts
import { getMyPost } from '../http/Api';
import { PostInfo } from '../model/PostInfo';
import router from '@ohos.router';
import promptAction from '@ohos.promptAction';
import { convertMillisecondsToTime } from '../utils/DataUtil';

@Entry
@Component
struct PostHistoryPage {
  @State postInfoList: PostInfo[] = []
  page: number = 1;
  total: number = 0;

  onPageShow() {
    this.postInfoList = []
    this.page = 1
    this.total = 0
    this.getMyPostInfoList(this.page)
  }

  async getMyPostInfoList(page: number) {
    let response = await getMyPost(page, 10)
    response.data.data.records.forEach(post => this.postInfoList.push(new PostInfo(post)))
    this.total = response.data.data.total;
    this.page += 1;
  }

  build() {
    Column() {
      Row() {
        Image($r('app.media.ic_back'))
          .width(24)
          .height(24)
          .onClick(() => {
            router.back()
          })
        Text('История посещений')
          .fontSize(18)
          .fontWeight(FontWeight.Bold)
        Image($r('app.media.ic_back'))
          .width(24)
          .height(24)
          .visibility(Visibility.Hidden)
      }.width('100%')
      .height(40)
      .justifyContent(FlexAlign.SpaceBetween)
      .padding({ left: 20, right: 20 })
      Разделитель()
        .цвет(Color.Black)
        .отступ({ left: 20, right: 20 })
      .width('100%')
      .height(1)
      .backgroundColor(Color.Black)
    }
  }
}
```      if (this.postInfoList.length > 0) {
        this.listBuilder()
      } else {
        this.emptyBuilder()
      }

    }
    .height('100%')
    .width('100%')
    .padding({
      top: 40
    })
  }

  @Builder
  listBuilder() {
    List() {
      ForEach(this.postInfoList, (post) => {
        ListItem() {
          this.postItemBuilder(post)
        }.width('100%')
      })
    }
    .width('100%')
    .listWeight(1)
    .alignItem(ListItemAlign.Center)
    .onReachEnd(() => {
      if (this.postInfoList.length < this.total) {
        this.getMyPostInfoList(this.page)
      } else {
        promptAction.showToast({ message: 'Нет больше данных...' })
      }
    })
  }

  @Builder
  emptyBuilder() {
    Column() {
      Image($r('app.media.ic_empty'))
        .width(200)
        .height(200)
      Text('Нет данных')
        .fontSize(20)
        .fontWeight(FontWeight.Medium)
        .fontColor('#7e8892')
    }.width('100%')
    .listWeight(1)
    .align(FlexAlign.Center)
  }

  @Builder
  postItemBuilder(post: PostInfo) {
    Row() {
      Column({ space: 10 }) {
        Text(post.createTime)
          .fontSize(14)
          .fontColor('#999999')
          .height(21)
        Row() {
          Text('Количество слов : ' + post.answeredCount)
            .fontSize(14)
            .fontColor('#1C1C1C')
            .height(21)
            .padding({
              right: 20
            })
          Text('Точность : ' + (post.rightCount / post.answeredCount * 100).toFixed(0) + '%')
            .fontSize(14)
            .fontColor('#1C1C1C')
            .height(21)
        }

        Text('Время выполнения : ' + convertMillisecondsToTime(post.timeUsed))
          .fontSize(14)
          .fontColor('#1C1C1C')
          .height(21)
      }
    }
  }```markdown
## 9. Информация об приложении

### 9.1. Обзор

Основные изменения включают в себя иконку и название приложения, как показано на следующем рисунке.

![image-20240510154320264](https://pic.imgdb.cn/item/663ddbe50ea9cb1403999cd6.png)

### 9.2. Способ реализации

#### 9.2.1. Требуемые навыки

<div style="background-color: #f0f9eb; padding: 10px;">
1. Знание основных концепций модели Stage для приложений HarmonyOS
2. Знание конфигурационных файлов проекта, созданного на основе модели Stage
</div>

Вышеуказанный материал можно найти в главе 15 книги "Разработка приложений для HarmonyOS 4.0".

#### 9.2.2. Способ реализации

В приложениях HarmonyOS иконки запуска приложений на рабочем столе имеют гранулярность **UIAbility** и поддерживают наличие нескольких иконок запуска для одного приложения. При нажатии на иконку запускается соответствующий **UIAbility**. Поэтому иконки на рабочем столе должны быть настроены в соответствующем Ability в файле `module.json5`.
```Иконка приложения в настройках имеет гранулярность приложения, и каждое приложение может иметь только одну иконку. Иконка должна быть настроена в файле `app.json5`.

Комментарии ( 0 )

Вы можете оставить комментарий после Вход в систему

Введение

Проект实战 с HarmonyOS 4.0 — Вocabularly打卡, основная функция этого приложения заключается в помощи запоминанию слов, в основном это проект для начинающих, написанный по教程 от Шангу Сили. Развернуть Свернуть
Отмена

Обновления

Пока нет обновлений

Участники

все

Язык

Недавние действия

Загрузить больше
Больше нет результатов для загрузки
1
https://api.gitlife.ru/oschina-mirror/guo-tai-0602-english-practice.git
git@api.gitlife.ru:oschina-mirror/guo-tai-0602-english-practice.git
oschina-mirror
guo-tai-0602-english-practice
guo-tai-0602-english-practice
master