From 2620edce31b44f38cb59210970a9ad86428be356 Mon Sep 17 00:00:00 2001
From: Super12138 <70494801+Super12138@users.noreply.github.com>
Date: Wed, 28 Aug 2024 15:28:06 +0800
Subject: [PATCH] First Commit
---
.github/workflows/deploy.yml | 57 ++++++
.gitignore | 27 +++
index.html | 30 +++
list.html | 2 +
package.json | 33 ++++
settings.html | 27 +++
src/global.d.ts | 21 +++
src/index.css | 213 +++++++++++++++++++++
src/index.ts | 176 ++++++++++++++++++
src/interfaces.ts | 346 +++++++++++++++++++++++++++++++++++
src/list/index.ts | 76 ++++++++
src/settings/index.ts | 49 +++++
src/test/index.ts | 322 ++++++++++++++++++++++++++++++++
src/test/option.ts | 35 ++++
src/test/question.ts | 40 ++++
src/test/scoring.ts | 103 +++++++++++
src/utils/LogHelper.ts | 26 +++
src/utils/element.ts | 19 ++
src/utils/localstorage.ts | 17 ++
src/utils/network.ts | 5 +
src/utils/notices.ts | 82 +++++++++
src/vite-env.d.ts | 1 +
test.html | 52 ++++++
tsconfig.json | 27 +++
typedoc.json | 4 +
vite.config.ts | 71 +++++++
26 files changed, 1861 insertions(+)
create mode 100644 .github/workflows/deploy.yml
create mode 100644 .gitignore
create mode 100644 index.html
create mode 100644 list.html
create mode 100644 package.json
create mode 100644 settings.html
create mode 100644 src/global.d.ts
create mode 100644 src/index.css
create mode 100644 src/index.ts
create mode 100644 src/interfaces.ts
create mode 100644 src/list/index.ts
create mode 100644 src/settings/index.ts
create mode 100644 src/test/index.ts
create mode 100644 src/test/option.ts
create mode 100644 src/test/question.ts
create mode 100644 src/test/scoring.ts
create mode 100644 src/utils/LogHelper.ts
create mode 100644 src/utils/element.ts
create mode 100644 src/utils/localstorage.ts
create mode 100644 src/utils/network.ts
create mode 100644 src/utils/notices.ts
create mode 100644 src/vite-env.d.ts
create mode 100644 test.html
create mode 100644 tsconfig.json
create mode 100644 typedoc.json
create mode 100644 vite.config.ts
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..ef8546b
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,57 @@
+name: Deploy
+
+on:
+ push:
+ branches: ["main"]
+ pull_request:
+ branches: ["main"]
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ pages: write
+ id-token: write
+
+concurrency:
+ group: pages
+ cancel-in-progress: false
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4.1.7
+ with:
+ fetch-depth: 0
+ - name: Setup Node
+ uses: actions/setup-node@v4.0.3
+ with:
+ node-version: 20
+
+ - name: Setup Pages
+ uses: actions/configure-pages@v5.0.0
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Build
+ run: npm run build
+
+ - name: Upload artifact
+ uses: actions/upload-pages-artifact@v3.0.1
+ with:
+ path: dist
+
+ deploy:
+ environment:
+ name: github-pages
+ url: ${{ steps.deployment.outputs.page_url }}
+ needs: build
+ runs-on: ubuntu-latest
+ name: Deploy
+ steps:
+ - name: Deploy to GitHub Pages
+ id: deployment
+ uses: actions/deploy-pages@v4.0.5
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..746efdd
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,27 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Cache
+package-lock.json
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..61a2ac7
--- /dev/null
+++ b/index.html
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+ 问心
+
+
+
+
+
+
+
+
+
+
+
+ 问心
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/list.html b/list.html
new file mode 100644
index 0000000..b9127fe
--- /dev/null
+++ b/list.html
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..6363ecf
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "ask-yourself",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/Super12138/Ask-Yourself.git"
+ },
+ "author": "Super12138",
+ "license": "GPL-3.0",
+ "bugs": {
+ "url": "https://github.com/Super12138/Ask-Yourself/issues"
+ },
+ "homepage": "https://github.com/Super12138/Ask-Yourself#readme",
+ "devDependencies": {
+ "@mdui/icons": "^1.0.2",
+ "@types/node": "^22.4.1",
+ "typedoc": "^0.26.6",
+ "typedoc-plugin-markdown": "^4.2.5",
+ "typescript": "^5.5.4",
+ "vite": "^5.4.2",
+ "vite-plugin-html": "^3.2.2"
+ },
+ "dependencies": {
+ "mdui": "^2.1.2"
+ }
+}
\ No newline at end of file
diff --git a/settings.html b/settings.html
new file mode 100644
index 0000000..f469f7b
--- /dev/null
+++ b/settings.html
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 本项目旨在解决网上寻找心理自评量表困难的问题。
+
+ 在项目的所有量表中,都会注明相关的参考信息。如果你在使用过程中发现错误/遗漏,欢迎通过 GitHub Issues 进行反馈/使用 Pull Request 进行提交
+ 本项目所有的量表均为收集互联网上的信息并整理,不对网站内任何量表具有版权。如侵权请联系我,我会及时删除。
+ 所有量表的测量结果仅供参考,不作为任何医学诊断的依据。
+
+
+
+ 由 Super12138 开发,网站本体遵循 GPL 3.0 协议在 GitHub 上开源
+ 量表内容的版权归研究者所有
+ ❤️
+
+ 确定
+
\ No newline at end of file
diff --git a/src/global.d.ts b/src/global.d.ts
new file mode 100644
index 0000000..06707bf
--- /dev/null
+++ b/src/global.d.ts
@@ -0,0 +1,21 @@
+/**
+ * 应用版本名称
+ */
+declare const VERSION_NAME: string;
+
+/**
+ * 应用运行环境
+ * * dev - 开发
+ * * web - 在线
+ */
+declare const VARIANT: string;
+
+/**
+ * 版本提交 Hash
+ */
+declare const COMMIT_HASH: string;
+
+/**
+ * 应用版本号
+ */
+declare const VERSION_CODE: string;
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..7de8bba
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,213 @@
+/* 页面加载动画 */
+body {
+ opacity: 0;
+ height: 100%;
+ margin: 8px;
+}
+
+body.ready {
+ opacity: 1;
+ transition: 0.15s opacity;
+}
+
+/* 充满页面 */
+mdui-layout {
+ height: calc(100vh - 17px);
+}
+
+/* 题库列表自适应宽度 */
+mdui-list {
+ width: 100%;
+}
+
+mdui-list-subheader {
+ width: 100%;
+}
+
+mdui-list-item {
+ flex: 0 0 25%;
+ /* display: flex; */
+ /* align-items: center; */
+}
+
+mdui-badge {
+ margin-left: 5px;
+}
+
+mdui-radio {
+ width: 100%;
+}
+
+h5 {
+ line-height: var(--mdui-typescale-headline-medium-line-height);
+ font-size: var(--mdui-typescale-headline-medium-size);
+ letter-spacing: var(--mdui-typescale-headline-medium-tracking);
+ font-weight: var(--mdui-typescale-headline-medium-weight);
+}
+
+p {
+ line-height: var(--mdui-typescale-body-large-line-height);
+ font-size: var(--mdui-typescale-body-large-size);
+ letter-spacing: var(--mdui-typescale-body-large-tracking);
+ font-weight: var(--mdui-typescale-body-large-weight);
+}
+
+strong {
+ line-height: var(--mdui-typescale-body-large-line-height);
+ font-size: var(--mdui-typescale-body-large-size);
+ letter-spacing: var(--mdui-typescale-body-large-tracking);
+}
+
+.items-container {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+/* 答题页面样式 */
+#testContainer {
+ display: none;
+ /*
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh; */
+ /* 或者设置为具体的高度 */
+}
+
+#controlArea {
+ margin: 5% 2% 2% 2%;
+}
+
+.prev-btn {
+ float: left;
+ display: none;
+}
+
+.next-btn {
+ float: right;
+}
+
+/* 页面描述 */
+.questionnaire-description {
+ /* */
+ margin-bottom: 2px;
+}
+
+/* 页面提示 */
+.questionnaire-tips {
+ /* line-height: var(--mdui-typescale-body-large-line-height);
+ font-size: var(--mdui-typescale-body-large-size);
+ letter-spacing: var(--mdui-typescale-body-large-tracking); */
+ margin-top: 5px;
+}
+
+/* 页面描述 */
+.question {
+ /* line-height: var(--mdui-typescale-body-large-line-height);
+ font-size: var(--mdui-typescale-body-large-size);
+ letter-spacing: var(--mdui-typescale-body-large-tracking);
+ font-weight: var(--mdui-typescale-body-large-weight); */
+ margin-bottom: 8px;
+}
+
+.progress-tip {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 2%;
+}
+
+.progress-bar {
+ margin-left: 10px;
+}
+
+.center{
+ text-align: center;
+}
+
+#nullTip {
+ display: none;
+ width: 100%;
+}
+
+#testArea {
+ display: none;
+}
+
+#references {
+ margin-top: 5%;
+}
+
+#resultArea {
+ margin-top: 3%;
+ display: none;
+}
+
+#backBtn {
+ display: none;
+}
+
+.result-tips {
+ line-height: var(--mdui-typescale-body-large-line-height);
+ font-size: var(--mdui-typescale-body-large-size);
+ letter-spacing: var(--mdui-typescale-body-large-tracking);
+}
+
+.red {
+ color: #B10000;
+}
+
+.orange {
+ color: #E65100;
+}
+
+.yellow {
+ color: #FFA000;
+}
+
+.green {
+ color: #1B5E20;
+}
+
+@media (prefers-color-scheme: dark) {
+ .red {
+ color: #D32F2F;
+ }
+
+ .orange {
+ color: #F57F17;
+ }
+
+ .yellow {
+ color: #e9d200;
+ }
+
+ .green {
+ color: #00C853;
+ }
+}
+
+@media (max-width: 1000px) {
+ .items-container {
+ flex-basis: 33.33%;
+ /* 中大屏幕3列 */
+ }
+}
+
+@media (max-width: 900px) {
+ .items-container {
+ flex-basis: 50%;
+ /* 中等屏幕2列 */
+ }
+}
+
+@media (max-width: 600px) {
+ .items-container {
+ flex-direction: column;
+ /* 小屏幕单列 */
+ }
+
+ mdui-list-item {
+ flex: 0 0 100%;
+ /* 小屏幕单列,item占满宽度 */
+ }
+}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..675b715
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,176 @@
+import 'mdui/components/button-icon.js';
+import 'mdui/components/layout-item.js';
+import 'mdui/components/layout-main.js';
+import 'mdui/components/layout.js';
+import 'mdui/components/navigation-bar-item.js';
+import 'mdui/components/navigation-bar.js';
+import 'mdui/components/navigation-rail-item.js';
+import 'mdui/components/navigation-rail.js';
+import 'mdui/components/top-app-bar-title.js';
+import 'mdui/components/top-app-bar.js';
+import 'mdui/mdui.css';
+
+import type { ButtonIcon } from 'mdui/components/button-icon.js';
+import type { LayoutMain } from 'mdui/components/layout-main.js';
+import type { NavigationBarItem } from 'mdui/components/navigation-bar-item.js';
+import type { NavigationBar } from 'mdui/components/navigation-bar.js';
+import type { NavigationRailItem } from 'mdui/components/navigation-rail-item.js';
+import type { NavigationRail } from 'mdui/components/navigation-rail.js';
+import type { TopAppBarTitle } from 'mdui/components/top-app-bar-title.js';
+
+import '@mdui/icons/arrow-back.js';
+import '@mdui/icons/brush--outlined.js';
+import '@mdui/icons/list--outlined.js';
+import '@mdui/icons/settings--outlined.js';
+import { PageItem } from './interfaces';
+import { hide, show } from './utils/element';
+import { getFile } from './utils/network';
+import { showDisclaimerDialog } from './utils/notices';
+
+const navigationRail: NavigationRail = document.querySelector('#navigationRail')!;
+const container: LayoutMain = document.querySelector('#container')!;
+const navigationBar: NavigationBar = document.querySelector('#navigationBar')!;
+const backBtn: ButtonIcon = document.querySelector('#backBtn')!;
+const appTitle: TopAppBarTitle = document.querySelector('#appTitle')!;
+
+const pages: PageItem[] = [
+ {
+ name: '题库',
+ value: 'list',
+ icon: 'list--outlined',
+ url: 'list.html'
+ },
+ {
+ name: '答题',
+ value: 'test',
+ icon: 'brush--outlined',
+ url: 'test.html'
+ },
+ {
+ name: '设置',
+ value: 'settings',
+ icon: 'settings--outlined',
+ url: 'settings.html'
+ }
+]
+
+window.addEventListener('resize', () => {
+ checkWidth();
+});
+
+const url: URL = new URL(window.location.href);
+const currentPage: string | null = url.searchParams.get("page");
+
+let page: PageItem;
+
+window.addEventListener('DOMContentLoaded', async () => {
+ // 设置当前页面
+ for (let i = 0; i < pages.length; i++) {
+ if (pages[i].value === currentPage) {
+ page = pages[i];
+ break;
+ }
+ }
+
+ if (currentPage === null) {
+ page = pages[0];
+ }
+
+ for (let i = 0; i < pages.length; i++) {
+ navigationRail.appendChild(makeActionElement(pages[i], 'mdui-navigation-rail-item'));
+ navigationBar.appendChild(makeActionElement(pages[i], 'mdui-navigation-bar-item'));
+ }
+
+ const pageData = await getFile(page.url);
+
+ navigationRail.value = page.value;
+ navigationBar.value = page.value;
+ document.title = `${page.name} - 问心`;
+ appTitle.textContent = page.name;
+
+ checkWidth();
+
+ // 创建 MutationObserver 实例并提供回调函数
+ const observer = new MutationObserver((mutationsList, observer) => {
+ for (const mutation of mutationsList) {
+ if (mutation.type === 'childList') {
+ // 检查是否插入了新内容
+ if (mutation.addedNodes.length > 0) {
+ // 页面加载完成(看看要不要删掉)
+ switch (page.value) {
+ case 'list':
+ document.dispatchEvent(new Event('listPageLoaded'));
+ break;
+
+ case 'test':
+ document.dispatchEvent(new Event('testPageLoaded'));
+ show(backBtn);
+ break;
+
+ case 'settings':
+ document.dispatchEvent(new Event('settingsPageLoaded'));
+ show(backBtn);
+ break;
+
+ default:
+ document.dispatchEvent(new Event('listPageLoaded'));
+ show(backBtn);
+ break;
+ }
+ observer.disconnect();
+ }
+ }
+ }
+ });
+
+ // 启动观察
+ observer.observe(container, { childList: true, subtree: true });
+
+ container.innerHTML = pageData;
+ showDisclaimerDialog();
+
+ document.body.classList.add('ready');
+});
+
+/**
+ * 生成侧边/底部导航栏的元素
+ * @param item 单个导航元素
+ * @param type 可选值:mdui-navigation-bar-item | mdui-navigation-rail-item
+ * @returns
+ */
+function makeActionElement(item: PageItem, type: string) {
+ const element: NavigationBarItem | NavigationRailItem = document.createElement(type) as NavigationBarItem | NavigationRailItem;
+ element.setAttribute("value", item.value);
+ element.addEventListener('click', () => {
+ if (item.value !== page.value) {
+ window.location.href = `?page=${item.value}`;
+ }
+ });
+
+ const iconElement: HTMLElement = document.createElement(`mdui-icon-${item.icon}`);
+ iconElement.setAttribute("slot", "icon");
+ element.appendChild(iconElement);
+
+ const nameTextNode: Text = document.createTextNode(item.name);
+ element.appendChild(nameTextNode);
+
+ return element;
+}
+
+/**
+ * 检查窗口宽度并判断是否显示侧边栏/底部导航栏
+ */
+function checkWidth() {
+ if (window.innerWidth < 840) {
+ hide(navigationRail);
+ navigationBar.style.display = '';
+ } else {
+ show(navigationRail);
+ navigationBar.style.display = 'none';
+ }
+}
+
+// 当返回键被按下时返回上一个页面
+backBtn.addEventListener('click', () => {
+ window.history.back();
+});
\ No newline at end of file
diff --git a/src/interfaces.ts b/src/interfaces.ts
new file mode 100644
index 0000000..de4fd6f
--- /dev/null
+++ b/src/interfaces.ts
@@ -0,0 +1,346 @@
+import type { ListItem } from 'mdui/components/list-item.js';
+
+export type Colors = 'green' | 'yellow' | 'orange' | 'red';
+export type Languages = 'zh' | 'en';
+export type Method = 'plus' | 'average' | 'multiply';
+
+/**
+ * 导航栏元素接口
+ */
+export interface PageItem {
+ /**
+ * 名称
+ */
+ name: string;
+ /**
+ * 页面代号
+ */
+ value: string;
+ /**
+ * Item 图标
+ */
+ icon: string;
+ /**
+ * 页面 URL
+ */
+ url: string;
+}
+
+/**
+ * 按钮内容接口
+ */
+export interface ButtonType {
+ /**
+ * 按钮文本
+ */
+ name: string;
+ /**
+ * 按钮图标
+ */
+ icon: string;
+}
+
+/**
+ * 量表列表接口
+ */
+export interface QuestionnaireListItem extends ListItem {
+ /**
+ * 量表内部名称(用于传参)
+ */
+ value: string;
+ /**
+ * 量表介绍(用于向用户展示)
+ */
+ description: string;
+ /**
+ * 量表语言,目前只支持`中文`和`英文`
+ * * `zh` 中文
+ * * `en` 英文
+ */
+ lang: Languages | string;
+ /**
+ * 是否为新增列表
+ * 如果是新增列表会在列表项目右侧显示一个`新`
+ */
+ new: boolean;
+}
+
+/**
+ * 量表列表接口
+ */
+export interface QuestionnaireList {
+ /**
+ * 量表介绍(用于向用户展示)
+ */
+ name: string;
+ /**
+ * 量表内部名称(用于传参)
+ */
+ value: string;
+ /**
+ * 量表语言,目前只支持`中文`和`英文`
+ * * `zh` 中文
+ * * `en` 英文
+ */
+ lang: Languages | string;
+ /**
+ * 是否为新增列表
+ * 如果是新增列表会在列表项目右侧显示一个`新`
+ */
+ new: boolean;
+}
+
+/**
+ * 量表列表分类接口
+ */
+export interface Category {
+ /**
+ * 类别名称
+ */
+ name: string;
+ /**
+ * 量表
+ */
+ questionnaires: QuestionnaireList[];
+}
+
+/**
+ * 量表列表接口
+ */
+export interface QuestionnairesList {
+ /**
+ * 量表列表版本
+ */
+ version: string;
+ /**
+ * 量表分类
+ */
+ categories: Category[];
+}
+
+
+// 量表文件类型接口
+
+/**
+ * 单个选项
+ */
+export interface OptionItem {
+ /**
+ * 选项名称
+ */
+ name: string;
+ /**
+ * 选项分值
+ */
+ score: number;
+}
+
+/**
+ * 单个题目
+ */
+export interface QuestionItem {
+ /**
+ * 题目id(可选,不作为真正题目id)
+ */
+ id?: number;
+ /**
+ * 题目组id
+ * 用于后续评分和匹配分值范围,需要完全对应
+ */
+ groupId: number;
+ /**
+ * 题目内容(题干)
+ */
+ content: string;
+}
+
+/**
+ * 单个评分标准
+ */
+export interface Criterion {
+ /**
+ * 评分组组id
+ * 需与题目的题目组id完全对应
+ * @see QuestionItem.groupId
+ */
+ groupId: number;
+ /**
+ * 计算方法
+ * * `plus` 全部求和
+ * * `average` 求和后取平均分(只返回平均分)
+ * * `multiply` 求和后将结果 × 2(只返回计算后的结果)
+ */
+ method: Method | string;
+}
+/**
+ * 单个分值范围
+ */
+export interface Range {
+ /**
+ * 范围名称
+ */
+ name: string;
+ /**
+ * 显示文字的颜色,可选值:
+ * * green 绿色
+ * * yellow 黄色
+ * * orange 橙色
+ * * red 红色
+ * * 啥也不填就是默认颜色
+ */
+ color: Colors | string;
+ /**
+ * 范围最小值(可以取等)
+ */
+ min: number;
+ /**
+ * 范围最大值(可以取等)
+ */
+ max: number;
+}
+
+/**
+ * 分值范围(带分组)
+ */
+export interface Ranges {
+ /**
+ * 范围组id
+ * 需与题目的题目组id完全对应
+ * @see QuestionItem.groupId
+ */
+ groupId: number;
+ /**
+ * 组名称
+ */
+ name: string;
+ /**
+ * 单个评分范围
+ * @type {Range[]}
+ */
+ ranges: Range[];
+}
+/**
+ * 评分标准组
+ */
+export interface Scoring {
+ /**
+ * 单个评分标准
+ */
+ criteria: Criterion[];
+ ranges: Ranges[];
+}
+
+/**
+ * 量表题目文件
+ */
+export interface QuestionnaireFile {
+ /**
+ * 量表名称
+ */
+ name: string;
+ /**
+ * 量表介绍
+ */
+ description: string;
+ /**
+ * 作答提示
+ */
+ answerTips: string;
+ /**
+ * 结果提示&解析,支持 `HTML` 标签,用 `innerHTML` 上屏
+ */
+ resultTips: string;
+ /**
+ * 题目来源(引用)
+ * 比如从哪儿找到题目,从哪儿找的评分标准之类的
+ * @type {string[]}
+ */
+ references: string[];
+ /**
+ * 题目选项
+ * @type {OptionItem[]}
+ */
+ options: OptionItem[];
+ /**
+ * 题目
+ * @type {QuestionItem[]}
+ */
+ questions: QuestionItem[];
+ /**
+ * 评分标准
+ * @type {Scoring}
+ */
+ scoring: Scoring;
+}
+
+/**
+ * 计算完成后评分接口
+ */
+export interface Score {
+ /**
+ * 评分组id
+ */
+ groupId: number;
+ /**
+ * 评分
+ */
+ score: number;
+ /**
+ * 测定范围
+ */
+ range: string;
+}
+
+/**
+ * 得分结果存储类
+ */
+export interface QuestionResult {
+ /**
+ * 题目组id
+ */
+ name: string;
+ /**
+ * 该题得分
+ */
+ value: string;
+}
+
+/**
+ * 得分计算结果
+ */
+export interface ScoreResult extends BasicScoreResult {
+ /**
+ * 得分所在范围的名称
+ */
+ range: string;
+ /**
+ * 得分所在范围的名称所显示文字的颜色,可选值:
+ * * green 绿色
+ * * yellow 黄色
+ * * orange 橙色
+ * * red 红色
+ * * 啥也不填就是默认颜色
+ */
+ color: Colors | string;
+}
+
+/**
+ * 得分计算结果基类
+ */
+export interface BasicScoreResult {
+ /**
+ * 得分所在组项目名称
+ */
+ name: string;
+ /**
+ * 得分
+ */
+ result: number;
+}
+
+/**
+ * 分好组的数据
+ */
+export interface GroupedData {
+ [key: string]: string[];
+}
\ No newline at end of file
diff --git a/src/list/index.ts b/src/list/index.ts
new file mode 100644
index 0000000..7154228
--- /dev/null
+++ b/src/list/index.ts
@@ -0,0 +1,76 @@
+import 'mdui/components/badge.js';
+import 'mdui/components/list-item.js';
+import 'mdui/components/list-subheader.js';
+import 'mdui/components/list.js';
+import 'mdui/components/text-field.js';
+
+import type { Badge } from 'mdui/components/badge.js';
+import type { ListSubheader } from 'mdui/components/list-subheader.js';
+import type { List } from 'mdui/components/list.js';
+import { Category, QuestionnaireList, QuestionnaireListItem, QuestionnairesList } from '../interfaces';
+import { hide } from '../utils/element';
+// import { TextField } from 'mdui/components/text-field.js';
+import { LogHelper } from '../utils/LogHelper';
+import { getFile } from '../utils/network';
+
+const languages: string[] = ['中文版', '英文版'];
+const logHelper = LogHelper.getInstance();
+
+document.addEventListener('listPageLoaded', async () => {
+ const mduiList: List = document.querySelector('#questionnaireList')!;
+ // const searchBar: TextField = document.querySelector('#searchBar')!;
+ getFile('https://cdn.jsdelivr.net/gh/Super12138/AY-Questionnaires-DB/list.json')
+ .then((response: string) => {
+ const json: QuestionnairesList = JSON.parse(response);
+ const catogories: Category[] = json.categories; // 获取分组
+
+ for (const group in catogories) { // 遍历大组内的每一个小组
+ const listContainer: HTMLDivElement = document.createElement('div'); // 创建包裹每个列表项的容器(响应式布局)
+ listContainer.className = 'items-container';
+
+ for (const item of catogories[group].questionnaires) { // 遍历每个小问卷组的每一个量表的信息
+ makeListElement(item, listContainer); // 生成列表项
+ }
+
+ const subheader: ListSubheader = document.createElement('mdui-list-subheader'); // 列表子标题
+ subheader.textContent = catogories[group].name; // 设置子标题为每个分组的名称
+
+ mduiList.appendChild(subheader); // 添加子标题
+ mduiList.appendChild(listContainer); // 添加列表项
+ }
+ })
+ .catch((error) => {
+ logHelper.error(error);
+ const errorElement: HTMLParagraphElement = document.createElement('p');
+ errorElement.style.width = '100%';
+ errorElement.style.textAlign = 'center';
+ errorElement.innerHTML = `加载列表时出现问题:${error}
`
+ document.querySelector('#container')!.appendChild(errorElement);
+ hide(mduiList);
+ })
+});
+
+function makeListElement(item: QuestionnaireList, listContainer: HTMLElement) {
+ // 创建列表项
+ const listItem: QuestionnaireListItem = document.createElement('mdui-list-item') as QuestionnaireListItem;
+ listItem.alignment = 'center'; // 居中对齐
+ listItem.value = item.value; // 设置传参名称
+ listItem.textContent = item.name; // 设置量表名称
+ listItem.description = item.lang === "en" ? languages[1] : languages[0]; // 判断语言
+ listItem.addEventListener('click', () => {
+ window.location.href = `?page=test&name=${item.value}`; // 点击跳转答题页面
+ })
+
+ // 判断是否为新增列表
+ if (item.new) {
+ const badge: Badge = document.createElement('mdui-badge');
+ badge.variant = 'large';
+ badge.textContent = '新';
+ // // 将标签上标
+ // const sup: HTMLElement = document.createElement('sup');
+ // sup.appendChild(badge);
+ listItem.appendChild(badge);
+ }
+
+ listContainer.appendChild(listItem);
+}
\ No newline at end of file
diff --git a/src/settings/index.ts b/src/settings/index.ts
new file mode 100644
index 0000000..dff0bb2
--- /dev/null
+++ b/src/settings/index.ts
@@ -0,0 +1,49 @@
+import 'mdui/components/dialog.js';
+import 'mdui/components/dropdown.js';
+import 'mdui/components/switch.js';
+import 'mdui/components/divider.js';
+
+import '@mdui/icons/delete--outlined.js';
+import '@mdui/icons/info--outlined.js';
+
+import type { Button } from 'mdui/components/button.js';
+import type { Dialog } from 'mdui/components/dialog.js';
+import type { ListItem } from 'mdui/components/list-item.js';
+
+import { showClearAllDataDialog } from '../utils/notices';
+import { getFile } from '../utils/network';
+import { QuestionnairesList } from '../interfaces';
+import { LogHelper } from '../utils/LogHelper';
+
+const logHelper = LogHelper.getInstance();
+
+document.addEventListener('settingsPageLoaded', async () => {
+ const clearAllDataItem: ListItem = document.querySelector('#clearAllDataItem')!;
+
+ const aboutItem: ListItem = document.querySelector('#aboutItem')!;
+ const aboutDialog: Dialog = document.querySelector('#about')!;
+ const aboutCloseBtn: Button = document.querySelector('#aboutCloseBtn')!;
+ const appVersion: HTMLParagraphElement = document.querySelector('#appVersion')!;
+ const listVersion: HTMLParagraphElement = document.querySelector('#listVersion')!;
+ appVersion.innerHTML = `网站版本:${VERSION_NAME}-${VARIANT}-${COMMIT_HASH} (${VERSION_CODE})`;
+
+ getFile('https://cdn.jsdelivr.net/gh/Super12138/AY-Questionnaires-DB/list.json')
+ .then((response: string) => {
+ const json: QuestionnairesList = JSON.parse(response);
+
+ listVersion.innerHTML = `题库版本:${json.version}`;
+ })
+ .catch(error => logHelper.log(error));
+
+ clearAllDataItem.addEventListener('click', () => {
+ showClearAllDataDialog();
+ });
+
+ aboutItem.addEventListener('click', () => {
+ aboutDialog.open = true;
+ });
+
+ aboutCloseBtn.addEventListener('click', () => {
+ aboutDialog.open = false;
+ });
+});
\ No newline at end of file
diff --git a/src/test/index.ts b/src/test/index.ts
new file mode 100644
index 0000000..60b2f51
--- /dev/null
+++ b/src/test/index.ts
@@ -0,0 +1,322 @@
+import 'mdui/components/button.js';
+import 'mdui/components/circular-progress.js';
+import 'mdui/components/linear-progress.js';
+import 'mdui/components/radio-group.js';
+import 'mdui/components/radio.js';
+
+import type { ButtonIcon } from 'mdui/components/button-icon.js';
+import type { Button } from 'mdui/components/button.js';
+import type { LinearProgress } from 'mdui/components/linear-progress.js';
+import type { TopAppBarTitle } from 'mdui/components/top-app-bar-title.js';
+
+import '@mdui/icons/arrow-back--outlined.js';
+import '@mdui/icons/arrow-forward--outlined.js';
+import '@mdui/icons/check--outlined.js';
+
+import { RadioGroup } from 'mdui/components/radio-group.js';
+import { ButtonType, Criterion, GroupedData, QuestionnaireFile, QuestionResult, Ranges, ScoreResult, Scoring } from '../interfaces';
+import { hide, show } from '../utils/element';
+import { showKeyboardNotice } from '../utils/notices';
+import { Question } from './question';
+import { getScore, SCL90Score } from './scoring';
+import { LogHelper } from '../utils/LogHelper';
+import { getFile } from '../utils/network';
+
+const appTitle: TopAppBarTitle = document.querySelector('#appTitle')!;
+const url: URL = new URL(window.location.href);
+const questionnaire: string | null = url.searchParams.get("name");
+const backBtn: ButtonIcon = document.querySelector('#backBtn')!;
+const logHelper = LogHelper.getInstance();
+
+let nextButtonType: number = 1; // 1: 下一题 | 2: 开始 | 3: 提交
+let currentQuestion: number = 0;
+
+let questions = [];
+
+const buttonType: ButtonType[] = [
+ {
+ name: '上一题',
+ icon: 'arrow-back--outlined'
+ },
+ {
+ name: '下一题',
+ icon: 'arrow-forward--outlined'
+ },
+ {
+ name: '开始',
+ icon: 'arrow-forward--outlined'
+ },
+ {
+ name: '提交',
+ icon: 'check--outlined'
+ }
+];
+
+document.addEventListener('testPageLoaded', async () => {
+ const container: HTMLDivElement = document.querySelector('#testContainer')!;
+ const nullTip: HTMLParagraphElement = document.querySelector('#nullTip')!;
+ const loadingTip: HTMLDivElement = document.querySelector('#loadingTip')!;
+ const referencesElement: HTMLDivElement = document.querySelector('#references')!;
+ const previousBtn: Button = document.querySelector('.prev-btn')!;
+ const nextBtn: Button = document.querySelector('.next-btn')!;
+ const progressBar: LinearProgress = document.querySelector('.progress-bar')!;
+ const progressText: HTMLSpanElement = document.querySelector('#progressText')!;
+
+ const testArea: HTMLDivElement = document.querySelector('#testArea')!;
+ const controlArea: HTMLDivElement = document.querySelector('#controlArea')!;
+ const introPart: HTMLDivElement = document.querySelector('#introPart')!;
+ const resultArea: HTMLDivElement = document.querySelector('#resultArea')!;
+
+ // 将“下一题”按钮设置为“开始”
+ setUpNextButton(nextBtn, 2);
+
+ // 获取量表json
+ getFile(`https://cdn.jsdelivr.net/gh/Super12138/AY-Questionnaires-DB/questionnaires/${questionnaire}.json`)
+ .then((response: string) => {
+ // 加载完了显示答题页面,隐藏加载提示
+ show(container);
+ hide(loadingTip);
+ const json: QuestionnaireFile = JSON.parse(response); // 解析量表json
+ const jsonName: string = json.name;
+ appTitle.textContent = jsonName; // 将标题设置为问卷名称
+
+ document.title = `${jsonName} - 问心`;
+
+ const questionnaireTotal: number = json.questions.length; // 量表试题总数
+ progressBar.max = questionnaireTotal; // 将进度条最大值设置为量表试题总数
+
+ let generatedId: number = 0;
+ for (const question of json.questions) {
+ generatedId += 1; // 遍历题目并添加到question数组
+ const questionItem: Question = new Question(generatedId, question.groupId, json.options, question.content);
+ testArea.appendChild(questionItem.html); // 题目上屏
+ hide(questionItem.html); // 在还没有点击开始按钮前先把题目隐藏起来
+ questions.push(questionItem);
+ }
+
+ document.querySelector('#questionnaireDescription')!.textContent = json.description; // 将问卷描述设置为问卷描述
+ document.querySelector('#questionnaireTips')!.textContent = json.answerTips; // 将提示设置为问卷提示
+ for (const links of json.references) { // 将引用内容上屏
+ const a: HTMLAnchorElement = document.createElement('a');
+ const br: HTMLBRElement = document.createElement('br');
+ a.target = '_blank'; // 在新页面打开链接
+ a.href = links;
+ a.textContent = links;
+ referencesElement.appendChild(a);
+ referencesElement.appendChild(br);
+ }
+
+ /**
+ * 更新进度条和进度提示文本
+ * @return {void}
+ */
+ const updateProgress = () => {
+ progressBar.value = currentQuestion + 1;
+ progressText.textContent = `${currentQuestion + 1}/${questionnaireTotal}`;
+ };
+
+ function checkQuestionChecked(id: number) {
+ const radioGroup: RadioGroup = document.querySelector(`#questions-${id}`)!;
+ if (radioGroup.value) {
+ nextBtn.disabled = false;
+ } else {
+ nextBtn.disabled = true;
+ const radioChangeListener = () => {
+ nextBtn.disabled = false;
+ radioGroup.removeEventListener('change', radioChangeListener);
+ };
+ radioGroup.addEventListener('change', radioChangeListener);
+ }
+ }
+
+
+ nextBtn.addEventListener('click', () => {
+ switch (nextButtonType) {
+ case 1: // “下一题”按钮
+ currentQuestion += 1;
+ logHelper.log(currentQuestion);
+ show(questions[currentQuestion].html);
+ hide(questions[currentQuestion - 1].html);
+ if (currentQuestion === questionnaireTotal - 1) {
+ setUpNextButton(nextBtn, 3);
+ }
+ checkQuestionChecked(currentQuestion + 1);
+ break;
+
+ case 2: // “开始”按钮
+ currentQuestion = 0;
+ setUpNextButton(nextBtn, 1);
+ hide(introPart);
+ show(testArea);
+ show(previousBtn);
+ show(questions[currentQuestion].html);
+ checkQuestionChecked(currentQuestion + 1); // 检查是否有一个选项被选中
+ showKeyboardNotice(); // 键盘操作提示
+ backBtn.disabled = true;
+
+ document.title = `答题中 - ${jsonName} - 问心`;
+ break;
+
+ case 3: // “提交”按钮
+ currentQuestion = 0;
+ nextButtonType = 2;
+ hide(testArea);
+ hide(controlArea);
+ nextBtn.disabled = true;
+
+ let questionsResult: QuestionResult[] = [];
+ // 获取页面上所有的题目
+ const groupRadio: NodeListOf = document.querySelectorAll('mdui-radio-group')!;
+ // 遍历每个题目
+ groupRadio.forEach((group: RadioGroup) => {
+ // 将每个题目的组id和用户分数存入数组
+ questionsResult.push({ name: group.name, value: group.value });
+ });
+ // 对题目按照groupId进行分组
+ const groupedQuestions: GroupedData = questionsResult.reduce((acc: GroupedData, current: QuestionResult) => {
+ const name: string = current.name;
+ if (!acc[name]) {
+ acc[name] = [];
+ }
+ acc[name].push(current.value); // 将用户分数存入数组
+ return acc;
+ }, {});
+
+
+ // 获取评分组
+ const scoring: Scoring = json.scoring;
+ // 获取评分标准
+ const criteria: Criterion[] = scoring.criteria;
+ // 获取分值范围
+ const ranges: Ranges[] = scoring.ranges;
+
+ // 获取每个评分标准,并按照groupId对其分组
+ const groupedCriteria: GroupedData = criteria.reduce((acc: GroupedData, current: Criterion) => {
+ const name: number = current.groupId;
+ if (!acc[name]) {
+ acc[name] = [];
+ }
+ acc[name].push(current.method);
+ return acc;
+ }, {});
+
+ const resultTbody: HTMLTableSectionElement = document.querySelector('#resultTbody')!;
+
+ if (questionnaire === 'scl90' || questionnaire === 'scl90-eng') {
+ SCL90Score(groupRadio).forEach((item) => {
+ const itemContainer: HTMLTableRowElement = document.createElement('tr');
+ // 项目名称
+ const itemName: HTMLTableCellElement = document.createElement('td');
+ // 得分
+ const itemScore: HTMLTableCellElement = document.createElement('td');
+ itemName.textContent = item.name;
+ itemScore.colSpan = 2;
+ itemScore.textContent = item.result.toString();
+ itemContainer.appendChild(itemName);
+ itemContainer.appendChild(itemScore);
+ resultTbody.appendChild(itemContainer);
+ });
+ }
+
+ // 将分组后的题目和分组后的评分标准匹配起来,并计算得分
+ const score: ScoreResult[] = getScore(groupedQuestions, groupedCriteria, ranges);
+ // 将得分上屏
+ score.forEach((item: ScoreResult) => {
+ const itemContainer: HTMLTableRowElement = document.createElement('tr');
+ // 项目名称
+ const itemName: HTMLTableCellElement = document.createElement('td');
+ // 得分
+ const itemScore: HTMLTableCellElement = document.createElement('td');
+ // 评价
+ const itemComment: HTMLTableCellElement = document.createElement('td');
+ itemName.textContent = item.name;
+ itemScore.textContent = item.result.toString();
+ itemComment.textContent = item.range;
+ itemComment.classList.add(item.color); // 设置文字颜色
+
+ itemContainer.appendChild(itemName);
+ itemContainer.appendChild(itemScore);
+ itemContainer.appendChild(itemComment);
+ resultTbody.appendChild(itemContainer);
+ });
+
+ if (json.resultTips !== undefined) {
+ document.querySelector('.result-tips')!.innerHTML = `结果解读说明(仅供参考,不作为诊断依据)
${json.resultTips}`;
+ }
+ // 展示结果区域
+ show(resultArea);
+ document.title = `测试结果 - ${jsonName} - 问心`;
+ backBtn.disabled = false;
+ break;
+
+ default:
+ currentQuestion = 0;
+ break;
+ }
+
+ previousBtn.disabled = currentQuestion === 0;
+ updateProgress();
+ });
+
+ previousBtn.addEventListener('click', () => {
+ if (currentQuestion > 0) {
+ currentQuestion -= 1;
+ show(questions[currentQuestion].html);
+ hide(questions[currentQuestion + 1].html);
+ logHelper.log(currentQuestion);
+ setUpNextButton(nextBtn, 1);
+ previousBtn.disabled = currentQuestion === 0;
+ updateProgress();
+ checkQuestionChecked(currentQuestion + 1);
+ }
+ });
+
+ // 监听键盘左右键切换题目
+ window.addEventListener('keydown', (event: KeyboardEvent) => {
+ switch (event.key) {
+ case 'ArrowLeft':
+ if (currentQuestion > 0) {
+ previousBtn.click();
+ }
+ break;
+
+ case 'ArrowRight':
+ if (currentQuestion < questionnaireTotal && nextButtonType === 1) {
+ nextBtn.click();
+ }
+ break;
+
+ default:
+ break;
+ }
+ });
+
+ })
+ .catch((error: any) => {
+ if (questionnaire === null) {
+ hide(container);
+ show(nullTip);
+ return;
+ } else {
+ hide(container);
+ nullTip.innerHTML = `错误:${error}
`;
+ show(nullTip);
+ return;
+ }
+ });
+});
+
+/**
+ * 更新“下一题”按钮的内容
+ * @param nextBtn “下一题”按钮
+ * @param type 1: “开始”按钮 | 2: “下一题”按钮 | 3: “提交”按钮
+ */
+function setUpNextButton(nextBtn: Button, type: number) {
+ for (let i = 0; i < buttonType.length; i++) {
+ if (buttonType[i].name === buttonType[type].name) {
+ nextBtn.innerHTML = `${buttonType[i].name}`;
+ break;
+ }
+ }
+ nextButtonType = type;
+}
\ No newline at end of file
diff --git a/src/test/option.ts b/src/test/option.ts
new file mode 100644
index 0000000..240b535
--- /dev/null
+++ b/src/test/option.ts
@@ -0,0 +1,35 @@
+import type { Button } from 'mdui/components/button.js';
+import type { Radio } from 'mdui/components/radio.js';
+
+export class Option {
+ html: Radio;
+ score: number;
+ /**
+ * 选项
+ * @param name 选项名称
+ * @param score 选项分数
+ */
+ constructor(name: string, score: number) {
+ /**
+ * 构建单选按钮元素
+ */
+ const radio: Radio = document.createElement('mdui-radio');
+ const nextBtn: Button = document.querySelector('.next-btn')!;
+
+ radio.textContent = name;
+ /**
+ * 将分数设置为value
+ */
+ radio.value = score.toString();
+ radio.addEventListener('click', () => {
+ setTimeout(() => {
+ nextBtn.click();
+ }, 280)
+ })
+ /**
+ * 返回单选按钮
+ */
+ this.html = radio;
+ this.score = score;
+ }
+}
\ No newline at end of file
diff --git a/src/test/question.ts b/src/test/question.ts
new file mode 100644
index 0000000..6f47b50
--- /dev/null
+++ b/src/test/question.ts
@@ -0,0 +1,40 @@
+import type { RadioGroup } from 'mdui/components/radio-group.js';
+import { OptionItem } from "../interfaces";
+import { Option } from './option';
+
+export class Question {
+ groupId: number;
+ html: HTMLDivElement;
+ /**
+ * 题目
+ * @param id 题目id
+ * @param groupId 题目组id
+ * @param options 题目选项
+ * @param content 题目内容
+ */
+ constructor(id: number, groupId: number, options: OptionItem[], content: string) {
+ const div: HTMLDivElement = document.createElement('div');
+ const radioGroup: RadioGroup = document.createElement('mdui-radio-group');
+
+ radioGroup.name = groupId.toString();
+ radioGroup.id = `questions-${id}`;
+
+ const questionContent: HTMLParagraphElement = document.createElement('p');
+ // questionContent.textContent = `${id}. ${content}`;
+ questionContent.textContent = content;
+
+ options.forEach((option: OptionItem) => {
+ const radio: Option = new Option(option.name, option.score);
+ radioGroup.appendChild(radio.html);
+ });
+ // 测试用 生成随机数(0-3)
+ // const randomNumber = Math.floor(Math.random() * 5);
+ // radioGroup.value = randomNumber.toString();
+
+ div.appendChild(questionContent);
+ div.appendChild(radioGroup);
+
+ this.html = div;
+ this.groupId = groupId;
+ }
+}
\ No newline at end of file
diff --git a/src/test/scoring.ts b/src/test/scoring.ts
new file mode 100644
index 0000000..45e9de6
--- /dev/null
+++ b/src/test/scoring.ts
@@ -0,0 +1,103 @@
+import { RadioGroup } from "mdui";
+import { BasicScoreResult, GroupedData, Range, Ranges, ScoreResult } from "../interfaces";
+import { LogHelper } from "../utils/LogHelper";
+
+const logHelper = LogHelper.getInstance();
+/**
+ * 计算最终得分
+ * @param groupedQuestions 分好组的题目组id+每题的分数
+ * @param groupedCriteria 分好组的评分组id+评分方法
+ * @param ranges 原始的范围对象
+ * @returns 最终得分的:项目名称+得分+范围名称+显示文字颜色
+ */
+export function getScore(groupedQuestions: GroupedData, groupedCriteria: GroupedData, ranges: Ranges[]): ScoreResult[] {
+ let score: ScoreResult[] = [];
+
+ Object.entries(groupedQuestions).forEach(([key, values]: [string, string[]]) => {
+ const method: string[] = groupedCriteria[key][0].split(", ", 2); // 对于有参数的计算方法按照`, `进行拆分
+ const operator: string = method[0]; // 使用计算方法字段进行判断
+ logHelper.log(`计算方法:${operator}`);
+
+ // 匹配与groupId对应的分值范围
+ const group: Ranges | undefined = ranges.find((group) => group.groupId === Number.parseInt(key, 10));
+ logHelper.log(group);
+
+ // group 被定义
+ if (group) {
+ let result: number = 0;
+ // 判断计算方法
+ switch (operator) {
+ case "plus": // 求和
+ result = getSum(values);
+ logHelper.log(`${key}的总和:${result}`);
+ break;
+ case "average": // 平均数
+ result = parseFloat((getSum(values) / values.length).toFixed(2)); // 确保输出结果为小数点后2位
+ logHelper.log(`${key}的平均值:${result}`);
+ break;
+ case "multiply": // 按照某个值翻倍
+ const ratio = Number.parseFloat(method[1]);
+ result = getSum(values) * ratio;
+ break;
+ default:
+ logHelper.error(`未知计算方法:${operator}`);
+ break;
+ }
+
+ // 匹配分数的分值范围
+ const matchingRange: Range | undefined = group.ranges.find((range: Range) => {
+ return result >= range.min && result <= range.max;
+ });
+
+ // 拼合结果
+ if (matchingRange) {
+ score.push({
+ name: group.name,
+ result: result,
+ range: matchingRange.name,
+ color: matchingRange.color
+ });
+ }
+ }
+ });
+ return score;
+}
+
+/**
+ * 计算总分通用方法
+ * @param values 得分字符串
+ * @returns 总分
+ */
+function getSum(values: string[]): number {
+ return values.reduce((acc: number, current: string) => {
+ if (current !== "") {
+ return acc + Number.parseInt(current, 10);
+ } else {
+ return acc;
+ }
+ }, 0);
+}
+
+/**
+ * SCL-90 量表特别适配
+ * @param groupRadio 所有题目
+ * @returns
+ */
+export function SCL90Score(groupRadio: NodeListOf): BasicScoreResult[] {
+ // 计算总分&总均分
+ let scl90Score: BasicScoreResult[] = [];
+ let sum: number = 0;
+ let positiveCount: number = 0;
+ groupRadio.forEach((group: RadioGroup) => {
+ const value = Number.parseInt(group.value, 10);
+ sum += value;
+ if (value > 0) positiveCount++;
+ });
+ const average: number = sum / groupRadio.length;
+ const positivePainLevel: number = sum / positiveCount;
+ scl90Score.push({ name: '总分', result: sum });
+ scl90Score.push({ name: '总均分', result: Number.parseInt(average.toFixed(2), 10) });
+ scl90Score.push({ name: '阳性项目数', result: positiveCount });
+ scl90Score.push({ name: '阳性症状痛苦水平', result: Number.parseInt(positivePainLevel.toFixed(2), 10) });
+ return scl90Score;
+}
\ No newline at end of file
diff --git a/src/utils/LogHelper.ts b/src/utils/LogHelper.ts
new file mode 100644
index 0000000..b0a1a89
--- /dev/null
+++ b/src/utils/LogHelper.ts
@@ -0,0 +1,26 @@
+export class LogHelper {
+ private static instance: LogHelper;
+
+ public static getInstance(): LogHelper {
+ if (!LogHelper.instance) {
+ LogHelper.instance = new LogHelper();
+ }
+ return LogHelper.instance;
+ }
+
+ log(message: any) {
+ if (import.meta.env.DEV) console.log(message);
+ }
+
+ info(message: any) {
+ if (import.meta.env.DEV) console.info(message);
+ }
+
+ warn(message: any) {
+ console.warn(message);
+ }
+
+ error(message: any) {
+ console.error(message);
+ }
+}
\ No newline at end of file
diff --git a/src/utils/element.ts b/src/utils/element.ts
new file mode 100644
index 0000000..9c0c242
--- /dev/null
+++ b/src/utils/element.ts
@@ -0,0 +1,19 @@
+/**
+ * 用于控制元素的工具类
+ */
+
+/**
+ * 显示一个元素
+ * @param element 要显示的元素
+ */
+export function show(element: HTMLElement) {
+ element.style.display = 'block';
+}
+
+/**
+ * 隐藏一个元素
+ * @param element 要隐藏的元素
+ */
+export function hide(element: HTMLElement) {
+ element.style.display = 'none';
+}
\ No newline at end of file
diff --git a/src/utils/localstorage.ts b/src/utils/localstorage.ts
new file mode 100644
index 0000000..53c43a2
--- /dev/null
+++ b/src/utils/localstorage.ts
@@ -0,0 +1,17 @@
+const myStorage: Storage = localStorage;
+
+export function setStorageItem(key: string, value: any) {
+ myStorage.setItem(key, value.toString());
+}
+
+export function getStorageItem(key: string): string | null {
+ return myStorage.getItem(key);
+}
+
+export function removeStorageItem(key: string) {
+ myStorage.removeItem(key);
+}
+
+export function clearStorage() {
+ myStorage.clear();
+}
\ No newline at end of file
diff --git a/src/utils/network.ts b/src/utils/network.ts
new file mode 100644
index 0000000..2007bfe
--- /dev/null
+++ b/src/utils/network.ts
@@ -0,0 +1,5 @@
+export function getFile(url: string): Promise {
+ return fetch(url)
+ .then(response => response.text())
+ .catch(error => Promise.reject(error));
+}
\ No newline at end of file
diff --git a/src/utils/notices.ts b/src/utils/notices.ts
new file mode 100644
index 0000000..baf6d09
--- /dev/null
+++ b/src/utils/notices.ts
@@ -0,0 +1,82 @@
+import { confirm } from 'mdui/functions/confirm.js';
+import { dialog } from 'mdui/functions/dialog.js';
+import { snackbar } from 'mdui/functions/snackbar.js';
+import { clearStorage, getStorageItem, setStorageItem } from './localstorage';
+
+/**
+ * 展示键盘操作提示
+ */
+export function showKeyboardNotice() {
+ if (getStorageItem("keyboardNotice") !== "true") {
+ snackbar({
+ message: "你也可以使用键盘的 “←” “→” 键来进行题目的切换",
+ // action: "知道了",
+ // onActionClick: () => true,
+ onClosed: () => setStorageItem("keyboardNotice", true),
+ });
+ }
+}
+
+/**
+ * 弹出清除全部数据确认对话框
+ */
+export function showClearAllDataDialog() {
+ confirm({
+ headline: "清除全部数据",
+ description: "是否清除本网站全部相关数据?",
+ closeOnEsc: false,
+ closeOnOverlayClick: false,
+ confirmText: "确定",
+ cancelText: "取消",
+ onConfirm: () => {
+ clearStorage();
+ window.location.href = '/';
+ return true;
+ },
+ onCancel: () => true
+ });
+}
+
+/**
+ * 展示使用提示对话框
+ */
+export function showDisclaimerDialog() {
+ if (getStorageItem('disclaimer') !== 'true') {
+ const body: HTMLDivElement = document.createElement('div');
+
+ const p1: HTMLParagraphElement = document.createElement('p');
+ const s1: HTMLElement = document.createElement('strong');
+ s1.textContent = "本网站为公益网站,仅收集互联网上的信息并整理,不对网站内任何量表具有版权。如侵权请联系我,我会及时删除。";
+ p1.appendChild(s1);
+
+ const p2: HTMLParagraphElement = document.createElement('p');
+ const s2: HTMLElement = document.createElement('strong');
+ s2.textContent = "本网站会尽最大限度保护您的答题数据和结果的安全。请不要随意将结果分享给他人。";
+ p2.appendChild(s2);
+
+ const p3: HTMLParagraphElement = document.createElement('p');
+ const s3: HTMLElement = document.createElement('strong');
+ s3.textContent = "所有量表的测量结果仅供参考,不作为任何医学诊断的依据。";
+ p3.appendChild(s3);
+
+ body.appendChild(p1);
+ body.appendChild(p2);
+ body.appendChild(p3);
+ dialog({
+ headline: "使用提示",
+ description: "请务必仔细阅读下方内容 Please read the following content carefully",
+ body: body,
+ closeOnEsc: false,
+ closeOnOverlayClick: false,
+ actions: [
+ {
+ text: "知道了",
+ onClick: () => {
+ setStorageItem('disclaimer', true);
+ return true;
+ },
+ }
+ ]
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/test.html b/test.html
new file mode 100644
index 0000000..4f4ea73
--- /dev/null
+++ b/test.html
@@ -0,0 +1,52 @@
+还没有选择测试题哟,前往题库页面选择一个吧
+
+
+
+
+
+
+
+
+
+ 上一题
+
+
+ 下一题
+
+
+
+
+
+
测试结果
+
+
+
+
+ 项目名称 |
+ 得分 |
+ 评价 |
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e13d7f8
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "module": "ESNext",
+ "lib": [
+ "ES2020",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": true,
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/typedoc.json b/typedoc.json
new file mode 100644
index 0000000..5dba97d
--- /dev/null
+++ b/typedoc.json
@@ -0,0 +1,4 @@
+{
+ "plugin": ["typedoc-plugin-markdown"],
+ "out": "./docs"
+}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..5ae46e5
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,71 @@
+import { exec } from 'child_process';
+import path from 'path';
+import { promisify } from 'util';
+import { defineConfig } from 'vite';
+import { createHtmlPlugin } from 'vite-plugin-html';
+import packageJson from './package.json';
+
+const execPromise = promisify(exec);
+
+async function getVersionInfo() {
+ try {
+ const { stdout: versionCode } = await execPromise("git rev-list --count HEAD");
+ const { stdout: commitHash } = await execPromise("git rev-parse --short HEAD");
+ return {
+ versionCode: versionCode.trim(),
+ commitHash: commitHash.trim(),
+ };
+ } catch (error) {
+ console.error(`执行命令时发生错误:${error}`);
+ throw error;
+ }
+}
+
+export default defineConfig(async ({ command, mode, isSsrBuild, isPreview }) => {
+ const { versionCode, commitHash } = await getVersionInfo();
+ const baseConfig = {
+ build: {
+ rollupOptions: {
+ input: {
+ index: path.resolve(__dirname, 'index.html'),
+ list: path.resolve(__dirname, 'list.html'),
+ test: path.resolve(__dirname, 'test.html'),
+ settings: path.resolve(__dirname, 'settings.html'),
+ },
+ },
+ },
+ plugins: [
+ createHtmlPlugin({
+ minify: true,
+ }),
+ ],
+ };
+ if (command === 'serve') {
+ return {
+ ...baseConfig,
+ server: {
+ open: true,
+ hmr: {
+ protocol: "ws",
+ }
+ },
+ define: {
+ VERSION_NAME: JSON.stringify(packageJson.version),
+ VARIANT: JSON.stringify("dev"),
+ COMMIT_HASH: JSON.stringify(commitHash),
+ VERSION_CODE: JSON.stringify(versionCode),
+ },
+ }
+ } else {
+ return {
+ ...baseConfig,
+ base: '/Ask-Yourself/',
+ define: {
+ VERSION_NAME: JSON.stringify(packageJson.version),
+ VARIANT: JSON.stringify("web"),
+ COMMIT_HASH: JSON.stringify(commitHash),
+ VERSION_CODE: JSON.stringify(versionCode),
+ },
+ }
+ }
+});
\ No newline at end of file