首次提交

This commit is contained in:
2026-07-13 12:06:16 +08:00
commit e3c810b1a6
1690 changed files with 228324 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
/node_modules
/oh_modules
/.preview
/build
/.cxx
/.test
+33
View File
@@ -0,0 +1,33 @@
{
"apiType": "stageMode",
"buildOption": {
"resOptions": {
"copyCodeResource": {
"enable": false
}
}
},
"buildOptionSet": [
{
"name": "release",
"arkOptions": {
"obfuscation": {
"ruleOptions": {
"enable": false,
"files": [
"./obfuscation-rules.txt"
]
}
}
}
},
],
"targets": [
{
"name": "default"
},
{
"name": "ohosTest",
}
]
}
+2
View File
@@ -0,0 +1,2 @@
// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently.
export { HapTasks } from '@ohos/hvigor-ohos-arkui-x-plugin';
+23
View File
@@ -0,0 +1,23 @@
# Define project specific obfuscation rules here.
# You can include the obfuscation configuration files in the current module's build-profile.json5.
#
# For more details, see
# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/source-obfuscation
# Obfuscation options:
# -disable-obfuscation: disable all obfuscations
# -enable-property-obfuscation: obfuscate the property names
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
# -compact: remove unnecessary blank spaces and all line feeds
# -remove-log: remove all console.* statements
# -print-namecache: print the name cache that contains the mapping from the old names to new names
# -apply-namecache: reuse the given cache file
# Keep options:
# -keep-property-name: specifies property names that you want to keep
# -keep-global-name: specifies names that you want to keep in the global scope
-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-filename-obfuscation
-enable-export-obfuscation
+10
View File
@@ -0,0 +1,10 @@
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {}
}
+161
View File
@@ -0,0 +1,161 @@
import { promptAction } from '@kit.ArkUI';
import DeviceListInterface from "../model/DeviceListInterface";
/**
* 添加页面组件 - 科技蓝风格
*/
@Component
export struct Add {
@State deviceCode: string = '';
@State deviceName: string = '';
// 确认添加设备
handleAddDevice(): void {
if (!this.deviceCode || !this.deviceName) {
promptAction.showToast({ message: '请填写完整信息' });
return;
}
// 创建新设备数据
const newDevice: DeviceListInterface = {
id: Date.now(),
name: this.deviceName,
code: this.deviceCode,
latitudeLongitude: '',
screenPosition: '',
status: 1 // 新添加的设备默认为正常状态
};
// 存储到AppStorage,供设备页面获取
AppStorage.setOrCreate('newDevice', newDevice);
promptAction.showToast({ message: '设备绑定成功' });
// 清空输入框
this.deviceCode = '';
this.deviceName = '';
}
build() {
Column() {
// 顶部标题栏 - 科技蓝风格
Row() {
Image($rawfile("add/添加 加号.png"))
.width(20)
.height(20)
.fillColor('#FFFFFF')
.margin({ left: 20, right: 10 })
Text("添加设备")
.fontSize(20)
.fontWeight(600)
.fontColor("#FFFFFF")
Blank()
Text('设备绑定')
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
}
.width("100%")
.height(60)
.padding({ left: 10, right: 20 })
.alignItems(VerticalAlign.Center)
// 表单区域
Column({ space: 16 }) {
// 设备编号输入
Column({ space: 8 }) {
Text('设备编号')
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
.width('100%')
Row() {
Image($rawfile("add/条形码.png"))
.width(18)
.height(18)
.fillColor('rgba(255,255,255,0.5)')
.margin({ left: 16 })
TextInput({ placeholder: "请输入设备编号", text: this.deviceCode })
.onChange((value: string) => {
this.deviceCode = value;
})
.backgroundColor(Color.Transparent)
.layoutWeight(1)
.placeholderColor('rgba(255,255,255,0.4)')
.fontColor('#FFFFFF')
.caretColor('#00FF88')
}
.width("100%")
.height(50)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
}
.width('88%')
.alignItems(HorizontalAlign.Start)
// 设备名称输入
Column({ space: 8 }) {
Text('设备名称')
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
.width('100%')
Row() {
Image($rawfile("add/编写.png"))
.width(18)
.height(18)
.fillColor('rgba(255,255,255,0.5)')
.margin({ left: 16 })
TextInput({ placeholder: "请输入设备名称", text: this.deviceName })
.onChange((value: string) => {
this.deviceName = value;
})
.backgroundColor(Color.Transparent)
.layoutWeight(1)
.placeholderColor('rgba(255,255,255,0.4)')
.fontColor('#FFFFFF')
.caretColor('#00FF88')
}
.width("100%")
.height(50)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
}
.width('88%')
.alignItems(HorizontalAlign.Start)
// 确认按钮
Button("确认绑定")
.width("70%")
.height(45)
.fontSize(16)
.fontColor('#FFFFFF')
.fontWeight(FontWeight.Bold)
.backgroundColor('rgba(0,255,136,0.25)')
.borderRadius(22)
.border({ width: 1, color: 'rgba(0,255,136,0.4)' })
.onClick(() => {
this.handleAddDevice();
})
// 提示信息
Text('绑定后可在设备列表中查看')
.fontSize(12)
.fontColor('rgba(255,255,255,0.4)')
.margin({ top: 12 })
}
.width("100%")
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,320 @@
import { webview } from '@kit.ArkWeb';
import { http } from '@kit.NetworkKit';
import CarbonInfoInterface from '../model/CarbonInfoInterface';
import StatisticalInterface from '../model/StatisticalInterface';
import DeviceListInterface from '../model/DeviceListInterface';
import { getCarbonInfo, getCarbonList, getDeviceList } from '../utils/ApiService';
// 监测点数据接口
interface MonitorPoint {
name: string;
lat: number;
lng: number;
status: number;
color: string;
}
/**
* 碳汇计算组件
*/
@Component
export struct CarbonCalc {
// 图表控制器
controller: webview.WebviewController = new webview.WebviewController();
// 状态数据
@State carbonInfo: CarbonInfoInterface | null = null;
@State carbonTrend: StatisticalInterface[] = [];
@State monitorPoints: MonitorPoint[] = [];
@State isLoading: boolean = true;
@State isWebViewReady: boolean = false;
@State avgTemp: number = 25.0;
// 模拟数据
private mockCarbonInfo: CarbonInfoInterface = {
carbonSequestration: 125.6,
ph: 7.4,
waterTemperature: 25.2
};
private mockTrend: number[] = [98, 105, 112, 118, 122, 126];
private mockPoints: MonitorPoint[] = [
{ name: '长江监测点A', lat: 30.5, lng: 114.3, status: 1, color: '#00FF88' },
{ name: '长江监测点B', lat: 31.2, lng: 115.1, status: 0, color: '#FF6B6B' },
{ name: '长江监测点C', lat: 29.8, lng: 113.5, status: 1, color: '#00FF88' },
{ name: '长江监测点D', lat: 32.0, lng: 118.7, status: 1, color: '#00FF88' }
];
aboutToAppear(): void {
this.loadData();
setTimeout(() => {
this.isWebViewReady = true;
this.updateChart();
}, 1000);
}
loadData(): void {
this.carbonInfo = this.mockCarbonInfo;
this.monitorPoints = this.mockPoints;
this.avgTemp = this.mockCarbonInfo.waterTemperature;
this.isLoading = false;
// 尝试获取真实数据
getCarbonInfo().then((result) => {
if (result && result.data) {
this.carbonInfo = result.data;
this.avgTemp = result.data.waterTemperature;
}
}).catch(() => {
// 使用模拟数据
});
getCarbonList().then((result) => {
if (result && result.data && result.data.length > 0) {
this.carbonTrend = result.data;
}
}).catch(() => {
// 使用模拟数据
});
getDeviceList().then((result) => {
if (result && result.data && result.data.length > 0) {
this.parseDeviceData(result.data);
}
}).catch(() => {
// 使用模拟数据
});
}
parseDeviceData(devices: DeviceListInterface[]): void {
const points: MonitorPoint[] = [];
for (let i: number = 0; i < devices.length; i++) {
const device: DeviceListInterface = devices[i];
const coords: string[] = device.latitudeLongitude.split(',');
if (coords.length >= 2) {
const lat: number = parseFloat(coords[0]);
const lng: number = parseFloat(coords[1]);
points.push({
name: device.name,
lat: lat,
lng: lng,
status: device.status,
color: device.status === 1 ? '#00FF88' : '#FF6B6B'
});
}
}
if (points.length > 0) {
this.monitorPoints = points;
}
}
updateChart(): void {
if (!this.isWebViewReady) return;
try {
const jsCode: string = `updateData([${this.mockTrend.join(',')}])`;
this.controller.runJavaScript(jsCode);
} catch (e) {
console.error('图表更新失败');
}
}
build() {
Column() {
// 标题
Row() {
Text('碳汇计算')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Blank()
Text('水体固碳能力')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.width('92%')
.padding({ top: 12, bottom: 8 })
// ===== 上部分:统计卡片 =====
Row({ space: 8 }) {
// 上月平均温度
Column({ space: 4 }) {
Text('上月平均温度')
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)')
Text(this.avgTemp.toFixed(1) + '°C')
.fontSize(18)
.fontWeight(600)
.fontColor('#FFE66D')
Row() {
Circle().width(4).height(4).fill('#FFE66D')
Text('正常')
.fontSize(9)
.fontColor('rgba(255,255,255,0.5)')
.margin({ left: 3 })
}
}
.width('31%')
.height(70)
.padding(8)
.borderRadius(12)
.border({ width: 1, color: 'rgba(255,230,109,0.3)' })
.backgroundColor('rgba(255,230,109,0.08)')
// 上月碳汇能力
Column({ space: 4 }) {
Text('上月碳汇能力')
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)')
Text((this.carbonInfo?.carbonSequestration ?? 0).toFixed(1) + 'kg')
.fontSize(18)
.fontWeight(600)
.fontColor('#00FF88')
Row() {
Circle().width(4).height(4).fill('#00FF88')
Text('良好')
.fontSize(9)
.fontColor('rgba(255,255,255,0.5)')
.margin({ left: 3 })
}
}
.width('31%')
.height(70)
.padding(8)
.borderRadius(12)
.border({ width: 1, color: 'rgba(0,255,136,0.3)' })
.backgroundColor('rgba(0,255,136,0.08)')
// 上月平均pH
Column({ space: 4 }) {
Text('上月平均pH')
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)')
Text((this.carbonInfo?.ph ?? 0).toFixed(1))
.fontSize(18)
.fontWeight(600)
.fontColor('#4ECDC4')
Row() {
Circle().width(4).height(4).fill('#4ECDC4')
Text('达标')
.fontSize(9)
.fontColor('rgba(255,255,255,0.5)')
.margin({ left: 3 })
}
}
.width('31%')
.height(70)
.padding(8)
.borderRadius(12)
.border({ width: 1, color: 'rgba(78,205,196,0.3)' })
.backgroundColor('rgba(78,205,196,0.08)')
}
.width('92%')
.justifyContent(FlexAlign.SpaceBetween)
// ===== 中间部分:碳汇趋势折线图 =====
Column({ space: 6 }) {
Row() {
Text('碳汇趋势')
.fontSize(13)
.fontWeight(500)
.fontColor('#FFFFFF')
Blank()
Text('近6个月')
.fontSize(10)
.fontColor('rgba(255,255,255,0.5)')
}
.width('100%')
Web({ src: $rawfile('chart/carbon_chart.html'), controller: this.controller })
.width('100%')
.height(160)
.backgroundColor('transparent')
.javaScriptAccess(true)
}
.width('92%')
.padding(10)
.margin({ top: 8 })
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.backgroundColor('rgba(13,40,71,0.5)')
// ===== 下部分:监测点分布地图 =====
Column({ space: 8 }) {
// 地图标题
Row() {
Text('监测点分布')
.fontSize(13)
.fontWeight(500)
.fontColor('#FFFFFF')
Blank()
Row({ space: 6 }) {
Row({ space: 3 }) {
Circle().width(5).height(5).fill('#00FF88')
Text('正常')
.fontSize(9)
.fontColor('rgba(255,255,255,0.6)')
}
Row({ space: 3 }) {
Circle().width(5).height(5).fill('#FF6B6B')
Text('异常')
.fontSize(9)
.fontColor('rgba(255,255,255,0.6)')
}
}
}
.width('100%')
// 监测点地图区域(图片占位符,后期添加图片后取消注释)
Image($rawfile('ces/all_point.png'))
.width('100%')
.height(180)
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
// 临时占位区域(待添加图片)
// Column()
// .width('100%')
// .height(180)
// .linearGradient({
// angle: 135,
// colors: [['#1a3a5c', 0], ['#0d2847', 1]]
// })
// .borderRadius(12)
// .border({ width: 1, color: 'rgba(22,119,255,0.3)' })
// 监测点列表
Row({ space: 10 }) {
ForEach(this.monitorPoints, (point: MonitorPoint) => {
Row({ space: 4 }) {
Circle()
.width(6)
.height(6)
.fill(point.color)
Text(point.name)
.fontSize(10)
.fontColor('rgba(255,255,255,0.8)')
}
.padding({ left: 6, right: 6, top: 4, bottom: 4 })
.borderRadius(8)
.backgroundColor('rgba(22,119,255,0.1)')
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('92%')
.layoutWeight(1)
.margin({ top: 8 })
.padding(10)
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.backgroundColor('rgba(13,40,71,0.4)')
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,457 @@
import { webview } from '@kit.ArkWeb';
import { buffer, JSON } from '@kit.ArkTS';
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { getContrast } from '../utils/ApiService';
import StatisticalInterface from '../model/StatisticalInterface';
interface IndicatorConfig {
name: string;
unit: string;
color: string;
range: string;
data: number[];
paramType: number;
}
interface DifyStreamChunk {
event?: string;
answer?: string;
conversation_id?: string;
message?: string;
}
interface DifyChatRequestBody {
inputs: Record<string, string>;
query: string;
response_mode: 'streaming';
conversation_id: string;
user: string;
}
interface DifyErrorResponse {
message: string;
code?: number;
}
interface StatisticalItem {
month: string;
value: number;
}
@Component
export struct ContrastAnalysis {
controller: webview.WebviewController = new webview.WebviewController();
@State currentIndicator: number = 0;
@State aiMessage: string = '';
@State isGenerating: boolean = false;
@State conversationId: string = '';
@State isWebReady: boolean = false;
@State isComponentActive: boolean = true;
private httpRequest: http.HttpRequest | null = null;
private sseBuffer: string = '';
private currentRequestIndicator: number = -1;
private readonly DIFY_BASE_URL: string = 'http://192.168.31.26';
private readonly DIFY_API_KEY: string = 'app-OeXDz5qppFhB4JMU9PQBEJai';
private indicators: IndicatorConfig[] = [
{ name: '水温', unit: '°C', color: '#FFE66D', range: '15-30', data: [], paramType: 1 },
{ name: 'COD', unit: 'mg/L', color: '#00FF88', range: '≤40', data: [], paramType: 2 },
{ name: 'pH', unit: 'pH', color: '#4ECDC4', range: '6.5-8.5', data: [], paramType: 3 },
{ name: '余氯', unit: 'mg/L', color: '#F38181', range: '0.3-0.5', data: [], paramType: 4 },
{ name: '电导率', unit: 'μS/cm', color: '#95E1D3', range: '500-1500', data: [], paramType: 5 },
{ name: '氨氮', unit: 'mg/L', color: '#FF6B6B', range: '≤1.0', data: [], paramType: 6 }
];
aboutToAppear(): void {
this.isComponentActive = true;
this.aiMessage = '点击"AI分析"按钮,将自动分析当前选中的指标数据。';
this.isWebReady = false;
this.currentRequestIndicator = -1;
}
aboutToDisappear(): void {
this.isComponentActive = false;
this.isWebReady = false;
this.cancelRequest();
}
cancelRequest(): void {
this.isGenerating = false;
this.destroyRequest();
if (this.aiMessage && this.aiMessage !== '等待分析...') {
this.aiMessage += '\n[已取消请求]';
} else {
this.aiMessage = '已取消请求';
}
}
private destroyRequest(): void {
if (this.httpRequest) {
try {
this.httpRequest.destroy();
} catch (e) {
console.error('销毁HTTP请求失败', JSON.stringify(e));
}
this.httpRequest = null;
}
this.sseBuffer = '';
}
private runChartScript(script: string): void {
if (!this.isComponentActive || !this.isWebReady) {
return;
}
try {
this.controller.runJavaScript(script);
console.info('执行图表脚本:' + script);
} catch (e) {
console.error('图表脚本执行失败', JSON.stringify(e));
}
}
private buildChartDataFromContrast(rawList: StatisticalInterface[]): StatisticalItem[] {
return rawList.map((item: StatisticalInterface): StatisticalItem => ({
month: `${item.type}${item.date}月`,
value: Number(item.value) || 0
}));
}
async loadChartData(): Promise<void> {
const targetIndicator: number = this.currentIndicator;
this.currentRequestIndicator = targetIndicator;
try {
const config: IndicatorConfig = this.indicators[targetIndicator];
console.info('请求指标 paramType' + config.paramType);
const res = await getContrast(config.paramType);
if (this.currentIndicator !== targetIndicator || !this.isComponentActive || !this.isWebReady) {
return;
}
const rawList: StatisticalInterface[] = res.data || [];
const chartData: StatisticalItem[] = this.buildChartDataFromContrast(rawList);
config.data = chartData.map((item: StatisticalItem): number => item.value);
this.runChartScript(`initChart(${JSON.stringify(chartData)})`);
} catch (err) {
console.error('加载图表数据失败', JSON.stringify(err));
} finally {
if (this.currentRequestIndicator === targetIndicator) {
this.currentRequestIndicator = -1;
}
}
}
onIndicatorChange(index: number): void {
this.currentIndicator = index;
this.loadChartData();
}
sendAiRequest(): void {
if (this.isGenerating || !this.isComponentActive) {
return;
}
const indicator: IndicatorConfig = this.indicators[this.currentIndicator];
const data: number[] = indicator.data;
if (!data.length) {
this.aiMessage = '暂无数据,无法分析';
return;
}
const avg: number = data.reduce((a: number, b: number): number => a + b, 0) / data.length;
const max: number = Math.max(...data);
const min: number = Math.min(...data);
const prompt: string =
`分析${indicator.name}指标年度数据:平均值${avg.toFixed(2)}${indicator.unit},最高${max.toFixed(2)}${indicator.unit},最低${min.toFixed(2)}${indicator.unit},标准${indicator.range}。简要分析趋势并给出建议。`;
this.isGenerating = true;
this.aiMessage = '';
this.requestDifyInStream(prompt);
}
private bufferToStr(src: ArrayBuffer, encoding: buffer.BufferEncoding = 'utf-8'): string {
const buf: buffer.Buffer = buffer.from(src);
return buf.toString(encoding);
}
private requestDifyInStream(query: string): void {
if (!this.DIFY_API_KEY.trim() || !this.isComponentActive) {
this.aiMessage = 'Dify API Key 未配置';
this.isGenerating = false;
return;
}
this.destroyRequest();
this.httpRequest = http.createHttp();
this.sseBuffer = '';
const url: string = this.DIFY_BASE_URL.replace(/\/+$/, '') + '/v1/chat-messages';
const body: DifyChatRequestBody = {
inputs: {
"code": 'DEVICE0121',
"oldMonth": `${new Date().getFullYear() - 1}年${new Date().getMonth().toString().padStart(2, '0')}月`,
"newMonth": `${new Date().getFullYear()}年${new Date().getMonth().toString().padStart(2, '0')}月`
},
query: query,
user: 'qyl',
response_mode: 'streaming',
conversation_id: this.conversationId || ''
};
let isSseResponse: boolean = false;
let rawNonSse: string = '';
this.aiMessage = '连接中...';
this.httpRequest.on('headersReceive', (headers: Object): void => {
if (!this.isComponentActive) {
return;
}
try {
const headerStr = JSON.stringify(headers).toLowerCase();
isSseResponse = headerStr.includes('text/event-stream');
} catch (e) {
console.error('解析响应头失败', JSON.stringify(e));
}
});
this.httpRequest.on('dataReceive', (data: ArrayBuffer): void => {
if (!this.isGenerating || !this.isComponentActive) {
return;
}
const chunk: string = this.bufferToStr(data);
if (isSseResponse) {
this.sseBuffer += chunk;
const events: string[] = this.sseBuffer.split('\n\n');
this.sseBuffer = events.pop() || '';
for (const event of events) {
const lines: string[] = event.split('\n');
let payload: string = '';
for (const line of lines) {
const trimLine: string = line.trim();
if (trimLine.startsWith('data:')) {
payload += trimLine.replace(/^data:\s*/, '');
}
}
if (!payload || payload === '[DONE]') {
continue;
}
try {
const js = JSON.parse(payload) as DifyStreamChunk;
if (js.event === 'message' || js.event === 'agent_message') {
if (this.aiMessage === '连接中...') {
this.aiMessage = '';
}
this.aiMessage += js.answer || '';
if (js.conversation_id) {
this.conversationId = js.conversation_id;
}
} else if (js.event === 'message_end') {
this.isGenerating = false;
} else if (js.event === 'error') {
this.aiMessage = 'Dify错误: ' + (js.message || '未知错误');
this.isGenerating = false;
}
} catch (e) {
console.error('解析SSE数据失败', JSON.stringify(e));
}
}
} else {
rawNonSse += chunk;
}
});
this.httpRequest.on('dataEnd', (): void => {
if (!this.isComponentActive) {
return;
}
if (!isSseResponse && rawNonSse.trim().length > 0) {
try {
const errJson = JSON.parse(rawNonSse) as DifyErrorResponse;
this.aiMessage = '错误:' + (errJson.message || rawNonSse);
} catch (e) {
this.aiMessage = rawNonSse;
}
} else if (this.aiMessage === '连接中...') {
this.aiMessage = '未收到有效数据';
}
this.isGenerating = false;
this.destroyRequest();
});
this.httpRequest.requestInStream(url, {
method: http.RequestMethod.POST,
header: {
Authorization: 'Bearer ' + this.DIFY_API_KEY,
'Content-Type': 'application/json'
},
extraData: JSON.stringify(body),
connectTimeout: 60000,
readTimeout: 120000
}).catch((err: BusinessError): void => {
if (!this.isComponentActive) {
return;
}
this.aiMessage = '网络错误: ' + err.message;
this.isGenerating = false;
this.destroyRequest();
});
}
build() {
Column() {
Row() {
Text('对比分析')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF');
Blank();
Text('历年趋势')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)');
}
.width('92%')
.padding({ top: 12, bottom: 8 });
Column({ space: 10 }) {
Row() {
Row({ space: 6 }) {
Text(this.indicators[this.currentIndicator].name)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(this.indicators[this.currentIndicator].color);
Text('单位: ' + this.indicators[this.currentIndicator].unit)
.fontSize(11)
.fontColor('rgba(255,255,255,0.6)');
}
Blank();
Row({ space: 4 }) {
Button() {
Text('◀')
.fontSize(8)
.fontColor('#FFFFFF');
}
.width(20)
.height(20)
.onClick(() => {
this.onIndicatorChange(
this.currentIndicator > 0 ? this.currentIndicator - 1 : this.indicators.length - 1
);
});
Button() {
Text('▶')
.fontSize(8)
.fontColor('#FFFFFF');
}
.width(20)
.height(20)
.onClick(() => {
this.onIndicatorChange(
this.currentIndicator < this.indicators.length - 1 ? this.currentIndicator + 1 : 0
);
});
}
}
Row() {
Text('标准范围: ')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)');
Text(this.indicators[this.currentIndicator].range)
.fontSize(11)
.fontColor(this.indicators[this.currentIndicator].color);
}
.justifyContent(FlexAlign.Center);
Web({
src: $rawfile('chart/contrast_chart.html'),
controller: this.controller
})
.height(210)
.width('100%')
.javaScriptAccess(true)
.onPageBegin(() => {
this.isWebReady = false;
})
.onPageEnd(() => {
this.isWebReady = true;
this.loadChartData();
});
}
.width('92%')
.padding(12)
.borderRadius(14)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.backgroundColor('rgba(13,40,71,0.55)');
Column({ space: 8 }) {
Row() {
Row({ space: 6 }) {
Text('AI智能分析')
.fontSize(13)
.fontColor('#FFFFFF');
}
Blank();
Circle()
.width(5)
.height(5)
.fill(this.isGenerating ? '#00FF88' : '#1677FF');
}
Button(this.isGenerating ? '停止' : 'AI分析')
.width('100%')
.height(34)
.borderRadius(17)
.backgroundColor(this.isGenerating ? 'rgba(255,77,79,0.25)' : 'rgba(0,255,136,0.15)')
.fontColor('#FFFFFF')
.onClick(() => {
this.isGenerating ? this.cancelRequest() : this.sendAiRequest();
});
Scroll() {
Text(this.aiMessage || '等待分析...')
.fontSize(12)
.fontColor('rgba(255,255,255,0.9)')
.lineHeight(18)
.width('100%')
.padding(10);
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
.borderRadius(10)
.backgroundColor('rgba(22,119,255,0.08)');
}
.width('92%')
.layoutWeight(1)
.margin({ top: 8 })
.padding(12)
.borderRadius(14)
.backgroundColor('rgba(13,40,71,0.4)');
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
});
}
}
@@ -0,0 +1,581 @@
import { getAlarmList, getAlarmStatistics } from '../utils/ApiService';
import AlarmItemInterface from '../model/AlarmItemInterface';
import AlarmStatisticsInterface from '../model/AlarmStatisticsInterface';
import DeviceListInterface from '../model/DeviceListInterface';
/**
* 设备报警组件
* 上部为地图区域,下部为设备报警列表
*/
@Component
export struct DeviceAlarm {
// 报警列表数据
@State alarmList: AlarmItemInterface[] = [];
// 报警统计数据
@State alarmStatistics: AlarmStatisticsInterface | null = null;
// 加载状态
@State isLoading: boolean = true;
// 当前设备信息
private currentDevice: DeviceListInterface | null = null;
// 模拟报警数据(用于演示)
private mockAlarmList: AlarmItemInterface[] = [
{
deviceId: 1,
id: 1,
msg: 'pH值超标',
value: '9.2',
standard: '6.5-8.5',
coordinate: '30.5,114.3',
screenPosition: '',
createTime: '2026-05-01 14:32:00',
name: '长江监测点A',
code: 'DEVICE001'
},
{
deviceId: 2,
id: 2,
msg: '氨氮超标',
value: '1.8',
standard: '≤1.0',
coordinate: '31.2,115.1',
screenPosition: '',
createTime: '2026-05-01 10:15:00',
name: '长江监测点B',
code: 'DEVICE002'
},
{
deviceId: 1,
id: 3,
msg: 'COD超标',
value: '58',
standard: '≤40',
coordinate: '30.5,114.3',
screenPosition: '',
createTime: '2026-04-30 16:45:00',
name: '长江监测点A',
code: 'DEVICE001'
},
{
deviceId: 3,
id: 4,
msg: '电导率异常',
value: '1850',
standard: '500-1500',
coordinate: '29.8,113.5',
screenPosition: '',
createTime: '2026-04-30 08:20:00',
name: '长江监测点C',
code: 'DEVICE003'
},
{
deviceId: 2,
id: 5,
msg: '水温异常',
value: '35.5',
standard: '15-30',
coordinate: '31.2,115.1',
screenPosition: '',
createTime: '2026-04-29 11:30:00',
name: '长江监测点B',
code: 'DEVICE002'
}
];
// 模拟统计数据
private mockStatistics: AlarmStatisticsInterface = {
todayCount: '3',
todayTrend: '1',
todayPercentage: '15',
weekCount: '12',
weekTrend: '0',
weekPercentage: '8',
monthCount: '28',
monthTrend: '1',
monthPercentage: '12'
};
// 科技蓝主色调
private primaryColor: string = '#1677FF';
aboutToAppear(): void {
// 从AppStorage获取当前设备信息
const device = AppStorage.get<DeviceListInterface>('point');
this.currentDevice = device ?? null;
// 加载报警数据
this.loadAlarmData();
}
// 加载报警数据
async loadAlarmData(): Promise<void> {
try {
// 获取报警列表
const listResult = await getAlarmList();
if (listResult && listResult.data && listResult.data.length > 0) {
this.alarmList = listResult.data;
} else {
// 如果API无数据,使用模拟数据
this.alarmList = this.mockAlarmList;
}
// 获取报警统计
const statsResult = await getAlarmStatistics();
if (statsResult && statsResult.data) {
this.alarmStatistics = statsResult.data;
} else {
// 如果API无数据,使用模拟数据
this.alarmStatistics = this.mockStatistics;
}
} catch (error) {
console.error('加载报警数据失败:', error);
// 加载失败时使用模拟数据
this.alarmList = this.mockAlarmList;
this.alarmStatistics = this.mockStatistics;
} finally {
this.isLoading = false;
}
}
// 计算水质超标数量
getWaterQualityAlarmCount(): number {
const keywords = ['pH', '氨氮', 'COD', '电导率', '水温', '余氯', '溶解氧'];
let count = 0;
this.alarmList.forEach((alarm: AlarmItemInterface) => {
if (alarm.msg && keywords.some(keyword => alarm.msg.includes(keyword))) {
count++;
}
});
return count;
}
// 格式化时间显示
formatTime(createTime: string): string {
if (!createTime) return '未知时间';
if (createTime.includes('-')) {
const parts = createTime.split(' ');
if (parts.length >= 2) {
const datePart = parts[0].substring(5);
const timePart = parts[1].substring(0, 5);
return `${datePart} ${timePart}`;
}
}
return createTime;
}
// 获取趋势图标
getTrendIcon(trend: string): string {
if (trend === '1') return '↑';
if (trend === '0') return '↓';
return '=';
}
// 获取趋势颜色
getTrendColor(trend: string): string {
if (trend === '1') return '#FF4D4F';
if (trend === '0') return '#52C41A';
return '#FFFFFF';
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 科技蓝渐变背景
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.4], ['#1677FF', 1]]
})
// 内容区域
Column({ space: 16 }) {
// 标题区域
Row() {
Text("设备报警")
.fontSize(18)
.fontWeight(600)
.fontColor('#FFFFFF')
Blank()
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill(this.primaryColor)
Text(`${this.alarmList.length} 条报警`)
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
}
}
.width('92%')
.padding({ top: 16, bottom: 8 })
// ===== 报警数量统计卡片 =====
Row({ space: 10 }) {
// 总报警数量
Column({ space: 6 }) {
Row({ space: 6 }) {
Image($rawfile('Equipment/设备管理.png'))
.width(16)
.height(16)
.fillColor('#FFFFFF')
Text("报警数量")
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
}
Text(`${this.alarmList.length}`)
.fontSize(28)
.fontWeight(700)
.fontColor('#FFFFFF')
}
.width('48%')
.height(80)
.backgroundColor('rgba(22,119,255,0.3)')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,255,255,0.2)')
// 水质超标数量
Column({ space: 6 }) {
Row({ space: 6 }) {
Column()
.width(16)
.height(16)
.borderRadius(8)
.backgroundColor('#FF4D4F')
Text("水质超标")
.fontSize(12)
.fontColor('rgba(255,255,255,0.8)')
}
Text(`${this.getWaterQualityAlarmCount()}`)
.fontSize(28)
.fontWeight(700)
.fontColor('#FFFFFF')
}
.width('48%')
.height(80)
.backgroundColor('rgba(255,77,79,0.2)')
.borderRadius(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,77,79,0.3)')
}
.width('92%')
// ===== 地图区域 =====
Column() {
Stack({ alignContent: Alignment.Center }) {
// 地图背景 - 科技蓝风格
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 135,
colors: [['#1a3a5c', 0], ['#0d2847', 1]]
})
// 模拟地图网格线
Column() {
ForEach([0, 1, 2, 3, 4], (row: number) => {
Row() {
ForEach([0, 1, 2, 3, 4, 5], (col: number) => {
Column()
.width('16%')
.height('20%')
.borderWidth(0.5)
.borderColor('rgba(22,119,255,0.2)')
})
}
.width('100%')
.height('20%')
})
}
.width('100%')
.height('100%')
// 设备位置标记点
if (this.currentDevice) {
Column({ space: 4 }) {
Stack({ alignContent: Alignment.Center }) {
Column()
.width(40)
.height(40)
.borderRadius(20)
.backgroundColor('rgba(22,119,255,0.4)')
Column()
.width(20)
.height(20)
.borderRadius(10)
.backgroundColor(this.primaryColor)
Column()
.width(8)
.height(8)
.borderRadius(4)
.backgroundColor('#FFFFFF')
}
Column() {
Text(this.currentDevice.name || '监测点')
.fontSize(10)
.fontColor('#FFFFFF')
.fontWeight(500)
}
.backgroundColor('rgba(22,119,255,0.8)')
.borderRadius(4)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
}
.position({ x: '45%', y: '40%' })
}
// 坐标信息
Column() {
Row({ space: 8 }) {
Image($rawfile('Equipment/设备管理.png'))
.width(14)
.height(14)
.fillColor('#FFFFFF')
if (this.currentDevice) {
Text(`坐标: ${this.currentDevice.latitudeLongitude || '未设置'}`)
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
} else {
Text('坐标: 未获取')
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
}
}
}
.position({ x: 12, y: 12 })
}
.width('92%')
.height('28%')
.borderRadius(12)
.clip(true)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.3)')
}
.width('92%')
// ===== 时间段统计 =====
if (this.alarmStatistics) {
Row({ space: 10 }) {
this.StatisticsCard('今日', this.alarmStatistics.todayCount,
this.alarmStatistics.todayTrend, this.alarmStatistics.todayPercentage)
this.StatisticsCard('本周', this.alarmStatistics.weekCount,
this.alarmStatistics.weekTrend, this.alarmStatistics.weekPercentage)
this.StatisticsCard('本月', this.alarmStatistics.monthCount,
this.alarmStatistics.monthTrend, this.alarmStatistics.monthPercentage)
}
.width('92%')
}
// ===== 报警列表 =====
Column() {
Row() {
Text("报警记录")
.fontSize(14)
.fontWeight(500)
.fontColor('#333333')
Blank()
Text(`共${this.alarmList.length}条`)
.fontSize(12)
.fontColor('#999999')
}
.width('100%')
.padding({ left: 14, right: 14, top: 12, bottom: 8 })
if (this.isLoading) {
Column() {
Text("加载中...")
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height(150)
.justifyContent(FlexAlign.Center)
} else if (this.alarmList.length === 0) {
Column() {
Image($rawfile('Equipment/设备管理.png'))
.width(40)
.height(40)
.fillColor('#CCCCCC')
Text("暂无报警记录")
.fontSize(14)
.fontColor('#999999')
.margin({ top: 8 })
}
.width('100%')
.height(150)
.justifyContent(FlexAlign.Center)
} else {
Scroll() {
Column({ space: 12 }) {
ForEach(this.alarmList, (alarm: AlarmItemInterface, index: number) => {
this.AlarmCard(alarm, index)
})
}
.width('100%')
.padding({ left: 14, right: 14, top: 8, bottom: 16 })
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Auto)
.edgeEffect(EdgeEffect.Spring)
}
}
.width('92%')
.layoutWeight(1)
.backgroundColor('#FFFFFF')
.borderRadius({ topLeft: 12, topRight: 12 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.width('100%')
.height('100%')
}
@Builder
StatisticsCard(title: string, count: string, trend: string, percentage: string) {
Column({ space: 6 }) {
Text(title)
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
Row({ space: 4 }) {
Text(count)
.fontSize(18)
.fontWeight(600)
.fontColor('#FFFFFF')
Text(this.getTrendIcon(trend))
.fontSize(12)
.fontColor(this.getTrendColor(trend))
}
Text(`${percentage}%`)
.fontSize(10)
.fontColor('rgba(255,255,255,0.5)')
}
.width('31%')
.height(60)
.backgroundColor('rgba(255,255,255,0.1)')
.borderRadius(8)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,255,255,0.2)')
}
@Builder
AlarmCard(alarm: AlarmItemInterface, index: number) {
Column({ space: 10 }) {
Row() {
Row({ space: 4 }) {
Image($rawfile('Equipment/设备管理.png'))
.width(12)
.height(12)
.fillColor(this.primaryColor)
Text(alarm.code || '未知设备')
.fontSize(12)
.fontWeight(500)
.fontColor(this.primaryColor)
}
.backgroundColor('#E6F4FF')
.borderRadius(4)
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
Blank()
Row({ space: 4 }) {
Text("掉线时间:")
.fontSize(11)
.fontColor('#999999')
Text(this.formatTime(alarm.createTime))
.fontSize(11)
.fontWeight(500)
.fontColor('#666666')
}
}
.width('100%')
Column({ space: 6 }) {
Row({ space: 8 }) {
Text("水质情况:")
.fontSize(12)
.fontColor('#333333')
Blank()
Text(alarm.msg || '异常')
.fontSize(13)
.fontWeight(500)
.fontColor('#FF4D4F')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
}
.width('100%')
Row({ space: 12 }) {
Column({ space: 4 }) {
Text("当前值")
.fontSize(10)
.fontColor('#999999')
Text(alarm.value || '-')
.fontSize(14)
.fontWeight(600)
.fontColor('#FF4D4F')
}
.alignItems(HorizontalAlign.Start)
Column()
.width(1)
.height(30)
.backgroundColor('#E5E5E5')
Column({ space: 4 }) {
Text("标准值")
.fontSize(10)
.fontColor('#999999')
Text(alarm.standard || '-')
.fontSize(14)
.fontWeight(600)
.fontColor('#52C41A')
}
.alignItems(HorizontalAlign.Start)
Blank()
Text(`#${index + 1}`)
.fontSize(10)
.fontColor('#CCCCCC')
}
.width('100%')
}
.width('100%')
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding(12)
}
.width('100%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.padding(14)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.15)')
.shadow({
radius: 4,
color: 'rgba(0,0,0,0.05)',
offsetX: 0,
offsetY: 2
})
}
}
+151
View File
@@ -0,0 +1,151 @@
import { router } from "@kit.ArkUI";
import DeviceListInterface from "../model/DeviceListInterface";
/**
* 设备页面组件 - 科技蓝风格
*/
@Component
export struct Equipment {
// 设备列表数据
@State deviceList: DeviceListInterface[] = [
{
id: 1,
name: '长江监测点A',
code: 'DEVICE001',
latitudeLongitude: '30.5,114.3',
screenPosition: '',
status: 1 // 正常
},
{
id: 2,
name: '长江监测点B',
code: 'DEVICE002',
latitudeLongitude: '31.2,115.1',
screenPosition: '',
status: 0 // 异常
}
];
aboutToAppear(): void {
// 从AppStorage获取新添加的设备
const newDevice = AppStorage.get<DeviceListInterface>('newDevice');
if (newDevice) {
// 添加到设备列表
this.deviceList = [
{
id: 1,
name: '长江监测点A',
code: 'DEVICE001',
latitudeLongitude: '30.5,114.3',
screenPosition: '',
status: 1
},
{
id: 2,
name: '长江监测点B',
code: 'DEVICE002',
latitudeLongitude: '31.2,115.1',
screenPosition: '',
status: 0
},
newDevice
];
// 清除临时存储
AppStorage.delete('newDevice');
}
}
build() {
Column() {
// 顶部标题栏 - 科技蓝风格
Row() {
Image($rawfile("Equipment/设备管理.png"))
.width(20)
.height(20)
.fillColor('#FFFFFF')
.margin({ left: 20, right: 10 })
Text("设备管理")
.fontSize(20)
.fontWeight(600)
.fontColor("#FFFFFF")
Blank()
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill('#00FF88')
Text(`${this.deviceList.length} 台`)
.fontSize(12)
.fontColor('rgba(255,255,255,0.7)')
}
}
.width("100%")
.height(60)
.padding({ left: 10, right: 20 })
.alignItems(VerticalAlign.Center)
// 设备列表区域
Column({ space: 12 }) {
ForEach(this.deviceList, (device: DeviceListInterface) => {
Row() {
Column() {
Row({ space: 10 }) {
Text(device.name)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text(device.code)
.fontSize(12)
.fontColor('rgba(255,255,255,0.6)')
}
Row({ space: 6 }) {
Circle()
.width(6)
.height(6)
.fill(device.status === 1 ? '#00FF88' : '#FF6B6B')
Text(device.status === 1 ? "正常运行中" : "异常报警")
.fontColor(device.status === 1 ? '#00FF88' : '#FF6B6B')
.fontSize(12)
}
.margin({ top: 8 })
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 16 })
Blank()
Image($rawfile("Equipment/右括号.png"))
.width(18)
.height(18)
.fillColor('rgba(255,255,255,0.5)')
.margin({ right: 16 })
}
.width("92%")
.height(80)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
// 存储当前设备信息供详情页使用(使用point作为key,与ApiService保持一致)
AppStorage.setOrCreate('point', device);
router.pushUrl({ url: 'pages/Details' });
})
})
}
.width("100%")
.layoutWeight(1)
.padding({ top: 16 })
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,354 @@
import { webview } from '@kit.ArkWeb';
import MqttService, { WaterQualityData, SimpleEmitterUtil } from '../utils/MqttService';
// 水质指标数据接口
interface IndicatorData {
name: string;
value: number;
unit: string;
color: string;
bgColor: string;
minValue: number;
maxValue: number;
normalRange: string;
}
/**
* 水情检测组件
*/
@Component
export struct WaterMonitor {
chartWebviewController: webview.WebviewController = new webview.WebviewController();
private mqttService: MqttService = MqttService.getInstance();
private refreshTimer: number = -1;
private chartUpdateTimer: number = -1;
// 最新MQTT数据缓冲
private latestData: WaterQualityData = {
temperature: 0, ph: 0, conductivity: 0,
cod: 0, ammonia: 0, chlorine: 0, timestamp: 0
};
// 当前UI显示的水质数据
@State currentData: WaterQualityData = {
temperature: 25.5, ph: 7.2, conductivity: 850,
cod: 28.5, ammonia: 0.35, chlorine: 0.42, timestamp: Date.now()
};
// 连接状态
@State isConnected: boolean = false;
// 是否有真实数据
@State hasRealData: boolean = false;
// WebView是否加载完成
@State isWebViewReady: boolean = false;
// 基本水质指标配置
private basicIndicators: IndicatorData[] = [
{ name: '水温', value: 0, unit: '°C', color: '#1677FF', bgColor: '#E6F4FF', minValue: 0, maxValue: 40, normalRange: '15-30°C' },
{ name: 'pH值', value: 0, unit: '', color: '#52C41A', bgColor: '#F6FFED', minValue: 0, maxValue: 14, normalRange: '6.5-8.5' },
{ name: '电导率', value: 0, unit: 'μS/cm', color: '#722ED1', bgColor: '#F9F0FF', minValue: 0, maxValue: 2000, normalRange: '500-1500' },
{ name: 'COD', value: 0, unit: 'mg/L', color: '#13C2C2', bgColor: '#E6FFFB', minValue: 0, maxValue: 100, normalRange: '≤40' }
];
aboutToAppear(): void {
// 配置MQTT连接参数
this.mqttService.setConfig('tcp://192.168.31.100:1883', 'sea_status');
// 使用SimpleEmitterUtil订阅MQTT消息
SimpleEmitterUtil.on((payload: string) => {
if (payload) {
this.latestData = this.mqttService.parseWaterQualityData(payload);
this.isConnected = true;
this.hasRealData = true;
}
});
// 异步连接MQTT
this.mqttService.connect().then(() => {
this.isConnected = this.mqttService.getConnectionStatus();
});
// 每4秒刷新一次UI数据
this.refreshTimer = setInterval(() => {
if (this.hasRealData) {
// 使用真实MQTT数据
this.currentData = this.latestData;
} else {
// 使用模拟数据
try {
}catch (e) {
console.error("mqtt_err:", JSON.stringify(e))
}
}
}, 4000);
// 每5秒更新图表(延迟启动,等待WebView加载)
setTimeout(() => {
this.startChartUpdate();
}, 2000);
}
aboutToDisappear(): void {
// 清除定时器
if (this.refreshTimer >= 0) {
clearInterval(this.refreshTimer);
this.refreshTimer = -1;
}
if (this.chartUpdateTimer >= 0) {
clearInterval(this.chartUpdateTimer);
this.chartUpdateTimer = -1;
}
// 取消订阅
SimpleEmitterUtil.off();
// 断开MQTT
this.mqttService.disconnect();
}
// 启动图表更新定时器
startChartUpdate(): void {
this.isWebViewReady = true;
// 立即更新一次
this.updateChartData();
// 每5秒更新图表
this.chartUpdateTimer = setInterval(() => {
this.updateChartData();
}, 5000);
}
updateChartData(): void {
try {
const chlorine: number = this.currentData.chlorine ?? 0;
const ammonia: number = this.currentData.ammonia ?? 0;
const jsCode: string = `updateData(${chlorine.toFixed(3)}, ${ammonia.toFixed(3)})`;
this.chartWebviewController.runJavaScript(jsCode);
console.info('图表更新: chlorine=' + chlorine.toFixed(3) + ', ammonia=' + ammonia.toFixed(3));
} catch (error) {
const errMsg: string = String(error);
console.error('更新图表失败: ' + errMsg);
}
}
// 获取数值显示
getValueByIndex(index: number): string {
const temp = this.currentData.temperature ?? 0;
const ph = this.currentData.ph ?? 0;
const conductivity = this.currentData.conductivity ?? 0;
const cod = this.currentData.cod ?? 0;
if (index === 0) return temp.toFixed(1);
if (index === 1) return ph.toFixed(2);
if (index === 2) return conductivity.toFixed(0);
return cod.toFixed(1);
}
build() {
Stack({ alignContent: Alignment.TopStart }) {
// 渐变背景(底层)
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.4], ['#1677FF', 1]]
})
// 内容区域(上层)
Scroll() {
Column({ space: 16 }) {
// 标题区域
Row() {
Text("水情监测")
.fontSize(18)
.fontWeight(600)
.fontColor('#FFFFFF')
Blank()
// 连接状态指示
Row({ space: 6 }) {
Circle()
.width(8)
.height(8)
.fill(this.hasRealData ? '#52C41A' : '#FAAD14')
Text(this.hasRealData ? '实时数据' : '模拟数据')
.fontSize(11)
.fontColor('rgba(255,255,255,0.7)')
}
}
.width('92%')
.padding({ top: 16, bottom: 8 })
// ===== 基本水质指标 =====
Column({ space: 10 }) {
Row({ space: 10 }) {
this.IndicatorCard(0)
this.IndicatorCard(1)
}
Row({ space: 10 }) {
this.IndicatorCard(2)
this.IndicatorCard(3)
}
}
.width('92%')
.padding({ top: 8, bottom: 8 })
// ===== 营养盐指标 =====
Row({ space: 10 }) {
// 氨氮
Column({ space: 8 }) {
Text("氨氮")
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
Row({ space: 4 }) {
Text((this.currentData.ammonia ?? 0).toFixed(3))
.fontSize(24)
.fontWeight(600)
.fontColor('#FF4D4F')
Text("mg/L")
.fontSize(12)
.fontColor('#999999')
}
Text('正常范围: ≤1.0')
.fontSize(10)
.fontColor('#999999')
}
.width('48%')
.height(90)
.linearGradient({
angle: 135,
colors: [['#FFFFFF', 0], ['#FFF1F0', 1]]
})
.borderRadius(12)
.padding(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(255,77,79,0.15)')
// 余氯
Column({ space: 8 }) {
Text("余氯")
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
Row({ space: 4 }) {
Text((this.currentData.chlorine ?? 0).toFixed(3))
.fontSize(24)
.fontWeight(600)
.fontColor('#FA8C16')
Text("mg/L")
.fontSize(12)
.fontColor('#999999')
}
Text('正常范围: 0.3-0.5')
.fontSize(10)
.fontColor('#999999')
}
.width('48%')
.height(90)
.linearGradient({
angle: 135,
colors: [['#FFFFFF', 0], ['#FFF7E6', 1]]
})
.borderRadius(12)
.padding(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(250,140,22,0.15)')
}
.width('92%')
// ===== 变化曲线 =====
Column() {
Text("实时变化曲线")
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
.padding({ left: 14, top: 12, bottom: 6 })
Web({ src: $rawfile('chart/line_chart.html'), controller: this.chartWebviewController })
.width('100%')
.height(220)
.backgroundColor('#FFFFFF')
.javaScriptAccess(true)
.domStorageAccess(true)
}
.width('92%')
.backgroundColor('#FFFFFF')
.borderRadius(12)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.15)')
// 底部提示
Row() {
Text(this.hasRealData ? 'MQTT实时数据' : '模拟数据展示')
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 8, bottom: 20 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.height('100%')
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
}
.width('100%')
.height('100%')
}
@Builder
IndicatorCard(index: number) {
Column({ space: 8 }) {
// 名称
Text(this.basicIndicators[index].name)
.fontSize(13)
.fontWeight(500)
.fontColor('#333333')
.width('100%')
// 数值
Row({ space: 2 }) {
Text(this.getValueByIndex(index))
.fontSize(24)
.fontWeight(600)
.fontColor(this.basicIndicators[index].color)
Text(this.basicIndicators[index].unit)
.fontSize(12)
.fontColor('#999999')
}
// 正常范围提示
Text('正常范围: ' + this.basicIndicators[index].normalRange)
.fontSize(10)
.fontColor('#999999')
}
.width('48%')
.height(90)
.linearGradient({
angle: 135,
colors: [['#FFFFFF', 0], ['#E6F4FF', 1]]
})
.borderRadius(12)
.padding(12)
.justifyContent(FlexAlign.Center)
.borderWidth(1)
.borderColor('rgba(22,119,255,0.15)')
}
}
+314
View File
@@ -0,0 +1,314 @@
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
/**
* 用户页面组件 - 科技蓝风格
*/
@Component
export struct User {
@State totalNumber: number = 2;
@State bugNumber: number = 1;
@State userName: string = '管理员';
// 退出登录
handleLogout(): void {
promptAction.showDialog({
title: '提示',
message: '确定要退出登录吗?',
buttons: [
{ text: '取消', color: 'rgba(255,255,255,0.6)' },
{ text: '确定', color: '#00FF88' }
]
}).then((result) => {
if (result.index === 1) {
// 清除登录信息
AppStorage.delete('token');
promptAction.showToast({ message: '已退出登录' });
// 跳转到登录页
router.pushUrl({ url: 'pages/Login' });
}
});
}
// 报告生成
handleGenerateReport(): void {
promptAction.showToast({ message: '报告生成功能开发中...' });
// TODO: 实现报告生成功能
}
// 设置
handleSettings(): void {
promptAction.showToast({ message: '设置功能开发中...' });
// TODO: 实现设置功能
}
build() {
Column() {
// 顶部标题栏 - 科技蓝风格
Row() {
Image($rawfile("user/个人.png"))
.width(20)
.height(20)
.fillColor('#FFFFFF')
.margin({ left: 20, right: 10 })
Text("个人中心")
.fontSize(20)
.fontWeight(600)
.fontColor("#FFFFFF")
Blank()
Text('用户管理')
.fontSize(12)
.fontColor('rgba(255,255,255,0.5)')
}
.width("100%")
.height(60)
.padding({ left: 10, right: 20 })
.alignItems(VerticalAlign.Center)
Scroll() {
Column({ space: 16 }) {
// 用户信息卡片
Column({ space: 12 }) {
// 头像区域
Row() {
Column() {
Image($rawfile("user/个人b.png"))
.width(40)
.height(40)
.fillColor('#FFFFFF')
}
.width(60)
.height(60)
.borderRadius(30)
.backgroundColor('rgba(22,119,255,0.3)')
.border({ width: 1, color: 'rgba(22,119,255,0.4)' })
Column({ space: 4 }) {
Text(this.userName)
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("鸿蒙水生态系统 · 专业版")
.fontSize(12)
.fontColor("rgba(255, 255, 255, 0.6)")
}
.alignItems(HorizontalAlign.Start)
.margin({ left: 12 })
}
.width('100%')
.justifyContent(FlexAlign.Start)
.padding({ left: 16, right: 16, top: 16, bottom: 12 })
}
.width("92%")
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.3)' })
// 数据统计卡片
Row({ space: 12 }) {
// 设备总数
Column({ space: 6 }) {
Text(this.totalNumber.toString())
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor('#00FF88')
Text("设备总数")
.fontSize(12)
.fontColor("rgba(255,255,255,0.6)")
}
.width('48%')
.height(80)
.backgroundColor('rgba(0,255,136,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(0,255,136,0.3)' })
.justifyContent(FlexAlign.Center)
// 异常设备
Column({ space: 6 }) {
Text(this.bugNumber.toString())
.fontSize(32)
.fontWeight(FontWeight.Bold)
.fontColor("#FF6B6B")
Text("异常设备")
.fontSize(12)
.fontColor("rgba(255,255,255,0.6)")
}
.width('48%')
.height(80)
.backgroundColor('rgba(255,107,107,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(255,107,107,0.3)' })
.justifyContent(FlexAlign.Center)
}
.width('92%')
// 功能菜单区域
Column({ space: 10 }) {
// 报告生成
Row() {
Row({ space: 12 }) {
Column() {
Image($rawfile("user/报告.png"))
.width(20)
.height(20)
.fillColor('#00FF88')
}
.width(40)
.height(40)
.borderRadius(10)
.backgroundColor('rgba(0,255,136,0.15)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text("报告生成")
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("生成设备运行报告")
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.alignItems(HorizontalAlign.Start)
}
Image($rawfile("user/右箭头.png"))
.width(16)
.height(16)
.fillColor('rgba(255,255,255,0.4)')
}
.width('100%')
.height(60)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.padding({ left: 12, right: 12 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.handleGenerateReport();
})
// 设置
Row() {
Row({ space: 12 }) {
Column() {
Image($rawfile("user/设置.png"))
.width(20)
.height(20)
.fillColor('#4ECDC4')
}
.width(40)
.height(40)
.borderRadius(10)
.backgroundColor('rgba(78,205,196,0.15)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text("系统设置")
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("个性化配置与偏好")
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.alignItems(HorizontalAlign.Start)
}
Image($rawfile("user/右箭头.png"))
.width(16)
.height(16)
.fillColor('rgba(255,255,255,0.4)')
}
.width('100%')
.height(60)
.backgroundColor('rgba(22,119,255,0.15)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(22,119,255,0.25)' })
.padding({ left: 12, right: 12 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.handleSettings();
})
// 退出登录
Row() {
Row({ space: 12 }) {
Column() {
Image($rawfile("user/退出.png"))
.width(20)
.height(20)
.fillColor('#FF6B6B')
}
.width(40)
.height(40)
.borderRadius(10)
.backgroundColor('rgba(255,107,107,0.15)')
.justifyContent(FlexAlign.Center)
Column({ space: 2 }) {
Text("退出登录")
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
Text("切换账号或重新登录")
.fontSize(11)
.fontColor('rgba(255,255,255,0.5)')
}
.alignItems(HorizontalAlign.Start)
}
Image($rawfile("user/右箭头.png"))
.width(16)
.height(16)
.fillColor('rgba(255,255,255,0.4)')
}
.width('100%')
.height(60)
.backgroundColor('rgba(255,107,107,0.1)')
.borderRadius(12)
.border({ width: 1, color: 'rgba(255,107,107,0.25)' })
.padding({ left: 12, right: 12 })
.justifyContent(FlexAlign.SpaceBetween)
.alignItems(VerticalAlign.Center)
.onClick(() => {
this.handleLogout();
})
}
.width('92%')
// 版本信息
Column() {
Text("版本 v1.0.0")
.fontSize(12)
.fontColor('rgba(255,255,255,0.4)')
Text("© 2024 鸿蒙水生态管理系统")
.fontSize(11)
.fontColor('rgba(255,255,255,0.3)')
.margin({ top: 4 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 16, bottom: 20 })
}
.width('100%')
.justifyContent(FlexAlign.Center)
}
.width('100%')
.layoutWeight(1)
.scrollBar(BarState.Off)
.edgeEffect(EdgeEffect.Spring)
.padding({ top: 12 })
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
@@ -0,0 +1,43 @@
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN = 0x0000;
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
}
onDestroy(): void {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy');
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
windowStage.loadContent('pages/Login', (err) => {
if (err.code) {
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
return;
}
hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
});
}
onWindowStageDestroy(): void {
// Main window is destroyed, release UI related resources
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
}
onForeground(): void {
// Ability has brought to foreground
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground');
}
onBackground(): void {
// Ability has back to background
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
}
}
@@ -0,0 +1,7 @@
/**
* AI请求消息接口
*/
export default interface AiMessageInterface {
role: string; // 角色: user 或 assistant
content: string; // 消息内容
}
@@ -0,0 +1,12 @@
export default interface AlarmItemInterface{
deviceId:number;
id:number;
msg:string;
value:string;
standard:string;
coordinate:string;
screenPosition:string;
createTime:string;
name:string;
code:string;
}
@@ -0,0 +1,11 @@
export default interface AlarmStatisticsInterface {
todayCount: string, //今日异常数量
todayTrend: string, //0 下降 1上升 2 等于
todayPercentage: string, //比率
weekCount: string, //本周异常数量
weekTrend: string, //0 下降 1上升 2 等于
weekPercentage: string, //比率
monthCount: string, //本月异常数量
monthTrend: string, //0 下降 1上升 2 等于
monthPercentage: string //比率
}
@@ -0,0 +1,12 @@
export default interface AlarmItemInterface{
deviceId: number;
id:number;
msg:string;
value:string;
standard:string;
coordinate:string;
screenPosition:string;
createTime:string;
name:string;
code:string;
}
@@ -0,0 +1,5 @@
export default interface CarbonInfoInterface {
carbonSequestration: number
ph: number
waterTemperature: number
}
@@ -0,0 +1,8 @@
export default interface DeviceListInterface{
id:number;
latitudeLongitude:string;
name:string;
screenPosition:string;
status:number
code:string;
}
@@ -0,0 +1,10 @@
/**
* Dify API请求体接口
*/
export default interface DifyRequestBody {
inputs: Record<string, string>;
query: string;
response_mode: string;
conversation_id: string;
user: string;
}
@@ -0,0 +1,14 @@
export default interface EutrophicationItemInterface{
//水温
waterTemperature:number
//cod
cod:number
//ph
ph:number
//余氯
residualChlorine:number
//氨氮
ammoniaNitrogen:number
//时间
createTime:string
}
@@ -0,0 +1,4 @@
export default interface LoginInterface{
username:string
password:string
}
@@ -0,0 +1,12 @@
import AiMessageInterface from './AiMessageInterface';
/**
* OpenAI API请求体接口
*/
export default interface OpenAiRequestBody {
model: string;
messages: AiMessageInterface[];
stream: boolean;
temperature?: number;
max_tokens?: number;
}
+32
View File
@@ -0,0 +1,32 @@
/**
* AI API响应模型 - 支持Dify和OpenAI两种格式
*/
export default interface RequestModel {
// Dify格式字段
answer?: string; // Dify: AI回答内容
event?: string; // Dify: 事件类型: message, message_end等
conversation_id?: string; // Dify: 会话ID
message_id?: string; // Dify: 消息ID
task_id?: string; // Dify: 任务ID
// OpenAI格式字段
choices?: ChoiceItem[]; // OpenAI: 选择列表
}
/**
* OpenAI Choice项
*/
export interface ChoiceItem {
delta?: DeltaItem;
finish_reason?: string;
index?: number;
}
/**
* OpenAI Delta项
*/
export interface DeltaItem {
content?: string;
role?: string;
}
@@ -0,0 +1,22 @@
export default interface SeaRangeInterface {
//水温
waterTemperatureBottom: number;
waterTemperatureTop: number;
//cod
codBottom: number;
codTop: number;
//ph
phBottom: number;
phTop: number;
//余氯
residualChlorineBottom:number;
residualChlorineTop:number;
//电导率
conductivityBottom:number
conductivityTop:number
//氨氮
ammoniaNitrogenBottom:number
ammoniaNitrogenTop:number
//id
id?:number
}
@@ -0,0 +1,5 @@
export default interface StatisticalInterface {
date: string
type: string
value: number
}
@@ -0,0 +1,11 @@
export default interface UserInfoInterface {
phonenumber: string | undefined, //用户姓名
avatar?: string | undefined, //用户头像
nickName: string
status?: string
userName?: string;
password?: string;
remark?: string | null;
userId?: string | undefined
editFlag?: boolean | undefined
}
@@ -0,0 +1,14 @@
export default interface SeaStatusInterface{
temp: number;
temp_compare:number;
cod:number;
cod_compare:number;
ph:number;
ph_compare:number;
residualChlorine:number;
residualChlorine_compare:number;
conductivity:number;
conductivity_compare:number;
ammoniaNitrogen:number;
ammoniaNitrogen_compare:number;
}
+78
View File
@@ -0,0 +1,78 @@
import { WaterMonitor } from '../components/WaterMonitor';
import { DeviceAlarm } from '../components/DeviceAlarm';
import { AiPredict } from '../components/AiPredict';
import { ContrastAnalysis } from '../components/ContrastAnalysis';
import { CarbonCalc } from '../components/CarbonCalc';
import DeviceListInterface from '../model/DeviceListInterface';
@Entry
@Component
struct Details {
@State currentIndex: number = 0;
@State currentDevice: DeviceListInterface | null = null;
private navItems: string[] = ['水情检测', '设备报警', 'AI预测', '对比分析', '碳汇计算'];
aboutToAppear(): void {
const device = AppStorage.get<DeviceListInterface>('point');
if (device) {
this.currentDevice = device;
}
}
build() {
Column() {
// 内容区域
Column() {
if (this.currentIndex === 0) {
WaterMonitor()
} else if (this.currentIndex === 1) {
DeviceAlarm()
} else if (this.currentIndex === 2) {
AiPredict()
} else if (this.currentIndex === 3) {
ContrastAnalysis()
} else {
CarbonCalc()
}
}
.width('100%')
.layoutWeight(1)
// 底部导航 - 科技蓝风格
Row() {
ForEach(this.navItems, (item: string, index: number) => {
Column({ space: 4 }) {
Image($rawfile(this.currentIndex === index
? `daohang/ic_tab_0${index + 1}_active.png`
: `daohang/ic_tab_0${index + 1}.png`))
.width(24)
.height(24)
Text(item)
.fontSize(11)
.fontColor(this.currentIndex === index ? '#FFFFFF' : 'rgba(255,255,255,0.5)')
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
}
.width('20%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = index;
})
})
}
.width('100%')
.height(65)
.backgroundColor('rgba(13,40,71,0.95)')
.borderWidth({ top: 1 })
.borderColor('rgba(22,119,255,0.3)')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
+76
View File
@@ -0,0 +1,76 @@
import { Equipment } from '../components/Equipment';
import { Add } from '../components/Add';
import { User } from '../components/user';
// 导航项接口
interface NavItem {
title: string;
trueIcon: string;
falseIcon: string;
}
@Entry
@Component
struct HomePage {
// 当前选中的导航索引,0:设备, 1:添加, 2:我的
@State currentIndex: number = 0;
// 导航项配置
private navItems: NavItem[] = [
{ title: '设备', trueIcon: 'index_true/设备.png', falseIcon: 'index_false/设备.png' },
{ title: '添加', trueIcon: 'index_true/添加.png', falseIcon: 'index_false/添加.png' },
{ title: '我的', trueIcon: 'index_true/我的.png', falseIcon: 'index_false/我的.png' }
];
build() {
Column() {
// 上方内容区域 - 显示对应页面
Column() {
if (this.currentIndex === 0) {
Equipment()
} else if (this.currentIndex === 1) {
Add()
} else {
User()
}
}
.width('100%')
.layoutWeight(1)
// 底部导航栏 - 科技蓝风格
Row() {
ForEach(this.navItems, (item: NavItem, index: number) => {
Column({ space: 5 }) {
Image($rawfile(this.currentIndex === index ? item.trueIcon : item.falseIcon))
.width(24)
.height(24)
.fillColor(this.currentIndex === index ? '#00FF88' : 'rgba(255,255,255,0.5)')
Text(item.title)
.fontSize(12)
.fontColor(this.currentIndex === index ? '#00FF88' : 'rgba(255,255,255,0.6)')
.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal)
}
.width('33.33%')
.justifyContent(FlexAlign.Center)
.onClick(() => {
this.currentIndex = index;
})
})
}
.width('100%')
.height(60)
.backgroundColor('rgba(13,40,71,0.95)')
.borderWidth({ top: 1 })
.borderColor('rgba(22,119,255,0.3)')
.justifyContent(FlexAlign.SpaceEvenly)
}
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [['#0a1628', 0], ['#0d2847', 0.5], ['#1677FF', 1]]
})
}
}
+356
View File
@@ -0,0 +1,356 @@
import LoginInterface from "../model/LoginInterface";
import UserInfoInterface from "../model/UserInfoInterface";
import { login, register } from "../utils/ApiService";
import { promptAction, router, Router } from '@kit.ArkUI';
@Entry
@Component
struct Login {
@State messageOne: string = '鸿蒙+AI水生态全流程管理系统';
@State messageTwo: string = 'HarmonyOS·AI·智慧水生态';
@State glowOpacity: number = 0.4;
@State waveScale: number = 1;
@State isLogin: boolean = true; // true: 登录, false: 注册
@State userName: string = '';
@State password: string = '';
@State name: string = '';
@State isAgree: boolean = false;
@State isLoading: boolean = false; // 加载状态
@State errorMsg: string = ''; // 错误信息
aboutToAppear() {
// 光晕呼吸动画
setInterval(() => {
this.glowOpacity = 0.3 + Math.sin(Date.now() / 1500) * 0.15;
}, 50);
// 波纹缩放动画
setInterval(() => {
this.waveScale = 1 + Math.sin(Date.now() / 2000) * 0.05;
}, 50);
}
// 测试账号配置
private testAccount: string = 'test';
private testPassword: string = '123456';
// 登录方法
async handleLogin(): Promise<void> {
if (!this.userName || !this.password) {
promptAction.showToast({ message: '请输入账户和密码' });
return;
}
// 测试账号直接登录(无需网络请求)
if (this.userName === this.testAccount && this.password === this.testPassword) {
promptAction.showToast({ message: '测试账号登录成功' });
AppStorage.setOrCreate('token', 'test_token_12345');
router.pushUrl({ url: 'pages/Index' });
return;
}
this.isLoading = true;
const loginData: LoginInterface = {
username: this.userName,
password: this.password
};
try {
const response = await login(loginData);
if (response.code === 200) {
promptAction.showToast({ message: '登录成功' });
// 保存token到AppStorage
AppStorage.setOrCreate('token', response.data);
// 登录成功后跳转到主页
router.pushUrl({ url: 'pages/Index' });
} else {
promptAction.showToast({ message: response.message || '登录失败' });
}
} catch (error) {
promptAction.showToast({ message: '网络请求失败,请检查网络连接' });
console.error('登录错误: ' + JSON.stringify(error));
} finally {
this.isLoading = false;
}
}
// 注册方法
async handleRegister(): Promise<void> {
if (!this.userName || !this.password || !this.name) {
promptAction.showToast({ message: '请填写完整信息' });
return;
}
this.isLoading = true;
const registerData: UserInfoInterface = {
phonenumber: this.userName,
password: this.password,
nickName: this.name
};
try {
const response = await register(registerData);
if (response.code === 200) {
promptAction.showToast({ message: '注册成功,请登录' });
this.isLogin = true; // 切换到登录
this.name = ''; // 清空姓名
} else {
promptAction.showToast({ message: response.message || '注册失败' });
}
} catch (error) {
promptAction.showToast({ message: '网络请求失败,请检查网络连接' });
console.error('注册错误: ' + JSON.stringify(error));
} finally {
this.isLoading = false;
}
}
build() {
Stack({ alignContent: Alignment.Center }) {
// 主渐变背景 - 铺满全屏
Column()
.width('100%')
.height('100%')
.linearGradient({
angle: 180,
colors: [
['#001E38', 0],
['#0958D9', 0.5],
['#1677FF', 0.8],
['#4096ff', 1]
]
})
// 柔和光晕 - 左上
Column()
.width(350)
.height(350)
.borderRadius(175)
.linearGradient({
angle: 135,
colors: [
['rgba(64, 150, 255, ' + this.glowOpacity + ')', 0],
['rgba(64, 150, 255, 0)', 0.7]
]
})
.position({ x: -100, y: -50 })
.blur(100)
// 柔和光晕 - 右下
Column()
.width(300)
.height(300)
.borderRadius(150)
.linearGradient({
angle: 315,
colors: [
['rgba(22, 119, 255, ' + (this.glowOpacity * 0.8) + ')', 0],
['rgba(22, 119, 255, 0)', 0.6]
]
})
.position({ x: '70%', y: '60%' })
.blur(80)
// 淡淡的波纹圆环
ForEach([1, 2], (item: number) => {
Column()
.width(220 + item * 60)
.height(220 + item * 60)
.borderRadius(110 + item * 30)
.borderWidth(1)
.borderColor('rgba(255, 255, 255, ' + (0.1 - item * 0.03) + ')')
.backgroundColor('transparent')
.scale({ x: this.waveScale, y: this.waveScale })
})
// 主内容 - 使用Column布局,标题在上,登录卡片在下
Column() {
// 标题区域 - 放在上方
Column({ space: 8 }) {
Text(this.messageOne)
.fontSize(25)
.fontWeight(500)
.fontWeight(FontWeight.Bold)
.fontColor(Color.White)
.textAlign(TextAlign.Center)
.textShadow({
radius: 20,
color: 'rgba(64, 150, 255, 0.5)',
offsetX: 0,
offsetY: 0
})
Text(this.messageTwo)
.fontSize(13)
.fontColor('rgba(255, 255, 255, 0.75)')
}
.width('100%')
.padding({ top: 80, bottom: 30 })
// 登录/注册卡片
Column({ space: 15 }) {
// 登录/注册切换
Row({ space: 50 }) {
Text("登录")
.fontSize(18)
.fontColor(this.isLogin ? '#FFFFFF' : 'rgba(255, 255, 255, 0.5)')
.fontWeight(this.isLogin ? FontWeight.Bold : FontWeight.Normal)
.onClick(() => {
this.isLogin = true;
})
.fontWeight(800)
Text("注册")
.fontSize(18)
.fontColor(!this.isLogin ? '#FFFFFF' : 'rgba(255, 255, 255, 0.5)')
.fontWeight(!this.isLogin ? FontWeight.Bold : FontWeight.Normal)
.onClick(() => {
this.isLogin = false;
})
.fontWeight(800)
}
.margin({ top: 5, bottom: 10 })
// 输入框区域
Column({ space: 15 }) {
// 账户输入框
Row(){
Image($rawfile('Login/用户.png'))
.width(15)
.height(15)
.margin({left:20})
TextInput({ placeholder: '请输入账户', text: this.userName })
.placeholderColor("#FFFFFF")
.placeholderFont({ size:12 })
.onChange((value: string) => {
this.userName = value;
})
.width('85%')
.height(45)
.borderRadius(8)
.fontColor('#FFFFFF')
.padding({ left: 15, right: 15 })
.backgroundColor(Color.Transparent)
}
.backgroundColor('rgba(255, 255, 255, 0.12)')
.width(300)
.borderRadius(10)
// 密码输入框
Row(){
Image($rawfile('Login/锁.png'))
.width(15)
.height(15)
.margin({left:20})
TextInput({ placeholder: '请输入密码', text: this.password })
.placeholderColor("#FFFFFF")
.placeholderFont({ size:12 })
.onChange((value: string) => {
this.password = value;
})
.width('85%')
.height(45)
.borderRadius(8)
.fontColor('#FFFFFF')
.type(InputType.Password)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.Transparent)
}
.backgroundColor('rgba(255, 255, 255, 0.12)')
.width(300)
.borderRadius(10)
// 注册时显示姓名输入框
if (!this.isLogin) {
Row(){
Image($rawfile('Login/电话.png'))
.width(15)
.height(15)
.margin({left:20})
TextInput({ placeholder: '请输入电话', text: this.name })
.placeholderColor("#FFFFFF")
.placeholderFont({ size:12 })
.onChange((value: string) => {
this.name = value;
})
.width('100%')
.height(45)
.borderRadius(8)
.fontColor('#FFFFFF')
.padding({ left: 15, right: 15 })
.backgroundColor(Color.Transparent)
}
.backgroundColor('rgba(255, 255, 255, 0.12)')
.width(300)
.borderRadius(10)
}
}
.width('100%')
// 协议同意
Row({ space: 8 }) {
Checkbox()
.select(this.isAgree)
.selectedColor('#4096ff')
.onChange((isChecked: boolean) => {
this.isAgree = isChecked;
})
.width(15)
.height(15)
Text() {
Span('我已阅读并同意')
.fontColor('rgba(255, 255, 255, 0.7)')
.fontSize(12)
Span('《用户协议》')
.fontColor('#FFFFFF')
.fontSize(12)
Span('和')
.fontColor('rgba(255, 255, 255, 0.7)')
.fontSize(12)
Span('《隐私政策》')
.fontColor('#FFFFFF')
.fontSize(12)
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 10 , left:-30})
// 登录/注册按钮
Button(this.isLoading ? '处理中...' : (this.isLogin ? '登录' : '注册'))
.width('90%')
.height(45)
.fontSize(16)
.fontColor('#1677FF')
.fontWeight(FontWeight.Bold)
.backgroundColor('#FFFFFF')
.borderRadius(15)
.enabled(this.isAgree && !this.isLoading)
.margin({ top: 15 })
.onClick(() => {
if (this.isLogin) {
this.handleLogin();
} else {
this.handleRegister();
}
})
}
.width('100%')
.padding(25)
.borderRadius(16)
.margin({top:80})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Start)
}
.width('100%')
.height('100%')
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP, SafeAreaEdge.BOTTOM])
}
}
+77
View File
@@ -0,0 +1,77 @@
import { iResponseModel, RequestUtil } from "./RequestUtil";
import LoginInterface from "../model/LoginInterface";
import UserInfoInterface from "../model/UserInfoInterface";
import DeviceListInterface from "../model/DeviceListInterface";
import CarbonInfoInterface from "../model/CarbonInfoInterface";
import StatisticalInterface from "../model/StatisticalInterface";
import SeaRangeInterface from "../model/SeaRangeInterface";
import EutrophicationItemInterface from "../model/EutrophicationItemInterface";
import AlarmItemInterface from "../model/AlarmItemInterface";
import AlarmStatisticsInterface from "../model/AlarmStatisticsInterface";
//登录
export function login(cls: LoginInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/login", cls, false);
}
//注册
export function register(cls: UserInfoInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/register", cls, false);
}
//查询长江设备列表-获取四个监测点的设备
export function getDeviceList(): Promise<iResponseModel<DeviceListInterface[]>> {
return RequestUtil.get("/sea/device/list");
}
//数据监测-查询长江最近7日数据->折线图(Ai的预测服务也是这个接口)
export function getSeaEutrophication(): Promise<iResponseModel<EutrophicationItemInterface[]>> {
return RequestUtil.get("/sea/day/getLimit7?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//AI预测-ai问答
export const chatUrl: string = "/v1/chat-messages";
//对比分析-> 通过参数类型查询长江按月统计
export function getContrast(paramType: number): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get("/sea/month/monthDetail?paramType=" + paramType + "&deviceCode=" +
AppStorage.get<DeviceListInterface>("point")?.code);
}
//最新异常列表
export function getAlarmList(): Promise<iResponseModel<AlarmItemInterface[]>> {
return RequestUtil.get("/sea/alarm/list?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//最新异常列表
export function getAlarmStatistics(): Promise<iResponseModel<AlarmStatisticsInterface>> {
return RequestUtil.get("/sea/alarm/getAlarmStatistics?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//异常趋势
export function getAlarmTrend(): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get("/sea/alarm/getAlarmTrend?code=" + AppStorage.get<DeviceListInterface>("point")?.code);
}
//获取长江数据区间范围值详细信息
export function getSeaRange(): Promise<iResponseModel<SeaRangeInterface>> {
return RequestUtil.get("/sea/range/1");
}
//修改长江数据区间范围值详细信息
export function editSeaRange(cls: SeaRangeInterface): Promise<iResponseModel<SeaRangeInterface>> {
cls.id = 1;
return RequestUtil.put("/sea/range", cls);
}
//碳汇详情
export function getCarbonInfo(): Promise<iResponseModel<CarbonInfoInterface>> {
let date = new Date().getFullYear() + "年" + new Date().getMonth().toString().padStart(2, "0");
return RequestUtil.get(`/sea/month/query?deviceCode=${AppStorage.get<DeviceListInterface>("point")?.code}&remark=${encodeURIComponent(date)}`);
}
//固碳6个月
export function getCarbonList(): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get(`/sea/month/sixMonthStatistics?deviceCode=${AppStorage.get<DeviceListInterface>("point")?.code}`);
}
@@ -0,0 +1,87 @@
import { iResponseModel, RequestUtil } from "./RequestUtil";
import LoginInterface from "../model/LoginInterface";
import UserInfoInterface from "../model/UserInfoInterface";
import DeviceListInterface from "../model/DeviceListInterface";
import CarbonInfoInterface from "../model/CarbonInfoInterface";
import StatisticalInterface from "../model/StatisticalInterface";
import SeaRangeInterface from "../model/SeaRangeInterface";
import EutrophicationItemInterface from "../model/EutrophicationItemInterface";
import AlarmItemInterface from "../model/Alarmltemlnterface";
import AlarmStatisticsInterface from "../model/AlarmStatisticsInterface";
// 登录
export function login(cls: LoginInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/login", cls, false);
}
// 注册
export function register(cls: UserInfoInterface | LoginInterface): Promise<iResponseModel<string>> {
return RequestUtil.post("/app/common/register", cls, false);
}
// 查询长江设备列表-获取四个监测点的设备
export function getDeviceList(): Promise<iResponseModel<DeviceListInterface[]>> {
return RequestUtil.get("/sea/device/list");
}
// 数据监测-查询长江最近7日数据
export function getSeaEutrophication(): Promise<iResponseModel<EutrophicationItemInterface[]>> {
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
return RequestUtil.get("/sea/day/getLimit7?code=" + code);
}
// AI预测-ai问答
export const chatUrl: string = "/v1/chat-messages";
// 对比分析 -> 通过参数类型查询长江按月统计
export function getContrast(paramType: number): Promise<iResponseModel<StatisticalInterface[]>> {
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
if (!code) {
// 这里直接抛错,避免发 deviceCode=undefined 的脏请求
return Promise.reject(new Error("deviceCode为空:请先在AppStorage写入point.code"));
}
return RequestUtil.get(`/sea/month/monthDetail?paramType=${paramType}&deviceCode=${code}`);
}
// 最新异常列表
export function getAlarmList(): Promise<iResponseModel<AlarmItemInterface[]>> {
return RequestUtil.get("/sea/alarm/list?code=" + (AppStorage.get<DeviceListInterface>("point")?.code ?? ""));
}
// 最新异常统计
export function getAlarmStatistics(): Promise<iResponseModel<AlarmStatisticsInterface>> {
return RequestUtil.get("/sea/alarm/getAlarmStatistics?code=" + (AppStorage.get<DeviceListInterface>("point")?.code
?? ""));
}
// 异常趋势
export function getAlarmTrend(): Promise<iResponseModel<StatisticalInterface[]>> {
return RequestUtil.get("/sea/alarm/getAlarmTrend?code=" + (AppStorage.get<DeviceListInterface>("point")?.code ??
""));
}
// 获取长江数据区间范围值详细信息
export function getSeaRange(): Promise<iResponseModel<SeaRangeInterface>> {
return RequestUtil.get("/sea/range/1");
}
// 修改长江数据区间范围值详细信息
export function editSeaRange(cls: SeaRangeInterface): Promise<iResponseModel<SeaRangeInterface>> {
cls.id = 1;
return RequestUtil.put("/sea/range", cls);
}
// 碳汇详情
export function getCarbonInfo(): Promise<iResponseModel<CarbonInfoInterface>> {
const date = new Date().getFullYear() + "年" + new Date().getMonth().toString().padStart(2, "0");
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
return RequestUtil.get(`/sea/month/query?deviceCode=${code}&remark=${encodeURIComponent(date)}`);
}
// 固碳6个月
export function getCarbonList(): Promise<iResponseModel<StatisticalInterface[]>> {
const code = AppStorage.get<DeviceListInterface>("point")?.code ?? "";
return RequestUtil.get(`/sea/month/sixMonthStatistics?deviceCode=${code}`);
}
+23
View File
@@ -0,0 +1,23 @@
/**
* 配置文件 - 存放服务器IP、端口等变量参数
*/
export class Config {
// 后端服务器IP地址
static readonly BASE_URL: string = 'http://192.168.31.100:8080';
// ==================== AI服务配置 ====================
// 主APIDify格式)
static readonly AI_PRIMARY_URL: string = 'http://192.168.31.26';
static readonly AI_PRIMARY_PATH: string = '/v1';
static readonly AI_PRIMARY_KEY: string = "app-druPEUrX7Qw7kWBv1JUfaG4q"
// 备用APIOpenAI格式)- 当主API失败时自动切换
static readonly AI_BACKUP_URL: string = 'https://ark.cn-beijing.volces.com';
static readonly AI_BACKUP_PATH: string = '/api/v3/chat/completions';
static readonly AI_BACKUP_KEY: string = 'ark-7bdaff26-adab-4332-a1f5-f97fb03e7d56-aa3a1'; // 填写你的OpenAI密钥
static readonly AI_BACKUP_MODEL: string = 'doubao-seed-2-0-code-preview-260215'; // 使用的模型
// 请求超时时间(毫秒)
static readonly TIMEOUT: number = 30000;
}
export default Config;
+309
View File
@@ -0,0 +1,309 @@
/**
* MQTT服务 - ArkTS版本
* 使用 @ohos/mqtt 原生库实现MQTT连接
*/
import {
MqttAsync,
MqttClient,
MqttMessage,
MqttQos,
MqttResponse,
MqttSubscribeOptions
} from '@ohos/mqtt';
import emitter from '@ohos.events.emitter';
// MQTT 事件ID (字符串形式)
const MQTT_EVENT_ID: string = 'mqtt_water_quality';
// 事件数据接口
interface PayloadData {
payload: string
}
// 简化的Emitter工具类(替代@pura/harmony-utils的EmitterUtil
export class SimpleEmitterUtil {
private static storedCallback: ((payload: string) => void) | undefined = undefined;
private static emitterCallback: ((eventData: emitter.EventData) => void) | undefined = undefined;
// 发送事件
static emit(data: string): void {
const payloadData: PayloadData = { payload: data };
const eventData: emitter.EventData = { data: payloadData };
emitter.emit(MQTT_EVENT_ID, eventData);
}
// 订阅事件
static on(callback: (payload: string) => void): void {
SimpleEmitterUtil.storedCallback = callback;
// 创建符合 emitter.EventData 签名的回调
const emitterCallback = (eventData: emitter.EventData): void => {
if (eventData && eventData.data) {
const payloadData = eventData.data as PayloadData;
if (payloadData.payload) {
callback(payloadData.payload);
}
}
};
SimpleEmitterUtil.emitterCallback = emitterCallback;
emitter.on(MQTT_EVENT_ID, emitterCallback);
}
// 取消订阅
static off(): void {
if (SimpleEmitterUtil.emitterCallback) {
emitter.off(MQTT_EVENT_ID, SimpleEmitterUtil.emitterCallback);
SimpleEmitterUtil.storedCallback = undefined;
SimpleEmitterUtil.emitterCallback = undefined;
}
}
}
// MQTT 事件ID导出(兼容旧代码使用数字)
export const MQTT_EVENT_ID_NUM: number = 1001;
// 水质数据接口
export interface WaterQualityData {
temperature: number;
ph: number;
conductivity: number;
cod: number;
ammonia: number;
chlorine: number;
timestamp: number;
}
// MQTT配置接口
interface MqttConfig {
url: string;
clientId: string;
userName: string;
password: string;
topic: string;
qos: MqttQos;
}
export class MqttService {
private static instance: MqttService;
private mqttClient: MqttClient | null = null;
private config: MqttConfig;
private isConnectedFlag: boolean = false;
constructor() {
this.config = {
url: 'tcp://192.168.31.100:1883',
clientId: 'sea-lwq',
userName: 'admin',
password: 'admin',
topic: 'sea_status',
qos: 0
};
}
public static getInstance(): MqttService {
if (!MqttService.instance) {
MqttService.instance = new MqttService();
}
return MqttService.instance;
}
// 设置配置
setConfig(url: string, topic: string): void {
this.config.url = url;
this.config.topic = topic;
console.info('MQTT配置: url=' + url + ', topic=' + topic +
', clientId=' + this.config.clientId +
', userName=' + this.config.userName);
}
// 初始化连接
async connect(): Promise<void> {
try {
const status = await this.getMqttConnectStatus();
if (status) {
console.info('MQTT已经连接,跳过重复连接');
return;
}
this.createMqttClient();
await this.connectMqtt();
await this.subscribe();
this.Arrived();
console.info('MQTT初始化完成');
} catch (err) {
console.error('MQTT初始化失败: ' + JSON.stringify(err));
}
}
// 获取连接状态
private async getMqttConnectStatus(): Promise<boolean> {
if (this.mqttClient) {
const result = await this.mqttClient.isConnected();
return result === true;
}
return false;
}
// 创建mqtt实例
createMqttClient(): void {
this.mqttClient = MqttAsync.createMqtt({
url: this.config.url,
clientId: this.config.clientId,
persistenceType: 1
});
console.info('MQTT客户端实例已创建: url=' + this.config.url + ', clientId=' + this.config.clientId);
}
// 连接服务器
async connectMqtt(): Promise<void> {
if (!this.mqttClient) {
console.error('MQTT客户端未创建');
return;
}
console.info('正在连接MQTT服务器: userName=' + this.config.userName);
await this.mqttClient.connect({
userName: this.config.userName,
password: this.config.password,
connectTimeout: 30,
automaticReconnect: true,
MQTTVersion: 3
}).then((res: MqttResponse) => {
this.isConnectedFlag = true;
console.info('MQTT服务器连接成功:' + JSON.stringify(res.message));
}).catch((err: Error) => {
this.isConnectedFlag = false;
console.error('MQTT服务器连接失败:' + JSON.stringify(err));
});
}
// 订阅主题
async subscribe(): Promise<void> {
if (!this.mqttClient) {
return;
}
const subscribeOption: MqttSubscribeOptions = {
topic: this.config.topic,
qos: this.config.qos
};
console.info('正在订阅主题: ' + this.config.topic);
await this.mqttClient.subscribe(subscribeOption).then((res: MqttResponse) => {
console.info('MQTT订阅主题成功:' + JSON.stringify(res.message));
}).catch((err: Error) => {
console.error('MQTT订阅主题失败:' + JSON.stringify(err));
});
}
// 监听消息到达 - 使用EmitterUtil发送到事件总线
Arrived(): void {
if (!this.mqttClient) {
return;
}
console.info('开始监听MQTT消息...');
this.mqttClient.messageArrived((err: Error, data: MqttMessage) => {
if (err) {
console.error('MQTT消息接收错误: ' + JSON.stringify(err));
} else {
const payloadStr: string = data.payload as string;
console.info('收到MQTT消息: ' + payloadStr);
// 通过SimpleEmitterUtil发送到事件总线
SimpleEmitterUtil.emit(payloadStr);
}
});
}
// 发布消息
async publish(message: string, topic?: string): Promise<void> {
if (!this.mqttClient) {
console.error('MQTT未连接,无法发送消息');
return;
}
await this.mqttClient.publish({
topic: topic || this.config.topic,
qos: this.config.qos,
payload: message
}).then((data: MqttResponse) => {
console.info('MQTT消息发送成功:' + JSON.stringify(data));
}).catch((err: Error) => {
console.error('MQTT消息发送失败:' + JSON.stringify(err));
});
}
// 断开连接
async disconnect(): Promise<void> {
if (this.mqttClient) {
try {
await this.mqttClient.destroy();
console.info('MQTT连接已断开');
} catch (err) {
console.error('MQTT断开失败: ' + JSON.stringify(err));
}
this.mqttClient = null;
}
this.isConnectedFlag = false;
}
// 销毁客户端
async destroy(): Promise<void> {
if (!this.mqttClient) {
return;
}
await this.mqttClient.destroy().then((data: boolean) => {
if (data) {
console.info('MQTT实例销毁成功');
SimpleEmitterUtil.off();
} else {
console.error('MQTT实例销毁失败');
}
});
this.mqttClient = null;
this.isConnectedFlag = false;
}
// 解析水质数据
// 后端: {"temp":21,"cod":44,"ph":0,"residualChlorine":0,"conductivity":0,"ammoniaNitrogen":0,...}
parseWaterQualityData(rawMessage: string): WaterQualityData {
try {
const raw = JSON.parse(rawMessage) as Record<string, number>;
const data: WaterQualityData = {
temperature: raw['temp'] ?? 0,
ph: raw['ph'] ?? 0,
conductivity: raw['conductivity'] ?? 0,
cod: raw['cod'] ?? 0,
ammonia: raw['ammoniaNitrogen'] ?? 0,
chlorine: raw['residualChlorine'] ?? 0,
timestamp: Date.now()
};
console.info('解析后水质数据: 温度=' + data.temperature +
', pH=' + data.ph +
', 电导率=' + data.conductivity +
', COD=' + data.cod +
', 氨氮=' + data.ammonia +
', 余氯=' + data.chlorine);
return data;
} catch (err) {
console.error('水质数据解析失败: ' + rawMessage);
return {
temperature: 0, ph: 0, conductivity: 0,
cod: 0, ammonia: 0, chlorine: 0, timestamp: Date.now()
};
}
}
getConnectionStatus(): boolean {
return this.isConnectedFlag;
}
getConfig(): MqttConfig {
return this.config;
}
}
export default MqttService;
+185
View File
@@ -0,0 +1,185 @@
/**
* HTTP请求工具类
* 使用 @kit.NetworkKit 进行网络请求
*/
import { http } from '@kit.NetworkKit';
import { BusinessError } from '@kit.BasicServicesKit';
import Config from './Config';
// 响应数据接口
export interface iResponseModel<T> {
code: number;
message: string;
data: T;
}
// HTTP请求头接口
interface HttpHeader {
'Content-Type': string;
'Accept': string;
'Authorization'?: string;
}
// HTTP请求选项接口
interface HttpOptions {
method: http.RequestMethod;
header: HttpHeader;
extraData?: Object;
connectTimeout: number;
readTimeout: number;
}
export class RequestUtil {
/**
* GET请求
*/
static get<T>(url: string): Promise<iResponseModel<T>> {
return new Promise((resolve, reject) => {
let httpRequest = http.createHttp();
const header: HttpHeader = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
const options: HttpOptions = {
method: http.RequestMethod.GET,
header: header,
connectTimeout: Config.TIMEOUT,
readTimeout: Config.TIMEOUT
};
httpRequest.request(
Config.BASE_URL + url,
options,
(err: BusinessError, data: http.HttpResponse) => {
if (!err) {
try {
let result: iResponseModel<T> = JSON.parse(data.result as string);
resolve(result);
} catch (e) {
reject(new Error('JSON解析失败'));
}
} else {
reject(new Error(err.message));
}
httpRequest.destroy();
}
);
});
}
/**
* GET请求(带query参数)
* @param url 请求地址,例如 /monthDetail
* @param params 查询参数,例如 { paramType: 'WATER_TEMPERATURE', deviceCode: 'D001' }
*/
static getWithParams<T>(url: string, params: Record<string, string | number | boolean>): Promise<iResponseModel<T>>
{
const query = Object.keys(params)
.map((key: string) => `${encodeURIComponent(key)}=${encodeURIComponent(String(params[key]))}`)
.join('&');
const fullUrl = query.length > 0 ? `${url}?${query}` : url;
return RequestUtil.get<T>(fullUrl);
}
/**
* POST请求
* @param url 请求地址
* @param cls 请求体对象
* @param needToken 是否需要Token
*/
static post<T>(url: string, cls: Object, needToken: boolean): Promise<iResponseModel<T>> {
return new Promise((resolve, reject) => {
let httpRequest = http.createHttp();
let header: HttpHeader;
if (needToken) {
const token = AppStorage.get<string>('token');
header = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + token
};
} else {
header = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
}
const options: HttpOptions = {
method: http.RequestMethod.POST,
header: header,
extraData: cls,
connectTimeout: Config.TIMEOUT,
readTimeout: Config.TIMEOUT
};
httpRequest.request(
Config.BASE_URL + url,
options,
(err: BusinessError, data: http.HttpResponse) => {
if (!err) {
try {
let result: iResponseModel<T> = JSON.parse(data.result as string);
resolve(result);
} catch (e) {
reject(new Error('JSON解析失败'));
}
} else {
reject(new Error(err.message));
}
httpRequest.destroy();
}
);
});
}
/**
* PUT请求
* @param url 请求地址
* @param cls 请求体对象
*/
static put<T>(url: string, cls: Object): Promise<iResponseModel<T>> {
return new Promise((resolve, reject) => {
let httpRequest = http.createHttp();
const token = AppStorage.get<string>('token');
const header: HttpHeader = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ' + token
};
const options: HttpOptions = {
method: http.RequestMethod.PUT,
header: header,
extraData: cls,
connectTimeout: Config.TIMEOUT,
readTimeout: Config.TIMEOUT
};
httpRequest.request(
Config.BASE_URL + url,
options,
(err: BusinessError, data: http.HttpResponse) => {
if (!err) {
try {
let result: iResponseModel<T> = JSON.parse(data.result as string);
resolve(result);
} catch (e) {
reject(new Error('JSON解析失败'));
}
} else {
reject(new Error(err.message));
}
httpRequest.destroy();
}
);
});
}
}
export default RequestUtil;
+46
View File
@@ -0,0 +1,46 @@
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:permission_internet_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
@@ -0,0 +1,8 @@
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
}
]
}
@@ -0,0 +1,8 @@
{
"float": [
{
"name": "page_text_font_size",
"value": "50fp"
}
]
}
@@ -0,0 +1,20 @@
{
"string": [
{
"name": "module_desc",
"value": "module description"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "label"
},
{
"name": "permission_internet_reason",
"value": "用于连接MQTT服务器获取实时水质数据"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

@@ -0,0 +1,7 @@
{
"layered-image":
{
"background" : "$media:background",
"foreground" : "$media:foreground"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,7 @@
{
"src": [
"pages/Index",
"pages/Login",
"pages/Details"
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

@@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>碳汇趋势图</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: transparent; }
#chart { width: 100%; height: 160px; }
</style>
</head>
<body>
<div id="chart"></div>
<script>
var chart = echarts.init(document.getElementById('chart'));
var months = ['1月', '2月', '3月', '4月', '5月', '6月'];
var data = [0];
var option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(13,40,71,0.9)',
borderColor: '#00FF88',
borderWidth: 1,
textStyle: { color: '#FFFFFF', fontSize: 12 },
formatter: function(p) {
return '<div style="font-weight:bold">' + p[0].axisValue + '</div><div style="color:#00FF88">' + p[0].value.toFixed(2) + ' kg</div>';
}
},
grid: { left: 35, right: 15, top: 15, bottom: 25 },
xAxis: {
type: 'category',
data: months,
axisLabel: { fontSize: 11, color: 'rgba(255,255,255,0.7)' },
axisLine: { lineStyle: { color: 'rgba(22,119,255,0.4)' } },
axisTick: { show: false }
},
yAxis: {
type: 'value',
axisLabel: { fontSize: 11, color: 'rgba(255,255,255,0.7)', formatter: '{value}' },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: 'rgba(22,119,255,0.2)', type: 'dashed' } }
},
series: [{
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 5,
lineStyle: { color: '#00FF88', width: 2 },
itemStyle: { color: '#00FF88' },
areaStyle: {
color: {
type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
colorStops: [{ offset: 0, color: 'rgba(0,255,136,0.35)' }, { offset: 1, color: 'rgba(0,255,136,0.05)' }]
}
},
data: data
}]
};
chart.setOption(option);
function updateData(arr) {
data = arr;
option.series[0].data = data;
chart.setOption(option);
}
</script>
</body>
</html>
@@ -0,0 +1,181 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>contrast chart</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
background: transparent;
overflow: hidden;
}
#chart {
width: 100%;
height: 100%;
background: transparent;
}
</style>
</head>
<body>
<div id="chart"></div>
<!-- 你项目里若是本地 echarts,请改成你的实际路径 -->
<!-- 例如:<script src="./echarts.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
<script>
let chart = null;
let currentColor = '#00FF88';
let currentData = [];
let currentUnit = '';
let currentName = '';
let xLabels = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
function getOption(data, unit, name, color) {
const max = Math.max.apply(null, data);
const min = Math.min.apply(null, data);
return {
backgroundColor: 'transparent',
animation: true,
grid: {
left: 18,
right: 12,
top: 24,
bottom: 24,
containLabel: true
},
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(10,22,40,0.9)',
borderColor: 'rgba(22,119,255,0.5)',
borderWidth: 1,
textStyle: { color: '#fff', fontSize: 11 },
formatter: function (params) {
const p = params[0];
return `${p.axisValue}<br/>${name}: ${p.value}${unit ? ' ' + unit : ''}`;
}
},
xAxis: {
type: 'category',
data: xLabels,
boundaryGap: false,
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.25)' } },
axisTick: { show: false },
axisLabel: {
color: 'rgba(255,255,255,0.65)',
fontSize: 10
},
splitLine: { show: false }
},
yAxis: {
type: 'value',
scale: true,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: {
color: 'rgba(255,255,255,0.65)',
fontSize: 10
},
splitLine: {
lineStyle: { color: 'rgba(255,255,255,0.12)' }
}
},
series: [
{
name: name,
type: 'line',
data: data,
smooth: true,
symbol: 'circle',
symbolSize: 6,
showSymbol: true,
lineStyle: {
width: 2,
color: color
},
itemStyle: {
color: color,
borderColor: '#ffffff',
borderWidth: 1
},
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: hexToRgba(color, 0.35) },
{ offset: 1, color: hexToRgba(color, 0.02) }
]
}
},
markPoint: {
symbolSize: 30,
label: { color: '#fff', fontSize: 9 },
itemStyle: { color: color },
data: [
{ type: 'max', name: '最大值' },
{ type: 'min', name: '最小值' }
]
}
}
]
};
}
function hexToRgba(hex, alpha) {
const c = hex.replace('#', '');
if (c.length !== 6) return `rgba(0,255,136,${alpha})`;
const r = parseInt(c.substring(0, 2), 16);
const g = parseInt(c.substring(2, 4), 16);
const b = parseInt(c.substring(4, 6), 16);
return `rgba(${r},${g},${b},${alpha})`;
}
function ensureChart() {
const dom = document.getElementById('chart');
if (!chart) {
chart = echarts.init(dom, null, { renderer: 'canvas' });
}
}
// 首次初始化(对应 TS: onPageEnd -> initChart
function initChart(data, unit, name) {
ensureChart();
currentData = Array.isArray(data) ? data : [];
currentUnit = unit || '';
currentName = name || '';
chart.setOption(getOption(currentData, currentUnit, currentName, currentColor), true);
}
// 更新数据(对应 TS: updateChart -> updateData
function updateData(data, unit, name) {
ensureChart();
currentData = Array.isArray(data) ? data : currentData;
currentUnit = unit !== undefined ? unit : currentUnit;
currentName = name !== undefined ? name : currentName;
chart.setOption(getOption(currentData, currentUnit, currentName, currentColor), true);
}
// 更新颜色
function setColor(color) {
currentColor = color || currentColor;
if (!chart) return;
chart.setOption(getOption(currentData, currentUnit, currentName, currentColor), true);
}
window.addEventListener('resize', function () {
if (chart) chart.resize();
});
// 暴露给 ArkTS 调用
window.initChart = initChart;
window.updateData = updateData;
window.setColor = setColor;
</script>
</body>
</html>
@@ -0,0 +1,234 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>水质实时曲线</title>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: transparent;
font-family: 'Microsoft YaHei', sans-serif;
padding: 8px;
}
#chart {
width: 100%;
height: 200px;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
var chartDom = document.getElementById('chart');
var myChart = echarts.init(chartDom);
// 初始化数据存储
var chlorineData = [];
var ammoniaData = [];
var timeData = [];
// 初始化20个空数据点
for(var i = 19; i >= 0; i--) {
timeData.push(formatTime(new Date(Date.now() - i * 5000)));
chlorineData.push(0);
ammoniaData.push(0);
}
// 时间格式化函数
function formatTime(date) {
var h = date.getHours();
var m = date.getMinutes();
var s = date.getSeconds();
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
}
// ECharts配置
var option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(255,255,255,0.95)',
borderColor: '#E5E5E5',
borderWidth: 1,
textStyle: {
color: '#333',
fontSize: 13
},
formatter: function(params) {
var result = '<div style="font-weight:bold;margin-bottom:4px">' + params[0].axisValue + '</div>';
params.forEach(function(item) {
var color = item.seriesName === '余氯' ? '#FA8C16' : '#FF4D4F';
result += '<div style="display:flex;align-items:center;margin:2px 0">';
result += '<span style="display:inline-block;width:10px;height:10px;background:' + color + ';border-radius:50%;margin-right:6px"></span>';
result += '<span style="flex:1">' + item.seriesName + '</span>';
result += '<span style="font-weight:bold;color:' + color + '">' + item.value.toFixed(3) + ' mg/L</span>';
result += '</div>';
});
return result;
}
},
legend: {
data: ['余氯', '氨氮'],
top: 5,
right: 10,
textStyle: {
fontSize: 12,
color: '#666'
},
itemWidth: 16,
itemHeight: 8,
itemGap: 15
},
grid: {
left: 45,
right: 15,
top: 30,
bottom: 30
},
xAxis: {
type: 'category',
boundaryGap: false,
data: timeData,
axisLabel: {
fontSize: 10,
color: '#999',
interval: 3,
rotate: 0
},
axisLine: {
lineStyle: { color: '#E5E5E5', width: 1 }
},
axisTick: { show: false }
},
yAxis: {
type: 'value',
min: 0,
max: 1,
interval: 0.2,
axisLabel: {
fontSize: 10,
color: '#999',
formatter: '{value}'
},
axisLine: { show: false },
axisTick: { show: false },
splitLine: {
lineStyle: {
color: '#F0F0F0',
type: 'dashed'
}
}
},
series: [
{
name: '余氯',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
showSymbol: false,
lineStyle: {
color: '#FA8C16',
width: 2.5
},
itemStyle: { color: '#FA8C16' },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(250,140,22,0.3)' },
{ offset: 0.5, color: 'rgba(250,140,22,0.15)' },
{ offset: 1, color: 'rgba(250,140,22,0.05)' }
]
}
},
data: chlorineData
},
{
name: '氨氮',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 4,
showSymbol: false,
lineStyle: {
color: '#FF4D4F',
width: 2.5
},
itemStyle: { color: '#FF4D4F' },
areaStyle: {
color: {
type: 'linear',
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: 'rgba(255,77,79,0.3)' },
{ offset: 0.5, color: 'rgba(255,77,79,0.15)' },
{ offset: 1, color: 'rgba(255,77,79,0.05)' }
]
}
},
data: ammoniaData
}
]
};
myChart.setOption(option);
// 更新数据函数 - 从HarmonyOS调用
function updateData(chlorine, ammonia) {
var now = formatTime(new Date());
// 添加新数据
timeData.push(now);
chlorineData.push(parseFloat(chlorine));
ammoniaData.push(parseFloat(ammonia));
// 保持最多20个数据点
if(timeData.length > 20) {
timeData.shift();
chlorineData.shift();
ammoniaData.shift();
}
// 更新图表
myChart.setOption({
xAxis: { data: timeData },
series: [
{ data: chlorineData },
{ data: ammoniaData }
]
});
}
// 清空数据函数
function clearData() {
timeData = [];
chlorineData = [];
ammoniaData = [];
for(var i = 19; i >= 0; i--) {
timeData.push(formatTime(new Date(Date.now() - i * 5000)));
chlorineData.push(0);
ammoniaData.push(0);
}
myChart.setOption({
xAxis: { data: timeData },
series: [
{ data: chlorineData },
{ data: ammoniaData }
]
});
}
// 响应窗口大小变化
window.addEventListener('resize', function() {
myChart.resize();
});
</script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

