|
|
@@ -0,0 +1,401 @@
|
|
|
+import { DefaultEnum, IBestToast, RouterPage, YTButton, YTHeader, yTRouter } from 'basic';
|
|
|
+import { display } from '@kit.ArkUI';
|
|
|
+
|
|
|
+interface ResultType {
|
|
|
+ src: ResourceStr,
|
|
|
+ color: ResourceColor,
|
|
|
+ border: BorderOptions,
|
|
|
+ state: DefaultEnum
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+@Component
|
|
|
+@RouterPage
|
|
|
+struct VisionTestPage {
|
|
|
+ @State private currentIndex: number = 0
|
|
|
+ @State private resultArr: ResultType[] = Array.from<ResultType>({ length: 7 }).fill({
|
|
|
+ src: '',
|
|
|
+ color: Color.White,
|
|
|
+ border: {
|
|
|
+ color: '#FFD5D5D5',
|
|
|
+ width: 1
|
|
|
+ },
|
|
|
+ state: DefaultEnum.NULL
|
|
|
+ })
|
|
|
+ @State private isStartTest: boolean = false
|
|
|
+ @State private currentVision: string = '4.0'
|
|
|
+ private declare deviceDpi: number
|
|
|
+ private eHeightMap: Map<string, number> = this.getEHeightMap1m()
|
|
|
+ private angleArray: number[] = this.generateRandomAngleArray()
|
|
|
+
|
|
|
+ aboutToAppear() {
|
|
|
+ this.deviceDpi = display.getDefaultDisplaySync().densityDPI
|
|
|
+ }
|
|
|
+
|
|
|
+ build() {
|
|
|
+ Column() {
|
|
|
+ YTHeader({ centerComp: this.centerComp })
|
|
|
+ if (!this.isStartTest) {
|
|
|
+ Column() {
|
|
|
+ Text('1、视力表显示区:居中显示当前测试视标(E字方向随机)。\n' +
|
|
|
+ '2、方向选择区:底部设置4个方向按钮(上、下、左、右)。\n' +
|
|
|
+ '3、连对三次,累计对四次,进入更小视标测试。\n' +
|
|
|
+ '4、连错三次,累计错四次,结束测试,视力检测为次一级的视力。')
|
|
|
+ .fontColor('#FF0B8DFF')
|
|
|
+ .backgroundColor(Color.White)
|
|
|
+ .shadow({ radius: 10, color: '#17002BFF' })
|
|
|
+ .borderRadius(22)
|
|
|
+ .padding({
|
|
|
+ top: 31,
|
|
|
+ left: 14,
|
|
|
+ bottom: 29,
|
|
|
+ right: 5
|
|
|
+ })
|
|
|
+ }
|
|
|
+ .margin({ top: 43, bottom: 44 })
|
|
|
+ .padding({ left: 21, right: 19 })
|
|
|
+
|
|
|
+ YTButton({
|
|
|
+ btContent: '开始测试',
|
|
|
+ btWidth: 225,
|
|
|
+ btHeight: 46,
|
|
|
+ btLinearGradient: { colors: [['#FF0088FF', 0], ['#FF51AEFF', 1]], angle: 270 },
|
|
|
+ click: () => {
|
|
|
+ this.isStartTest = true
|
|
|
+ }
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ Column() {
|
|
|
+ Text('保持手机在眼前40厘米处,选择E的朝向')
|
|
|
+ .fontSize(16)
|
|
|
+ .fontWeight(400)
|
|
|
+ .fontColor('#FF3D3D3D')
|
|
|
+ .margin({ bottom: 17 })
|
|
|
+ Column() {
|
|
|
+ Row({ space: 3 }) {
|
|
|
+ ForEach(this.resultArr, (result: ResultType) => {
|
|
|
+ Column() {
|
|
|
+ Image(result.src)
|
|
|
+ .width(16)
|
|
|
+ }
|
|
|
+ .width(38)
|
|
|
+ .aspectRatio(1)
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
+ .backgroundColor(result.color)
|
|
|
+ .border(result.border)
|
|
|
+ .borderRadius(999)
|
|
|
+ })
|
|
|
+ }
|
|
|
+ .margin({ bottom: 32 })
|
|
|
+
|
|
|
+ Column() {
|
|
|
+ Image($rawfile('opToTypeChart/4.0_0.1_右.png'))
|
|
|
+ .height(this.mmToVp(this.eHeightMap.get(this.currentVision)!))
|
|
|
+ .rotate({ angle: this.angleArray[this.currentIndex] })
|
|
|
+ }
|
|
|
+ .width(198)
|
|
|
+ .aspectRatio(1)
|
|
|
+ .backgroundColor(Color.White)
|
|
|
+ .borderRadius(24)
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
+ .margin({ bottom: 40 })
|
|
|
+
|
|
|
+ RelativeContainer() {
|
|
|
+ Column() {
|
|
|
+ YTButton({
|
|
|
+ btWidth: 78,
|
|
|
+ btHeight: 78,
|
|
|
+ btContent: '看不清',
|
|
|
+ btFontSize: 22,
|
|
|
+ btFontWeight: 700,
|
|
|
+ btBorderRadius: 999,
|
|
|
+ btLinearGradient: { colors: [['#FF0088FF', 0], ['#FF51AEFF', 1]] },
|
|
|
+ click: () => {
|
|
|
+ this.checkContinue(DefaultEnum.FALSE)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ .id('centerComp')
|
|
|
+ .height(98)
|
|
|
+ .width(98)
|
|
|
+ .justifyContent(FlexAlign.Center)
|
|
|
+ .alignRules({
|
|
|
+ center: { anchor: '__container__', align: VerticalAlign.Center },
|
|
|
+ middle: { anchor: '__container__', align: HorizontalAlign.Center },
|
|
|
+ })
|
|
|
+
|
|
|
+ Image($r('app.media.right'))
|
|
|
+ .alignRules({
|
|
|
+ center: { anchor: '__container__', align: VerticalAlign.Center },
|
|
|
+ left: { anchor: 'centerComp', align: HorizontalAlign.End }
|
|
|
+ })
|
|
|
+ .height(37)
|
|
|
+ .onClick(() => {
|
|
|
+ if (this.angleArray[this.currentIndex] == 0) {
|
|
|
+ IBestToast.show({ message: "正确", type: "success" })
|
|
|
+ this.checkContinue(DefaultEnum.TRUE)
|
|
|
+ } else {
|
|
|
+ IBestToast.show({ message: "错误", type: "fail" })
|
|
|
+ this.checkContinue(DefaultEnum.FALSE)
|
|
|
+ }
|
|
|
+
|
|
|
+ })
|
|
|
+
|
|
|
+ Image($r('app.media.left'))
|
|
|
+ .alignRules({
|
|
|
+ center: { anchor: '__container__', align: VerticalAlign.Center },
|
|
|
+ right: { anchor: 'centerComp', align: HorizontalAlign.Start }
|
|
|
+ })
|
|
|
+ .height(37)
|
|
|
+ .onClick(() => {
|
|
|
+ if (this.angleArray[this.currentIndex] == 180) {
|
|
|
+ IBestToast.show({ message: "正确", type: "success" })
|
|
|
+ this.checkContinue(DefaultEnum.TRUE)
|
|
|
+ } else {
|
|
|
+ IBestToast.show({ message: "错误", type: "fail" })
|
|
|
+ this.checkContinue(DefaultEnum.FALSE)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ Image($r('app.media.up'))
|
|
|
+ .alignRules({
|
|
|
+ bottom: { anchor: 'centerComp', align: VerticalAlign.Top },
|
|
|
+ middle: { anchor: '__container__', align: HorizontalAlign.Center }
|
|
|
+ })
|
|
|
+ .width(37)
|
|
|
+ .onClick(() => {
|
|
|
+ if (this.angleArray[this.currentIndex] == 270) {
|
|
|
+ IBestToast.show({ message: "正确", type: "success" })
|
|
|
+ this.checkContinue(DefaultEnum.TRUE)
|
|
|
+ } else {
|
|
|
+ IBestToast.show({ message: "错误", type: "fail" })
|
|
|
+ this.checkContinue(DefaultEnum.FALSE)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ Image($r('app.media.down'))
|
|
|
+ .alignRules({
|
|
|
+ top: { anchor: 'centerComp', align: VerticalAlign.Bottom },
|
|
|
+ middle: { anchor: '__container__', align: HorizontalAlign.Center }
|
|
|
+ })
|
|
|
+ .width(37)
|
|
|
+ .onClick(() => {
|
|
|
+ if (this.angleArray[this.currentIndex] == 90) {
|
|
|
+ IBestToast.show({ message: "正确", type: "success" })
|
|
|
+ this.checkContinue(DefaultEnum.TRUE)
|
|
|
+ } else {
|
|
|
+ IBestToast.show({ message: "错误", type: "fail" })
|
|
|
+ this.checkContinue(DefaultEnum.FALSE)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ }
|
|
|
+ .height(218)
|
|
|
+ .aspectRatio(1)
|
|
|
+ }
|
|
|
+ .width('100%')
|
|
|
+ .layoutWeight(1)
|
|
|
+ }
|
|
|
+ .margin({ top: 25 })
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ .height('100%')
|
|
|
+ .width('100%')
|
|
|
+ .backgroundImage($r('app.media.bgi'))
|
|
|
+ .backgroundImageSize({ width: '100%', height: '100%' })
|
|
|
+ }
|
|
|
+
|
|
|
+ @Builder
|
|
|
+ centerComp() {
|
|
|
+ Text('视力检测')
|
|
|
+ .fontFamily('Alimama FangYuanTi VF')
|
|
|
+ .fontSize(24)
|
|
|
+ .fontWeight(700)
|
|
|
+ .fontColor('#FF0B0B0B')
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将毫米(mm)转换为鸿蒙虚拟像素(vp)
|
|
|
+ * @param mm 毫米尺寸(如 E 字高度 36.37mm)
|
|
|
+ * @param dpi 设备像素密度(每英寸点数,可从系统获取)
|
|
|
+ * @returns 对应的 vp 值(保留 2 位小数)
|
|
|
+ */
|
|
|
+ private mmToVp(mm: number): number {
|
|
|
+ // 核心公式:vp = mm × (dpi / 160)
|
|
|
+ const vp = mm * (this.deviceDpi / 160);
|
|
|
+ return parseFloat(vp.toFixed(2)); // 保留2位小数,避免浮点误差
|
|
|
+ }
|
|
|
+
|
|
|
+ private addResult(state: DefaultEnum): DefaultEnum {
|
|
|
+ switch (state) {
|
|
|
+ case DefaultEnum.TRUE:
|
|
|
+ this.resultArr[this.currentIndex] = {
|
|
|
+ src: $r('app.media.success'),
|
|
|
+ color: '#FFD9F3FC',
|
|
|
+ border: {
|
|
|
+ color: Color.Transparent,
|
|
|
+ width: 1
|
|
|
+ },
|
|
|
+ state: DefaultEnum.TRUE
|
|
|
+ }
|
|
|
+ // 检查连续3次正确错误或累计4次正确错误
|
|
|
+ return this.checkConsecutiveOrTotal()
|
|
|
+ case DefaultEnum.FALSE:
|
|
|
+ this.resultArr[this.currentIndex] = {
|
|
|
+ src: $r('app.media.error'),
|
|
|
+ color: '#FFF7F7F7',
|
|
|
+ border: {
|
|
|
+ color: Color.Transparent,
|
|
|
+ width: 1
|
|
|
+ },
|
|
|
+ state: DefaultEnum.FALSE
|
|
|
+ }
|
|
|
+ return this.checkConsecutiveOrTotal()
|
|
|
+ default:
|
|
|
+ return this.checkConsecutiveOrTotal()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private generateRandomAngleArray(): number[] {
|
|
|
+ // 候选角度集合
|
|
|
+ const candidateAngles = [0, 90, 180, 270];
|
|
|
+ // 数组长度
|
|
|
+ const length = 7;
|
|
|
+
|
|
|
+ // 生成随机数组:通过随机索引从候选集合中取元素
|
|
|
+ return Array.from<number, number>(
|
|
|
+ { length },
|
|
|
+ () => candidateAngles[Math.floor(Math.random() * candidateAngles.length)]
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ private checkContinue(state: DefaultEnum) {
|
|
|
+ switch (this.addResult(state)) {
|
|
|
+ case DefaultEnum.FALSE:
|
|
|
+ //跳转结束
|
|
|
+ yTRouter.router2TestResultPage(this.currentVision)
|
|
|
+ break
|
|
|
+ case DefaultEnum.TRUE:
|
|
|
+
|
|
|
+ if (Number(this.currentVision) < 5.3) {
|
|
|
+ this.currentVision = (Number(this.currentVision) + 0.1).toFixed(1)
|
|
|
+ } else {
|
|
|
+ yTRouter.router2TestResultPage(this.currentVision)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ this.resultArr = Array.from<ResultType>({ length: 7 }).fill({
|
|
|
+ src: '',
|
|
|
+ color: Color.White,
|
|
|
+ border: {
|
|
|
+ color: '#FFD5D5D5',
|
|
|
+ width: 1
|
|
|
+ },
|
|
|
+ state: DefaultEnum.NULL
|
|
|
+ })
|
|
|
+ this.angleArray = this.generateRandomAngleArray()
|
|
|
+ this.currentIndex = 0
|
|
|
+
|
|
|
+ break
|
|
|
+ case DefaultEnum.NULL:
|
|
|
+ this.currentIndex++
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成 5 分记录法视力值与 1 米距离 E 字高度的映射 Map
|
|
|
+ * @returns Map<视力值(字符串), 1米距离E字高度(mm,保留2位小数)>
|
|
|
+ */
|
|
|
+ private getEHeightMap1m(): Map<string, number> {
|
|
|
+ // 5 米距离下的标准 E 字高度(数据来源:GB 11533-2011 标准对数视力表)
|
|
|
+ const standard5mHeights: Record<string, number> = {
|
|
|
+ "4.0": 36.37,
|
|
|
+ "4.1": 30.31,
|
|
|
+ "4.2": 25.00,
|
|
|
+ "4.3": 21.05,
|
|
|
+ "4.4": 18.18,
|
|
|
+ "4.5": 14.54,
|
|
|
+ "4.6": 12.12,
|
|
|
+ "4.7": 9.09,
|
|
|
+ "4.8": 7.27,
|
|
|
+ "4.9": 6.06,
|
|
|
+ "5.0": 5.45,
|
|
|
+ "5.1": 4.55,
|
|
|
+ "5.2": 3.64,
|
|
|
+ "5.3": 3.03,
|
|
|
+ };
|
|
|
+
|
|
|
+ // 1 米距离是 5 米的 1/5,因此高度缩放比例为 0.2
|
|
|
+ const scale = 0.2;
|
|
|
+
|
|
|
+ // 创建并填充 Map
|
|
|
+ const eHeightMap1m = new Map<string, number>();
|
|
|
+ for (const key of Object.keys(standard5mHeights)) {
|
|
|
+ const height1m = Number((standard5mHeights[key] * scale).toFixed(2)); // 保留2位小数
|
|
|
+ eHeightMap1m.set(key, height1m);
|
|
|
+ }
|
|
|
+
|
|
|
+ return eHeightMap1m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private checkConsecutiveOrTotal(): DefaultEnum {
|
|
|
+ let rightConsecutiveCount = 0
|
|
|
+
|
|
|
+ let rightTotalCount = 0
|
|
|
+
|
|
|
+ let errConsecutiveCount = 0
|
|
|
+
|
|
|
+ let errTotalCount = 0
|
|
|
+
|
|
|
+ // 检查累计正确次数
|
|
|
+ for (let i = 0; i <= this.currentIndex; i++) {
|
|
|
+ if (this.resultArr[i].state === DefaultEnum.TRUE) {
|
|
|
+ rightTotalCount++
|
|
|
+ rightConsecutiveCount++
|
|
|
+ } else {
|
|
|
+ rightConsecutiveCount = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ if (this.resultArr[i].state === DefaultEnum.FALSE) {
|
|
|
+ errTotalCount++
|
|
|
+ errConsecutiveCount++
|
|
|
+ } else {
|
|
|
+ errConsecutiveCount = 0
|
|
|
+ }
|
|
|
+
|
|
|
+ if (errConsecutiveCount >= 3) {
|
|
|
+ return DefaultEnum.FALSE
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有连续3次正确
|
|
|
+ if (rightConsecutiveCount >= 3) {
|
|
|
+ return DefaultEnum.TRUE
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (errTotalCount >= 4) {
|
|
|
+ return DefaultEnum.FALSE
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果累计4次正确
|
|
|
+ if (rightTotalCount >= 4) {
|
|
|
+ return DefaultEnum.TRUE
|
|
|
+ }
|
|
|
+
|
|
|
+ return DefaultEnum.NULL
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@Builder
|
|
|
+function VisionTestBuilder() {
|
|
|
+ NavDestination() {
|
|
|
+ VisionTestPage()
|
|
|
+ }
|
|
|
+ .hideTitleBar(true)
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+
|