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