+2
View File
@@ -0,0 +1,2 @@
{
}
@@ -0,0 +1,35 @@
import { hilog } from '@kit.PerformanceAnalysisKit';
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function abilityTest() {
describe('ActsAbilityTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
})
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
})
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
})
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
})
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
hilog.info(0x0000, 'testTag', '%{public}s', 'it begin');
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
})
})
}
@@ -0,0 +1,5 @@
import abilityTest from './Ability.test';
export default function testsuite() {
abilityTest();
}
+11
View File
@@ -0,0 +1,11 @@
{
"module": {
"name": "entry_test",
"type": "feature",
"deviceTypes": [
"phone"
],
"deliveryWithInstall": true,
"installationFree": false
}
}
+5
View File
@@ -0,0 +1,5 @@
import localUnitTest from './LocalUnit.test';
export default function testsuite() {
localUnitTest();
}
+33
View File
@@ -0,0 +1,33 @@
import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium';
export default function localUnitTest() {
describe('localUnitTest', () => {
// Defines a test suite. Two parameters are supported: test suite name and test suite function.
beforeAll(() => {
// Presets an action, which is performed only once before all test cases of the test suite start.
// This API supports only one parameter: preset action function.
});
beforeEach(() => {
// Presets an action, which is performed before each unit test case starts.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: preset action function.
});
afterEach(() => {
// Presets a clear action, which is performed after each unit test case ends.
// The number of execution times is the same as the number of test cases defined by **it**.
// This API supports only one parameter: clear action function.
});
afterAll(() => {
// Presets a clear action, which is performed after all test cases of the test suite end.
// This API supports only one parameter: clear action function.
});
it('assertContain', 0, () => {
// Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function.
let a = 'abc';
let b = 'b';
// Defines a variety of assertion methods, which are used to declare expected boolean conditions.
expect(a).assertContain(b);
expect(a).assertEqual(a);
});
});
}