git clone https://gitee.com/guo-tai-0602/english-price.git
После загрузки проекта на локальную машину, используйте DevEco Studio
для открытия проекта. Приложение автоматически загрузит зависимости из директории oh_modules
. В данном проекте используется плагин axios
для выполнения HTTP-запросов.
В проекте включены собственные серверные службы, расположенные в директории HttpServer
. В этой директории есть версии для MacOS
и Windows
. Выберите соответствующую версию в зависимости от вашей операционной системы. В данном случае используется версия для Windows. После установки и запуска сервера, откроется окно, как показано на следующем рисунке:
Когда отображаются адреса интерфейсов, это означает, что серверные службы успешно запущены.
Внимание: В зависимости от используемой сети, адреса интерфейсов могут отличаться. Убедитесь, что вы изменяете значение baseURL
в фронтенде в соответствии с вашими реальными адресами интерфейсов.
Место изменения в фронтенде: entry/src/main/ets/http/Axios.ets
, как показано на следующем рисунке:
Все используемые API в данном проекте описаны в файле
entry/src/main/ets/http/Api.ets
. После экспорта, эти API можно импортировать и использовать на нужных страницах, что облегчает управление и изменение.
Основная функция данного приложения - помощь в запоминании английских слов. Оно состоит из трех модулей, как показано на следующем рисунке:
Страница с вопросами имеет три состояния: ответы, пауза и остановка. Начальное состояние - остановка, в котором нельзя отвечать на вопросы. При нажатии на варианты ответов, необходимо отображать подсказки, как показано на следующем рисунке:
В состоянии остановка можно изменять количество слов для тестирования (в других состояниях это невозможно). После изменения количества слов, необходимо заново выбрать соответствующее количество вопросов из базы данных.
Нажатие кнопки начать тестирование переводит страницу в состояние ответы, где начинается отсчет времени:
Ответственный процесс имеет логику, представленную ниже:
Во время ответа на вопросы требуется в реальном времени обновлять статистическую информацию, которая включает в себя прогресс и точность, как показано ниже:
Во время ответа на вопросы, нажатие кнопки Приостановить тест переводит процесс в приостановленное состояние, в котором таймер останавливает отсчет времени.
Повторное нажатие кнопки Начать тест возвращает процесс в состояние ответа на вопросы, при этом таймер возобновляет отсчет времени.
Когда все вопросы текущего теста будут завершены или нажата кнопка Завершить тест, процесс переходит в остановленное состояние, и появляется окно с результатами статистики, как показано ниже:
В этом случае:
1. Использование стандартных компонентов разметки: Column, Row и т.д.
2. Использование стандартных компонентов: Progress, Button, Image, Text, TextTimer (таймер) и т.д.
3. Создание пользовательских компонентов.
4. Создание пользовательских диалоговых окон.
5. Управление состоянием компонентов: @State, @Prop, @Link, @Watch и т.д.
Вышеуказанный контент можно найти в главах OnClickListener, 4 и 5 учебника по разработке приложений для HarmonyOS 4.0.
Основная разметка страницы ответов представлена ниже:
Стили компонентов представлены ниже| Компонент | Стиль | Эффект |
| ---------------- | :----------------------------------------------------------- | ------------------------------------------------------------ |
| Фон страницы | @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
Управление практическим состоянием осуществляется с помощью двух кнопок в нижней части интерфейса. Важно отметить, что стили кнопок также должны изменяться в зависимости от текущего состояния, как показано на следующих изображениях:
Практическое состояние | Остановка | Ответственный режим | Пауза |
---|---|---|---|
Эффект | ![]() |
![]() |
![]() |
Для более подробного понимания можно обратиться к следующему коду:
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: Создание компонента кнопки выбора**
Оценка правильности ответа требует изменения стиля кнопок выбора, которые имеют три различных стиля, как показано на следующем рисунке:

Учитывая вышеупомянутые стили, можно извлечь кнопку выбора в отдельный компонент и определить переменную состояния для управления стилем кнопки. Тип переменной состояния можно определить следующим перечислением:
```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>
Конкретная логика представлена на следующей схеме
<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-компонент**

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

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

###### 2.2.2.5.1. Точность
Для статистики **точности** необходимо определить два состояния: `answeredCount` и `rightCount`. Переменная `answeredCount` представляет количество ответов в текущем тесте, а `rightCount` — количество правильных ответов. Эти переменные обновляются после каждого ответа.
###### 2.2.2.5.2. Прогресс
Для статистики прогресса используются два состояния: `totalCount` и `answeredCount`, которые отображаются с помощью компонента **Progress**.
###### 2.2.2.5.3. Количество словКоличество слов отображается с помощью компонента кнопки **Button**. При нажатии на кнопку должен появляться текстовый выборщик, позволяющий выбрать количество слов для следующего теста. После выбора необходимо перезагрузить задания с указанным количеством слов. Стили кнопки можно увидеть в таблице ниже.
<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`.Структура модального окна представлена на следующем рисунке.

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

