首次提交
@@ -0,0 +1,6 @@
|
||||
/node_modules
|
||||
/oh_modules
|
||||
/.preview
|
||||
/build
|
||||
/.cxx
|
||||
/.test
|
||||
@@ -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",
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "entry",
|
||||
"version": "1.0.0",
|
||||
"description": "Please describe the basic information.",
|
||||
"main": "",
|
||||
"author": "",
|
||||
"license": "",
|
||||
"dependencies": {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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)')
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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]]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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]]
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 配置文件 - 存放服务器IP、端口等变量参数
|
||||
*/
|
||||
export class Config {
|
||||
// 后端服务器IP地址
|
||||
static readonly BASE_URL: string = 'http://192.168.31.100:8080';
|
||||
|
||||
// ==================== AI服务配置 ====================
|
||||
// 主API(Dify格式)
|
||||
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"
|
||||
// 备用API(OpenAI格式)- 当主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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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服务器获取实时水质数据"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"layered-image":
|
||||
{
|
||||
"background" : "$media:background",
|
||||
"foreground" : "$media:foreground"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"src": [
|
||||
"pages/Index",
|
||||
"pages/Login",
|
||||
"pages/Details"
|
||||
]
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
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>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
@@ -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();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"module": {
|
||||
"name": "entry_test",
|
||||
"type": "feature",
|
||||
"deviceTypes": [
|
||||
"phone"
|
||||
],
|
||||
"deliveryWithInstall": true,
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import localUnitTest from './LocalUnit.test';
|
||||
|
||||
export default function testsuite() {
|
||||
localUnitTest();
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||