<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. Обзор
В этом разделе необходимо реализовать таб-разметку. Конкретный результат представлен ниже:

### 3.2. Реализация
#### 3.2.1. Необходимые навыки
**Tabs** компонент
#### 3.2.2. Процесс реализации
##### 3.2.2.1. Стили для табов
Стили для табов представлены в таблице ниже:

## 4. Страница приветствия
### 4.1. Обзор
Страница приветствия имеет относительно простую функциональность. Конкретный результат представлен ниже:
### 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. Базовая разметка и стили
Базовая разметка страницы приветствия представлена на следующем рисунке:

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

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

Как видно, данные анимации относятся к анимации перехода компонентов. Поэтому можно использовать метод `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-код, как показано на следующем рисунке.

### 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. Описание
После реализации функции входа в систему, можно реализовать функцию отметки присутствия после завершения тестирования.
В зависимости от текущего состояния авторизации, в окне результатов должны отображаться разные кнопки для отметки присутствия. Если пользователь авторизован, должна отображаться кнопка **Immediate Presence Marking**, в противном случае — кнопка **Authorization for Presence Marking**.
При нажатии на кнопку **Immediate Presence Marking** должна быть отправлена заявка на отметку присутствия, после чего происходит переход на страницу отметки присутствия. Конкретный процесс представлен на следующем рисунке.

При нажатии на кнопку **Authorization for Presence Marking** сначала происходит переход на страницу авторизации. После успешной авторизации отправляется заявка на отметку присутствия, после чего происходит переход на страницу отметки присутствия. Конкретный процесс представлен на следующем рисунке.
### 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. Обзор
Страница отметки присутствия используется для отображения всех записей отметки присутствия пользователей и предоставляет функцию лайка.

### 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` указывает, лайкнул ли текущий пользователь. В зависимости от этого свойства должны отображаться иконки лайка в разных цветах, как показано ниже.
При выполнении пользователем действия лайка или отмены лайка необходимо изменить значение `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: ложь
})]
```
Круг постов требует, чтобы пользователь был авторизован для доступа, поэтому необходимо отображать разные данные в зависимости от состояния авторизации, как показано ниже:

Состояние авторизации можно определить по **токену**
```arkts
@СвойствоХранилища('токен') токен: строка = ''
```
Основной фрейм страницы можно использовать следующий код в качестве примера:

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

**Авторизованный** пользователь видит следующий контент:
```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. Реализация логики загрузки данных
Загрузка данных для списка постов осуществляется с использованием ленивой загрузки (ленивая загрузка). Вначале загружается только одна страница данных, а затем при достижении конца списка загружается следующая страница. После загрузки всех данных необходимо вывести уведомление, как показано на следующем рисунке:

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

Для реализации этой функции необходимо, чтобы страница обновления знала о событии завершения обновления и могла запустить логику обновления. Событие можно уведомить с помощью **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);
---
Обновление представления можно реализовать следующим образом:

#### 7.2.6. Логика добавления/удаления лайков
Логика добавления и удаления лайков довольно проста. При выполнении действия необходимо изменить атрибуты `isLike` и `likeCount` и отправить запрос на добавление или удаление лайка на сервер.
#### 7.2.7. Логика возврата к верхней части страницы
Логика возврата к верхней части страницы также довольно проста. Необходимо привязать компонент List к Scroller и затем вызвать метод `scrollToIndex`.
#### 7.2.8. Логика ручного обновления
Ручное обновление можно использовать логику автоматического обновления, описанную выше.
## 8. Страница личного кабинета
### 8.1. Обзор
Функции личного кабинета включают вход/выход и просмотр личных записей. Ниже приведены состояния страницы при входе и выходе:

Ниже приведена страница личных записей. Важно отметить, что доступ к личным записям возможен только при входе в систему:
### 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. Обзор
Основные изменения включают в себя иконку и название приложения, как показано на следующем рисунке.

### 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 )