Compare commits

..

20 Commits

Author SHA1 Message Date
p95fco63j c0969db113 Merge pull request '最终版' (#176) from develop into main
1 day ago
pc8xi2fbj dc0829a457 Merge pull request '最终版' (#175) from zhanghongwei_branch into develop
1 day ago
tianyuan b472e87ef2 用例文档最终稿
3 days ago
tianyuan 2143cd1385 用例文档最终稿
3 days ago
pc8xi2fbj 60d9c085d8 Merge pull request '1.0' (#174) from zhanghongwei_branch into develop
6 days ago
tianyuan d8b2edce9e 需求规格说明书
6 days ago
tianyuan 385a7f61f2 修改用例命名符合主谓宾规范
6 days ago
tianyuan 5c85ec0fd9 测试报告与需求文档
1 week ago
hnu202326010106 19d62700ae Merge pull request '王磊-第十五周周总结' (#173) from wanglei_branch into develop
1 week ago
luoyuehang 3f9eea9469 罗月航15周周总结
1 week ago
wanglei 75d5430bb8 王磊-第十五周周总结
1 week ago
周竞由 128d05846a 周竞由的15周周总结
1 week ago
luoyuehang e727d9df66 罗月航15周周总结
1 week ago
luoyuehang 087f84a653 Merge remote-tracking branch 'origin/develop' into luoyuehang_branch
1 week ago
luoyuehang ee31451b51 罗月航15周周总结
1 week ago
hnu202326010125 67f101b0cb Merge pull request '稳定版本1' (#162) from develop into main
2 weeks ago
p95fco63j d3605bef69 Merge pull request '稳定运行版本1.0' (#161) from develop into main
2 weeks ago
tianyuan 085b7e1170 Merge branch 'main' of https://bdgit.educoder.net/p95fco63j/Water_Machine_Management_System
4 weeks ago
tianyuan 53a2bd9017 重置所有代码到Initial commit(51f4cd5)版本,撤销后续所有提交(含合并提交)
4 weeks ago
hnu202326010125 e9fe15385e Merge pull request 'app1第一次迭代功能实现' (#89) from luoyuehang_branch into main
4 weeks ago

3
.idea/.gitignore vendored

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="water-management-system" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="water-management-system" options="-parameters" />
</option>
</component>
</project>

@ -0,0 +1,439 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DDLFileAttachmentManager">
<mappings />
<preferences />
</component>
<component name="DBNavigator.Project.DatabaseAssistantManager">
<assistants />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
<enable-sticky-paths value="true" />
<enable-quick-filters value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="JSON_VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="JAVA_CLASS" enabled="true" />
<object-type name="JAVA_FIELD" enabled="true" />
<object-type name="JAVA_METHOD" enabled="true" />
<object-type name="JAVA_RESOURCE" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
<object-type name="CREDENTIAL" enabled="true" />
<object-type name="AI_PROFILE" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
<object-type name="TYPE ATTRIBUTE" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="JSON VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="JAVA CLASS" enabled="true" />
<object-type name="INNER CLASS" enabled="true" />
<object-type name="JAVA FIELD" enabled="true" />
<object-type name="JAVA METHOD" enabled="true" />
<object-type name="JAVA PARAMETER" enabled="true" />
<object-type name="JAVA RESOURCE" enabled="true" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="false" />
<object-type name="CREDENTIAL" enabled="false" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<audit-columns>
<column-names value="" />
<visible value="true" />
<editable value="false" />
</audit-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="Properties" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="Java" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="Groovy" enabled="true" />
<content-type name="YAML" enabled="true" />
<content-type name="Manifest" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
<exit-on-changes value="ASK" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="json view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="json view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="json view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
<mapping file-type-id="JAVA_SOURCE" extensions="sql" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<assistant-settings>
<credential-settings>
<credentials />
</credential-settings>
</assistant-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src" charset="UTF-8" />
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoogleJavaFormatSettings">
<option name="enabled" value="false" />
</component>
</project>

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_23" default="true" project-jdk-name="23" project-jdk-type="JavaSDK" />
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

@ -0,0 +1,23 @@
# 个人周计划-第15周
## 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-01-03
## 本周任务计划安排
| 序号 | 计划内容 | 协作人 | 情况说明 |
|----|----------|-------|-------------------------------------------------------------------------------|
| 1 | 模块功能开发 | 组员 | 基于第14周确定的分工完成个人负责模块的核心功能开发含接口实现、数据校验逻辑编写及单元测试覆盖率不低于80%),每日下班前同步开发进度至团队群 |
| 2 | 联调问题迭代修复 | 前端/组员 | 跟进第14周未解决的联调问题完成剩余2个非核心接口的对接实时响应新联调过程中出现的功能异常、数据返回错误等问题确保核心业务流程联调通过率100% |
| 3 | 中期进度复盘 | 团队全员 | 2026-01-02组织团队中期复盘会议同步各模块开发进度、联调情况及当前存在的阻塞问题协商解决方案调整后续任务优先级输出《第15周中期复盘报告》 |
| 4 | 接口文档完善 | 组员 | 基于开发和联调实际情况,补充接口文档的参数示例、异常响应说明及调用注意事项,确保文档与实际接口一致,便于前端查阅和后续维护 |
## 小结
1. **技术重点:** 聚焦模块核心功能实现与单元测试编写,熟练运用数据校验、异常处理技巧;针对性解决联调中的复杂问题,提升接口兼容性设计能力
2. **协作重点:** 保持与前端的实时沟通,及时同步开发进度与接口变更;通过中期复盘会议,高效协调团队资源,解决项目阻塞问题
3. **学习重点:** 学习单元测试框架如JUnit的高级用法提升测试用例设计合理性研究接口文档标准化编写规范提高文档可读性与实用性

@ -0,0 +1,25 @@
# 个人周总结-第14周
## 周竞由 - 个人周总结
### 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-22
**结束时间:** 2025-12-28
### 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
|----|------|------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| 1 | 确定分工 | 完成 | 2025-12-22按时组织团队线下会议结合第14周迭代开发需求基于组员技能特长与过往任务完成情况细分8个功能模块的开发职责明确各模块交付物含接口文档、测试用例及截止时间输出《第14周团队分工明细表》组织组员逐一确认职责无分工异议 |
| 2 | 联调支持 | 完成 | 全程配合前端开展接口联调工作建立实时沟通群响应问题累计处理字段类型不匹配、跨域配置异常、分页参数错误等联调问题12个平均问题解决时长1.5小时协助前端完成6个核心功能模块的接口对接含用户操作日志同步、数据统计报表生成等关键场景联调通过率达98%剩余2个非核心接口问题已记录并计划下周优先处理 |
### 对团队工作的建议
1. **建议优化接口文档维护机制**本周联调中发现部分接口文档字段说明不清晰、参数示例缺失导致沟通成本增加建议建立接口文档“谁开发谁维护”的责任制要求更新接口后2小时内同步更新文档每周安排专人抽检文档完整性确保前后端信息一致
2. **建议建立联调问题实时同步看板**建议使用团队协作工具如飞书看板、Trello搭建联调问题跟踪看板按“待处理/处理中/已解决/复盘”分类管理问题,标注问题责任人、预计解决时间,方便团队实时掌握联调进度,避免重复沟通
### 小结
1. **技术收获**通过处理多样化的联调问题深化了对HTTP请求参数校验、跨域配置、接口兼容性处理的理解在分工规划中提升了基于项目需求拆解任务、匹配团队资源的能力学会结合组员优势合理分配职责以提升整体开发效率
2. **协作收获**:通过建立实时沟通机制,缩短了跨角色问题响应周期,提升了与前端团队的协作默契;在分工会议中,通过充分听取组员意见,锻炼了协调不同诉求、达成共识的沟通能力
3. **后续重点**跟进剩余2个接口的联调收尾工作确保功能全量对接系统学习接口性能分析工具如JMeter与调优方法重点攻克SQL查询优化、数据缓存设计等知识点协助团队搭建联调问题解决方案库与接口文档抽检机制
4. **希望得到的帮助**:希望能获取团队过往项目的接口性能调优案例集,或邀请技术导师针对“高并发场景下接口优化技巧”进行专项指导,帮助快速掌握实操方法,提升系统性能优化能力

@ -0,0 +1,36 @@
# 个人周总结-第15周
## 姓名和起止时间
**姓  名:** 罗月航
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
## 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- | -------- | -------- | -------- |
| 1 | 与老师项目讨论会议 | 完成 | 成功召开项目讨论会,老师对项目成果给予肯定,并提出宝贵的改进建议 |
| 2 | 项目不足分析整理 | 完成 | 系统整理了老师提出的改进建议 |
| 3 | 业务流程梳理优化 | 完成 | 重新梳理了扫码用水和工单处理的核心业务流程 |
| 5 | 代码质量二次审查 | 完成 | 对修改后的代码进行了全面审查,确保代码质量和规范符合要求 |
| 6 | 性能优化持续跟进 | 完成 | 针对老师提出的性能问题进行了进一步优化关键页面加载速度提升25% |
| 7 | 学生端APK打包 | 完成 | 成功将学生端项目打包为APK安装包经过测试可在Android设备上正常运行 |
| 8 | 维修人员APP APK打包 | 完成 | 成功将维修人员APP打包为APK安装包功能完整运行稳定 |
## 对团队工作的建议
1. **安装包管理**建议建立APK版本管理机制方便跟踪不同版本的更新内容
2. **用户分发渠道**建议规划APK的分发渠道和安装指南便于用户获取和安装
3. **更新策略**:建议制定后续版本更新策略和维护计划;
## 小结
1. **专业指导收获**:通过与老师的深入交流,获得了宝贵的专业指导,项目质量得到进一步提升;
2. **改进及时有效**:对老师提出的建议进行了快速响应和改进,问题解决及时有效;
3. **技术成果显著**成功将两个应用打包为APK
4. **打包流程掌握**掌握了Vue项目打包为移动应用的技术流程和配置要点
5. **测试验证充分**对打包后的APK进行了全面测试确保功能完整、运行稳定
6. **项目里程碑**APK打包完成是项目的重要里程碑为后续的演示和交付奠定了基础

@ -0,0 +1,39 @@
# 个人周计划-第15周
## 姓名和起止时间
**姓  名:** 王磊
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2025-1-4
## 本周任务计划安排
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| --- | --------- | ---- | ------------------------ |
| 1 | 流程闭环所有子流程 | 完成 | 告警→派单→接单→维修→审核→结单,流程形成闭环 |
| 2 | 管理员人工派单 | 完成 | 管理员人工派单是强制的、不受限制的 |
| 3 | 维修工接单 | 完成 | 维修人员能同时接许多工单 |
| 4 | 片区管理 | 完成 | 给片区的分级关联上 |
## 对团队工作的建议
1.****建立项目复盘与知识沉淀机制**** 建议在项目关键节点或里程碑结束后,组织团队进行简短复盘,总结技术实现、协作流程中的亮点与可优化点。
2.****推行轻量级代码评审流程**** 为提升代码质量与团队技术一致性,建议在合并重要功能前,开展轻量级代码评审。可结对或小组内轮换评审,重点关注逻辑清晰度、代码规范与潜在风险。
## 小结
1. **技术收获** 通过完整参与项目流程闭环的实现,深入理解了系统模块间的数据流转与状态控制,并在权限设计、接口优化等方面积累了实战经验。
2. **协作收获** 在与产品、前端、测试等多角色协作中,逐步形成了“需求对齐-接口约定-同步验证”的高效协作模式,增强了跨职能沟通与任务协同能力。
3. **后续重点** 对项目的细节方面进行优化完善。
---
## 【注】
1. 在小结一栏中写出希望得到如何的帮助,如讲座等;
2. 请将个人计划和总结提前发给负责人;
3. 周任务总结与计划是项目小组评分考核的重要依据,将直接记入平时成绩,请各位同学按要求认真填写并按时提交;
4. 所有组员都需提交个人周计划、周总结文档,按时上传至代码托管平台;

@ -0,0 +1,27 @@
# 个人周总结-第15周
## 周竞由 - 个人周总结
### 姓名和起止时间
**姓  名:** 周竞由
**团队名称:** 1班-汪汪队
**开始时间:** 2025-12-29
**结束时间:** 2026-01-04
### 本周任务完成情况
| 序号 | 总结内容 | 是否完成 | 情况说明 |
| ---- |--------------------|----------|--------------------------------------------------------------------------|
| 1 | 模块功能开发 | 完成 | 按分工完成个人负责的“数据统计分析模块”核心功能开发实现3个核心接口数据查询、统计报表生成、导出编写数据校验逻辑20+条单元测试覆盖率达85%通过本地自测及组员交叉测试未出现功能性bug已提交代码至Git仓库并标注版本号 |
| 2 | 联调问题迭代修复 | 完成 | 已解决第14周遗留的2个非核心接口对接问题字段格式兼容、分页参数逻辑调整本周新联调过程中响应前端问题8个含报表数据渲染异常、导出文件格式错误等平均解决时长1小时核心业务流程联调通过率100%非核心功能联调完成率95% |
| 3 | 中期进度复盘 | 完成 | 2026-01-02如期组织团队中期复盘会议各模块负责人同步进度8个模块中6个已完成核心开发2个模块滞后1天梳理出“第三方接口依赖阻塞”“部分测试用例缺失”等3个关键问题协商确定解决方案协调后端协助对接第三方接口、共享测试用例模板输出《第15周中期复盘报告》并同步全员 |
| 4 | 接口文档完善 | 完成 | 基于开发和联调实际情况补充3个核心接口的参数示例含正常/异常场景、异常响应码说明新增5类常见异常及调用注意事项如请求频率限制、权限要求优化文档排版结构通过团队文档抽检确保与实际接口100%一致 |
### 对团队工作的建议
1. **建议建立测试用例共享库**:本周交叉测试中发现部分组员测试用例设计不全面,建议搭建团队共享测试用例库,按模块分类存储,要求每人提交功能开发时同步上传测试用例,便于交叉测试和后续回归测试,提升测试效率
2. **建议优化第三方接口对接协作**针对本周出现的第三方接口依赖阻塞问题建议指定1名技术负责人统筹对接第三方同步接口文档、调试进度及问题反馈避免多成员重复沟通缩短对接周期
### 小结
1. **技术收获**熟练运用MyBatis-Plus实现复杂数据查询与统计逻辑提升了单元测试用例设计的全面性在联调问题修复中深化了对接口兼容性、异常场景处理的理解掌握了报表导出功能的常见问题解决方案
2. **协作收获**:通过组织中期复盘会议,提升了问题梳理、方案协商的组织协调能力;在交叉测试和联调过程中,加强了与组员、前端的高效沟通,学会通过共享文档、实时同步等方式减少信息差
3. **后续重点**:针对模块功能进行优化迭代(如查询性能优化、界面交互细节调整);配合团队完成系统集成测试,及时修复测试反馈的问题;协助搭建团队测试用例共享库
4. **希望得到的帮助**:希望能获取数据统计模块的性能优化案例(如大数据量下查询提速技巧),或邀请技术导师指导第三方接口对接的容错处理方法,提升系统稳定性设计能力

@ -0,0 +1,36 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/

@ -0,0 +1,6 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer"
]
}

@ -0,0 +1,44 @@
# app2
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Run Unit Tests with [Vitest](https://vitest.dev/)
```sh
npm run test:unit
```

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,28 @@
{
"name": "app2",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest"
},
"dependencies": {
"pinia": "^3.0.4",
"vue": "^3.5.25",
"vue-router": "^4.6.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.2",
"@vue/test-utils": "^2.4.6",
"jsdom": "^27.2.0",
"vite": "^7.2.4",
"vite-plugin-vue-devtools": "^8.0.5",
"vitest": "^4.0.14"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,70 @@
<script setup>
import { RouterView } from 'vue-router'
import { onMounted } from 'vue'
onMounted(() => {
//
document.addEventListener('touchstart', function(event) {
if (event.touches.length > 1) {
event.preventDefault()
}
}, { passive: false })
//
let lastTouchEnd = 0
document.addEventListener('touchend', function(event) {
const now = Date.now()
if (now - lastTouchEnd <= 300) {
event.preventDefault()
}
lastTouchEnd = now
}, false)
})
</script>
<template>
<div class="app-container">
<div class="mobile-frame">
<RouterView />
</div>
</div>
</template>
<style scoped>
.app-container {
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
background: #f0f2f5;
padding: 20px;
}
.mobile-frame {
width: 375px;
height: 667px;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
overflow: hidden;
position: relative;
border: 1px solid #e8e8e8;
}
/* 移动端适配 */
@media (max-width: 420px) {
.app-container {
padding: 0;
background: #f0f2f5;
}
.mobile-frame {
width: 100%;
height: 100vh;
border-radius: 0;
box-shadow: none;
border: none;
}
}
</style>

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

@ -0,0 +1,138 @@
/* 更新现有的 main.css */
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
'Inter',
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* 移动端优化 */
.no-scroll {
overflow: hidden;
position: fixed;
width: 100%;
height: 100%;
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 移动端点击效果优化 */
@media (hover: none) and (pointer: coarse) {
* {
cursor: pointer;
}
input,
textarea,
select,
button {
font-size: 16px !important; /* 防止iOS缩放 */
}
a,
button {
-webkit-tap-highlight-color: transparent;
}
}
/* 修复移动端overflow-scrolling问题 */
.element

@ -0,0 +1,37 @@
/* 移动端全局样式 */
:root {
--mobile-width: 375px;
--mobile-height: 667px;
--primary-color: #1156b1;
--secondary-color: #81d3f8;
--success-color: #04d919;
--warning-color: #ff9800;
--danger-color: #f44336;
}
/* 移动端基础样式 */
#app {
width: 100%;
min-height: 100vh;
background: #f5f5f5;
overflow-x: hidden;
}
/* 移动端容器 */
.mobile-container {
max-width: var(--mobile-width);
min-height: var(--mobile-height);
margin: 0 auto;
background: white;
position: relative;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
/* 安全区域适配 */
.safe-area {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}

@ -0,0 +1,44 @@
<script setup>
defineProps({
msg: {
type: String,
required: true,
},
})
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

@ -0,0 +1,95 @@
<script setup>
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
>Vue - Official</a
>. If you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

@ -0,0 +1,86 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

@ -0,0 +1,11 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } })
expect(wrapper.text()).toContain('Hello Vitest')
})
})

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

@ -0,0 +1,17 @@
// src/main.js
import './assets/main.css'
import './assets/mobile.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import './router/permission' // 确保引入了路由守卫
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

@ -0,0 +1,39 @@
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'StudentLoginPage',
component: () => import('../views/StudentLoginPage.vue')
},
{
path: '/home',
name: 'HomePage',
component: () => import('../views/HomePage.vue')
},
{
path: '/water-quality',
name: 'WaterQuality',
component: () => import('../views/WaterQualityPage.vue')
},
{
path: '/scan',
name: 'ScanPage',
component: () => import('../views/ScanPage.vue')
},
{
path: '/profile',
name: 'ProfilePage',
component: () => import('../views/ProfilePage.vue')
},
{
path: '/history',
name: 'HistoryPage',
component: () => import('../views/HistoryPage.vue')
}
]
})
export default router

@ -0,0 +1,30 @@
// src/router/permission.js
import router from './index'
// 不需要登录的白名单路由
const whiteList = ['/', '/login', '/register']
router.beforeEach((to, from, next) => {
// 检查是否有有效的token
const token = localStorage.getItem('token')
if (token) {
// 有token的情况下
if (to.path === '/' || to.path === '/login') {
// 已登录用户访问登录页时重定向到首页
next('/home')
} else {
// 访问其他需要权限的页面,允许通过
next()
}
} else {
// 没有token的情况下
if (whiteList.includes(to.path)) {
// 白名单路由可以直接访问
next()
} else {
// 非白名单路由重定向到登录页
next('/')
}
}
})

@ -0,0 +1,45 @@
// src/services/api.js
import axios from 'axios'
const apiClient = axios.create({
baseURL: 'http://localhost:8080',
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
apiClient.interceptors.request.use(
(config) => {
// 从本地存储获取token
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
apiClient.interceptors.response.use(
(response) => {
return response
},
(error) => {
if (error.response?.status === 401) {
// token过期或无效清除用户信息并跳转到登录页
localStorage.removeItem('token')
localStorage.removeItem('userId')
localStorage.removeItem('username')
localStorage.removeItem('userType')
localStorage.removeItem('studentId')
window.location.href = '/'
}
return Promise.reject(error)
}
)
export default apiClient

@ -0,0 +1,58 @@
// src/services/authServices.js
import api from './api'
export const authServices = {
// 通用登录接口
async login(loginData) {
try {
const response = await api.post('/api/common/login', loginData)
return response.data
} catch (error) {
throw error.response?.data || error.message
}
},
// 学生登录接口
async studentLogin(studentId, password) {
try {
const loginData = {
username: studentId,
password: password,
userType: 'user' // 注意:后端中用户类型是"user"
}
const response = await api.post('/api/common/login', loginData)
return response.data
} catch (error) {
throw error.response?.data || error.message
}
},
// 修改:学生注册接口 - 使用正确的路径和字段
async studentRegister(registerData) {
try {
// 构建符合RegisterRequest的数据
const requestData = {
username: registerData.name, // 用户名(学生姓名)
password: registerData.password, // 密码
userType: 'user', // 固定为用户类型
studentId: registerData.studentId, // 学号
studentName: registerData.name // 学生姓名
// 注意后端RegisterRequest目前只支持这些字段
// 手机号、邮箱等需要扩展User实体和RegisterRequest
}
console.log('发送注册数据到后端:', requestData)
const response = await api.post('/api/common/register', requestData)
console.log('后端注册响应:', response.data)
return response.data
} catch (error) {
console.error('注册API错误:', error)
// 返回更详细的错误信息
const errorData = error.response?.data || {
code: 500,
message: error.message || '网络错误'
}
throw errorData
}
}
}

@ -0,0 +1,32 @@
// src/services/deviceService.js
import api from './api'
export const deviceService = {
// 获取终端设备信息
async getTerminalInfo(terminalId) {
try {
const response = await api.get(`/api/water/terminal/${terminalId}`)
return response.data
} catch (error) {
// 更好的错误处理
if (error.response?.status === 403) {
console.error('权限不足,请重新登录')
// 可以在这里触发重新登录逻辑
}
throw error.response?.data || error.message
}
},
// 获取水质信息
async getWaterQualityInfo(deviceId) {
try {
const response = await api.get(`/api/water/quality/${deviceId}`)
return response.data
} catch (error) {
if (error.response?.status === 403) {
console.error('权限不足,无法获取水质信息')
}
throw error.response?.data || error.message
}
}
}

@ -0,0 +1,30 @@
// src/stores/auth.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue' // 添加 computed 导入
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref(localStorage.getItem('token'))
const login = (userData, authToken) => {
user.value = userData
token.value = authToken
localStorage.setItem('token', authToken)
}
const logout = () => {
user.value = null
token.value = null
localStorage.removeItem('token')
}
const isAuthenticated = computed(() => !!token.value) // 使用 computed 包装
return {
user,
token,
isAuthenticated,
login,
logout
}
})

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

@ -0,0 +1,67 @@
// src/stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userId = ref(localStorage.getItem('userId') || '')
const username = ref(localStorage.getItem('username') || '')
const userType = ref(localStorage.getItem('userType') || '')
const studentId = ref(localStorage.getItem('studentId') || '')
// 计算属性:是否已登录
const isLoggedIn = computed(() => !!token.value)
// 设置用户信息
const setUser = (userInfo) => {
token.value = userInfo.token
userId.value = userInfo.userId
username.value = userInfo.username
userType.value = userInfo.userType
studentId.value = userInfo.studentId
// 保存到本地存储
localStorage.setItem('token', userInfo.token)
localStorage.setItem('userId', userInfo.userId)
localStorage.setItem('username', userInfo.username)
localStorage.setItem('userType', userInfo.userType)
localStorage.setItem('studentId', userInfo.studentId)
}
// 清除用户信息
const clearUser = () => {
token.value = ''
userId.value = ''
username.value = ''
userType.value = ''
studentId.value = ''
// 清除本地存储
localStorage.removeItem('token')
localStorage.removeItem('userId')
localStorage.removeItem('username')
localStorage.removeItem('userType')
localStorage.removeItem('studentId')
}
// 从本地存储初始化用户信息
const initFromStorage = () => {
token.value = localStorage.getItem('token') || ''
userId.value = localStorage.getItem('userId') || ''
username.value = localStorage.getItem('username') || ''
userType.value = localStorage.getItem('userType') || ''
studentId.value = localStorage.getItem('studentId') || ''
}
return {
token,
userId,
username,
userType,
studentId,
isLoggedIn,
setUser,
clearUser,
initFromStorage
}
})

@ -0,0 +1,501 @@
<template>
<div class="history-page">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-title">历史记录</div>
<button class="back-btn" @click="goBack"></button>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 统计信息 -->
<div class="summary-info">
<div class="total-count">
{{ historyList.length }}条记录
</div>
</div>
<!-- 历史记录列表 -->
<div class="history-list">
<!-- 今日 -->
<div class="date-group" v-if="todayRecords.length > 0">
<div class="group-title">今日</div>
<div class="records-container">
<div
v-for="record in todayRecords"
:key="record.id"
class="record-card"
>
<div class="record-header">
<div class="device-name">{{ record.deviceName }}</div>
<div class="record-time">{{ record.time }}</div>
</div>
<div class="record-details">
<div class="water-amount">{{ record.amount }}</div>
<div class="device-id">ID: {{ record.deviceId }}</div>
</div>
</div>
</div>
</div>
<!-- 07-20 -->
<div class="date-group" v-if="dateGroupedRecords['07-20']?.length > 0">
<div class="group-title">07-20</div>
<div class="records-container">
<div
v-for="record in dateGroupedRecords['07-20']"
:key="record.id"
class="record-card"
>
<div class="record-header">
<div class="device-name">{{ record.deviceName }}</div>
<div class="record-time">{{ record.time }}</div>
</div>
<div class="record-details">
<div class="water-amount">{{ record.amount }}</div>
<div class="device-id">ID: {{ record.deviceId }}</div>
</div>
</div>
</div>
</div>
<!-- 07-19 -->
<div class="date-group" v-if="dateGroupedRecords['07-19']?.length > 0">
<div class="group-title">07-19</div>
<div class="records-container">
<div
v-for="record in dateGroupedRecords['07-19']"
:key="record.id"
class="record-card"
>
<div class="record-header">
<div class="device-name">{{ record.deviceName }}</div>
<div class="record-time">{{ record.time }}</div>
</div>
<div class="record-details">
<div class="water-amount">{{ record.amount }}</div>
<div class="device-id">ID: {{ record.deviceId }}</div>
</div>
</div>
</div>
</div>
<!-- 07-18 -->
<div class="date-group" v-if="dateGroupedRecords['07-18']?.length > 0">
<div class="group-title">07-18</div>
<div class="records-container">
<div
v-for="record in dateGroupedRecords['07-18']"
:key="record.id"
class="record-card"
>
<div class="record-header">
<div class="device-name">{{ record.deviceName }}</div>
<div class="record-time">{{ record.time }}</div>
</div>
<div class="record-details">
<div class="water-amount">{{ record.amount }}</div>
<div class="device-id">ID: {{ record.deviceId }}</div>
</div>
</div>
</div>
</div>
<!-- 没有记录时显示 -->
<div class="empty-state" v-if="historyList.length === 0">
<div class="empty-icon">📊</div>
<div class="empty-text">暂无历史记录</div>
<div class="empty-hint">开始取水后会在这里显示记录</div>
</div>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-button" @click="goToPage('home')">
<div class="nav-icon">🗺</div>
<div class="nav-text">地图</div>
</div>
<div class="nav-button" @click="goToPage('scan')">
<div class="nav-icon">📷</div>
<div class="nav-text">扫码</div>
</div>
<div class="nav-button" @click="goToPage('profile')">
<div class="nav-icon">👤</div>
<div class="nav-text">我的</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
//
const historyList = ref([
{
id: 1,
date: '今日',
deviceName: '湖南大学教学楼1F饮水机',
deviceId: 'A201',
time: '12:08',
amount: '200ml'
},
{
id: 2,
date: '今日',
deviceName: '湖南大学信息楼2F饮水机',
deviceId: 'B301',
time: '09:28',
amount: '200ml'
},
{
id: 3,
date: '今日',
deviceName: '湖南大学教学楼1F饮水机',
deviceId: 'A201',
time: '08:30',
amount: '200ml'
},
{
id: 4,
date: '07-20',
deviceName: '湖南大学教学楼1F饮水机',
deviceId: 'A201',
time: '12:40',
amount: '200ml'
},
{
id: 5,
date: '07-19',
deviceName: '湖南大学教学楼1F饮水机',
deviceId: 'A201',
time: '12:08',
amount: '200ml'
},
{
id: 6,
date: '07-19',
deviceName: '湖南大学教学楼1F饮水机',
deviceId: 'A201',
time: '12:08',
amount: '200ml'
},
{
id: 7,
date: '07-18',
deviceName: '湖南大学教学楼1F饮水机',
deviceId: 'A201',
time: '12:08',
amount: '200ml'
},
{
id: 8,
date: '07-18',
deviceName: '湖南大学教学楼1F饮水机',
deviceId: 'A201',
time: '12:08',
amount: '200ml'
}
])
//
const dateGroupedRecords = computed(() => {
const grouped = {}
historyList.value.forEach(record => {
if (record.date !== '今日') {
if (!grouped[record.date]) {
grouped[record.date] = []
}
grouped[record.date].push(record)
}
})
return grouped
})
//
const todayRecords = computed(() => {
return historyList.value.filter(record => record.date === '今日')
})
//
const loadHistoryFromStorage = () => {
const savedHistory = localStorage.getItem('waterHistory')
if (savedHistory) {
try {
const parsedHistory = JSON.parse(savedHistory)
//
if (parsedHistory && parsedHistory.length > 0) {
historyList.value = [...parsedHistory, ...historyList.value]
}
} catch (error) {
console.error('加载历史记录失败:', error)
}
}
}
//
const goBack = () => {
router.back()
}
//
const goToPage = (page) => {
switch(page) {
case 'home':
router.push('/home')
break
case 'scan':
router.push('/scan')
break
case 'profile':
router.push('/profile')
break
}
}
onMounted(() => {
loadHistoryFromStorage()
})
</script>
<style scoped>
.history-page {
width: 375px;
height: 667px;
background: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
height: 40px;
background: linear-gradient(135deg, #1156b1 0%, #81d3f8 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.header-title {
font-size: 16px;
font-weight: 600;
color: white;
letter-spacing: 1px;
}
.back-btn {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 20px 0;
overflow-y: auto;
}
/* 统计信息 */
.summary-info {
padding: 0 16px 16px;
}
.total-count {
font-size: 14px;
color: #666;
font-weight: 500;
}
/* 历史记录列表 */
.history-list {
padding: 0 16px;
}
.date-group {
margin-bottom: 20px;
}
.group-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
padding-left: 8px;
border-left: 4px solid #1890ff;
}
.records-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.record-card {
background: white;
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #e8e8e8;
transition: all 0.3s;
}
.record-card:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #1890ff;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.device-name {
font-size: 14px;
font-weight: 500;
color: #333;
flex: 1;
margin-right: 12px;
}
.record-time {
font-size: 12px;
color: #999;
flex-shrink: 0;
}
.record-details {
display: flex;
justify-content: space-between;
align-items: center;
}
.water-amount {
font-size: 14px;
font-weight: 600;
color: #1890ff;
}
.device-id {
font-size: 12px;
color: #666;
background: #f8f9fa;
padding: 4px 8px;
border-radius: 12px;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.3;
}
.empty-text {
font-size: 16px;
color: #666;
margin-bottom: 8px;
}
.empty-hint {
font-size: 14px;
color: #999;
}
/* 底部导航栏 */
.bottom-nav {
height: 60px;
background: white;
border-top: 1px solid #e8e8e8;
display: grid;
grid-template-columns: repeat(3, 1fr);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.nav-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
color: #666;
background: none;
border: none;
padding: 0;
}
.nav-button:hover {
background: #f8f9fa;
}
.nav-button.active {
color: #1890ff;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
}
.nav-text {
font-size: 12px;
font-weight: 500;
}
/* 响应式调整 */
@media (max-width: 420px) {
.history-page {
width: 100%;
height: 100vh;
}
.main-content {
padding: 16px 0;
}
.record-card {
padding: 12px;
}
.device-name {
font-size: 13px;
}
}
</style>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,679 @@
<template>
<div class="profile-page">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-title">个人主页</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 用户信息卡片 -->
<div class="user-card">
<div class="avatar-section">
<div class="avatar-circle">
<div class="avatar-text">{{ userInfo.lastName }}</div>
</div>
<div class="user-name">{{ userInfo.fullName }}</div>
</div>
<div class="divider"></div>
<!-- 用户详细信息 -->
<div class="user-details">
<div class="detail-item">
<span class="detail-label">学号</span>
<span class="detail-value">{{ userInfo.studentId }}</span>
</div>
<div class="detail-item">
<span class="detail-label">学院</span>
<span class="detail-value">{{ userInfo.college }}</span>
</div>
<div class="detail-item">
<span class="detail-label">班级</span>
<span class="detail-value">{{ userInfo.class }}</span>
</div>
</div>
</div>
<!-- 数据统计卡片 -->
<div class="stats-section">
<div class="stats-cards">
<div class="stat-card">
<div class="stat-value">{{ userStats.days }}</div>
<div class="stat-label">累计用水天数</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ userStats.todayWater }}</div>
<div class="stat-label">今日饮水量</div>
</div>
</div>
</div>
<!-- 时间切换 -->
<div class="time-switch">
<div
class="time-option"
:class="{ active: selectedPeriod === 'week' }"
@click="selectPeriod('week')"
>
本周
</div>
<div
class="time-option"
:class="{ active: selectedPeriod === 'month' }"
@click="selectPeriod('month')"
>
本月
</div>
</div>
<!-- 饮水数据图表 -->
<div class="chart-section">
<div class="chart-title">饮水统计图表</div>
<div class="chart-container">
<!-- 柱状图容器 -->
<div class="chart-placeholder" v-if="!showChart">
<div class="placeholder-text">数据加载中...</div>
</div>
<!-- 模拟柱状图 -->
<div class="mock-chart" v-else>
<div class="chart-axis">
<!-- Y轴 -->
<div class="y-axis">
<div class="y-label">800ml</div>
<div class="y-label">600ml</div>
<div class="y-label">400ml</div>
<div class="y-label">200ml</div>
<div class="y-label">0ml</div>
</div>
<!-- 柱状图 -->
<div class="bars-container">
<div
v-for="(item, index) in chartData"
:key="index"
class="bar-item"
>
<div
class="bar"
:style="{ height: item.value + 'px' }"
:class="{ active: item.active }"
>
<div class="bar-value">{{ item.value }}ml</div>
</div>
<div class="bar-label">{{ item.label }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 功能按钮 -->
<div class="action-section">
<button class="action-btn history-btn" @click="goToHistory">
<span class="btn-icon">📊</span>
<span class="btn-text">查看历史饮水记录</span>
<span class="btn-arrow"></span>
</button>
<button class="action-btn setting-btn" @click="goToSettings">
<span class="btn-icon"></span>
<span class="btn-text">设置</span>
<span class="btn-arrow"></span>
</button>
<button class="action-btn logout-btn" @click="handleLogout">
<span class="btn-icon">🚪</span>
<span class="btn-text">退出登录</span>
</button>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-button" @click="goToPage('home')">
<div class="nav-icon">🗺</div>
<div class="nav-text">地图</div>
</div>
<div class="nav-button" @click="goToPage('scan')">
<div class="nav-icon">📷</div>
<div class="nav-text">扫码</div>
</div>
<div class="nav-button active" @click="goToPage('profile')">
<div class="nav-icon">👤</div>
<div class="nav-text">我的</div>
</div>
</div>
</div>
</template>
<script setup>
// ProfilePage.vue script setup
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = useRouter()
const userStore = useUserStore()
//
const userInfo = reactive({
lastName: '',
fullName: '',
studentId: '',
college: '信息科学与工程学院',
class: '软件2301班'
})
//
const userStats = reactive({
days: '26天',
todayWater: '500ml'
})
//
const selectedPeriod = ref('week')
const showChart = ref(false)
const chartData = ref([])
//
const initChartData = () => {
if (selectedPeriod.value === 'week') {
//
chartData.value = [
{ label: '周一', value: 450, active: false },
{ label: '周二', value: 620, active: false },
{ label: '周三', value: 380, active: false },
{ label: '周四', value: 540, active: true },
{ label: '周五', value: 280, active: false },
{ label: '周六', value: 720, active: false },
{ label: '周日', value: 400, active: false }
]
} else {
// 4
chartData.value = [
{ label: '第1周', value: 1800, active: false },
{ label: '第2周', value: 2200, active: false },
{ label: '第3周', value: 2500, active: true },
{ label: '第4周', value: 800, active: false }
]
}
}
//
const selectPeriod = (period) => {
selectedPeriod.value = period
initChartData()
}
//
const goToHistory = () => {
router.push('/history')
}
//
const goToSettings = () => {
alert('设置页面(待开发)')
}
// 退
const handleLogout = () => {
if (confirm('确定要退出登录吗?')) {
//
userStore.clearUser()
//
router.push('/')
}
}
//
const goToPage = (page) => {
switch(page) {
case 'home':
router.push('/home')
break
case 'scan':
router.push('/scan')
break
case 'profile':
//
break
}
}
onMounted(() => {
//
if (userStore.isLoggedIn) {
userInfo.studentId = userStore.studentId
userInfo.fullName = userStore.username
userInfo.lastName = userStore.username.charAt(0)
}
//
initChartData()
//
setTimeout(() => {
showChart.value = true
}, 500)
})
</script>
<style scoped>
.profile-page {
width: 375px;
height: 667px;
background: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
height: 40px;
background: linear-gradient(135deg, #1156b1 0%, #81d3f8 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.header-title {
font-size: 16px;
font-weight: 600;
color: white;
letter-spacing: 1px;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 20px 16px;
overflow-y: auto;
}
/* 用户信息卡片 */
.user-card {
background: white;
border-radius: 16px;
padding: 24px 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.avatar-section {
text-align: center;
margin-bottom: 20px;
}
.avatar-circle {
width: 70px;
height: 70px;
background: linear-gradient(135deg, #409eff, #66b1ff);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
border: 3px solid #f2f2f2;
}
.avatar-text {
font-size: 28px;
font-weight: 600;
color: white;
}
.user-name {
font-size: 18px;
font-weight: 600;
color: #333;
}
/* 分割线 */
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #e8e8e8, transparent);
margin: 20px 0;
}
/* 用户详细信息 */
.user-details {
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-item {
display: flex;
align-items: center;
font-size: 14px;
}
.detail-label {
color: #666;
min-width: 60px;
}
.detail-value {
color: #333;
font-weight: 500;
}
/* 数据统计卡片 */
.stats-section {
margin-bottom: 20px;
}
.stats-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px 16px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #e8e8e8;
transition: all 0.3s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #1890ff;
margin-bottom: 8px;
}
.stat-label {
font-size: 12px;
color: #666;
}
/* 时间切换 */
.time-switch {
display: flex;
background: white;
border-radius: 25px;
padding: 4px;
margin-bottom: 20px;
border: 1px solid #e8e8e8;
}
.time-option {
flex: 1;
text-align: center;
padding: 10px;
font-size: 14px;
color: #666;
cursor: pointer;
border-radius: 21px;
transition: all 0.3s;
}
.time-option.active {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
font-weight: 500;
}
/* 图表区域 */
.chart-section {
background: white;
border-radius: 16px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
}
.chart-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-left: 4px;
border-left: 4px solid #1890ff;
}
.chart-container {
min-height: 200px;
position: relative;
}
.chart-placeholder {
width: 100%;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
background: #f8f9fa;
border-radius: 8px;
border: 1px dashed #e8e8e8;
}
.placeholder-text {
color: #999;
font-size: 14px;
}
/* 模拟柱状图 */
.mock-chart {
height: 200px;
position: relative;
}
.chart-axis {
display: flex;
height: 100%;
padding-left: 40px;
position: relative;
}
.y-axis {
position: absolute;
left: 0;
top: 0;
bottom: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
width: 40px;
}
.y-label {
font-size: 10px;
color: #999;
transform: translateY(50%);
}
.bars-container {
flex: 1;
display: flex;
justify-content: space-around;
align-items: flex-end;
height: 100%;
padding-bottom: 20px;
border-bottom: 1px solid #e8e8e8;
}
.bar-item {
display: flex;
flex-direction: column;
align-items: center;
height: 100%;
width: 30px;
}
.bar {
width: 20px;
background: linear-gradient(to top, #409eff, #66b1ff);
border-radius: 4px 4px 0 0;
position: relative;
transition: height 0.5s ease;
margin-bottom: 4px;
}
.bar.active {
background: linear-gradient(to top, #ff6b6b, #ff8e8e);
}
.bar-value {
position: absolute;
top: -24px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
color: #666;
white-space: nowrap;
}
.bar-label {
font-size: 10px;
color: #666;
margin-top: 4px;
}
/* 功能按钮区域 */
.action-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.action-btn {
background: white;
border: 1px solid #e8e8e8;
border-radius: 12px;
padding: 16px 20px;
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.3s;
}
.action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.btn-icon {
font-size: 20px;
margin-right: 12px;
}
.btn-text {
flex: 1;
text-align: left;
font-size: 14px;
color: #333;
font-weight: 500;
}
.btn-arrow {
color: #999;
font-size: 18px;
}
.history-btn:hover {
border-color: #409eff;
background: #f0f5ff;
}
.setting-btn:hover {
border-color: #67c23a;
background: #f0f9eb;
}
.logout-btn:hover {
border-color: #f56c6c;
background: #fef0f0;
}
/* 底部导航栏 */
.bottom-nav {
height: 60px;
background: white;
border-top: 1px solid #e8e8e8;
display: grid;
grid-template-columns: repeat(3, 1fr);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.nav-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
color: #666;
background: none;
border: none;
padding: 0;
}
.nav-button:hover {
background: #f8f9fa;
}
.nav-button.active {
color: #1890ff;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
}
.nav-text {
font-size: 12px;
font-weight: 500;
}
/* 响应式调整 */
@media (max-width: 420px) {
.profile-page {
width: 100%;
height: 100vh;
}
.main-content {
padding: 16px 12px;
}
.stats-cards {
grid-template-columns: 1fr;
}
.user-card {
padding: 20px 16px;
}
.bars-container {
gap: 4px;
}
.bar-item {
width: 25px;
}
}
</style>

@ -0,0 +1,821 @@
<template>
<div class="scan-page">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-title">扫码取水</div>
<button class="back-btn" @click="goBack"></button>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 扫描区域 -->
<div class="scan-section" v-if="!deviceInfo">
<div class="scan-area">
<!-- 扫描框 -->
<div class="scan-frame">
<div class="scan-lines">
<div class="scan-line"></div>
</div>
<div class="scan-corners">
<div class="corner top-left"></div>
<div class="corner top-right"></div>
<div class="corner bottom-left"></div>
<div class="corner bottom-right"></div>
</div>
</div>
<div class="scan-instruction">
请扫描设备二维码
</div>
<div class="scan-hint">
将二维码放入框内即可自动扫描
</div>
</div>
<!-- 手动输入选项 -->
<div class="manual-input" @click="showManualInput">
<span class="manual-icon">🔢</span>
<span class="manual-text">手动输入设备ID</span>
</div>
</div>
<!-- 设备信息区域扫描后显示 -->
<div class="device-section" v-else>
<div class="device-card">
<div class="device-header">
<div class="device-icon">🚰</div>
<div class="device-info">
<div class="device-name">{{ deviceInfo.name }}</div>
<div class="device-id">ID: {{ deviceInfo.id }}</div>
</div>
<div class="device-status" :class="deviceInfo.status">
{{ deviceInfo.statusText }}
</div>
</div>
<div class="divider"></div>
<!-- 取水量选择 -->
<div class="water-amount-section">
<div class="section-title">选择取水量</div>
<div class="amount-options">
<div
v-for="amount in waterAmounts"
:key="amount.value"
class="amount-option"
:class="{ selected: selectedAmount === amount.value }"
@click="selectAmount(amount.value)"
>
<div class="amount-value">{{ amount.value }}ml</div>
<div class="amount-price">{{ amount.price }}</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<button class="action-btn primary" @click="confirmWater" :disabled="!selectedAmount">
确认取水
</button>
<button class="action-btn secondary" @click="viewWaterQuality">
查看水质
</button>
</div>
</div>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-button" @click="goToPage('home')">
<div class="nav-icon">🗺</div>
<div class="nav-text">地图</div>
</div>
<div class="nav-button active" @click="goToPage('scan')">
<div class="nav-icon">📷</div>
<div class="nav-text">扫码</div>
</div>
<div class="nav-button" @click="goToPage('profile')">
<div class="nav-icon">👤</div>
<div class="nav-text">我的</div>
</div>
</div>
<!-- 手动输入弹窗 -->
<div v-if="showManualDialog" class="dialog-overlay">
<div class="dialog-content">
<div class="dialog-header">
<h3>手动输入设备ID</h3>
<button class="close-btn" @click="closeManualDialog"></button>
</div>
<div class="dialog-body">
<input
type="text"
v-model="manualDeviceId"
placeholder="请输入设备IDA201"
class="device-input"
@keyup.enter="submitManualInput"
/>
<div class="input-hint">可在设备上找到二维码下方的ID</div>
</div>
<div class="dialog-footer">
<button class="dialog-btn secondary" @click="closeManualDialog">
取消
</button>
<button class="dialog-btn primary" @click="submitManualInput">
确定
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
//
const deviceInfo = ref(null)
const showManualDialog = ref(false)
const manualDeviceId = ref('')
const selectedAmount = ref(null)
//
const waterAmounts = [
{ value: 150, price: '免费' },
{ value: 200, price: '免费' },
{ value: 250, price: '免费' }
]
//
const mockDevices = {
'A201': {
id: 'A201',
name: '湖南大学教学楼1F饮水机',
status: 'online',
statusText: '在线'
},
'B201': {
id: 'B201',
name: '天马学生公寓1F饮水机',
status: 'online',
statusText: '在线'
},
'C101': {
id: 'C101',
name: '图书馆2F饮水机',
status: 'offline',
statusText: '离线'
}
}
// 使API
const simulateScan = () => {
// A201
setTimeout(() => {
deviceInfo.value = mockDevices['A201']
}, 1000)
}
onMounted(() => {
//
//
simulateScan()
})
//
const selectAmount = (amount) => {
selectedAmount.value = amount
}
//
const confirmWater = () => {
if (!selectedAmount.value) {
alert('请选择取水量')
return
}
alert(`开始取水:${selectedAmount.value}ml`)
// API
}
//
const viewWaterQuality = () => {
if (deviceInfo.value) {
router.push({
path: '/water-quality',
query: { deviceId: deviceInfo.value.id }
})
}
}
//
const showManualInput = () => {
showManualDialog.value = true
}
//
const closeManualDialog = () => {
showManualDialog.value = false
manualDeviceId.value = ''
}
//
const submitManualInput = () => {
if (!manualDeviceId.value.trim()) {
alert('请输入设备ID')
return
}
const device = mockDevices[manualDeviceId.value.trim()]
if (device) {
deviceInfo.value = device
closeManualDialog()
} else {
alert('设备ID不存在请检查后重新输入')
}
}
//
const goBack = () => {
if (deviceInfo.value) {
//
deviceInfo.value = null
selectedAmount.value = null
} else {
//
router.push('/home')
}
}
//
const goToPage = (page) => {
switch(page) {
case 'home':
router.push('/home')
break
case 'scan':
//
break
case 'profile':
router.push('/profile')
break
}
}
</script>
<style scoped>
.scan-page {
width: 375px;
height: 667px;
background: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
height: 40px;
background: linear-gradient(135deg, #1156b1 0%, #81d3f8 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.header-title {
font-size: 16px;
font-weight: 600;
color: white;
letter-spacing: 1px;
}
.back-btn {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
/* 扫描区域 */
.scan-section {
text-align: center;
}
.scan-area {
background: white;
border-radius: 12px;
padding: 30px 20px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
/* 扫描框 */
.scan-frame {
width: 250px;
height: 250px;
margin: 0 auto 20px;
position: relative;
border: 2px solid rgba(24, 144, 255, 0.3);
border-radius: 12px;
background: rgba(255, 255, 255, 0.8);
overflow: hidden;
}
/* 扫描线动画 */
.scan-lines {
position: absolute;
width: 100%;
height: 100%;
}
.scan-line {
position: absolute;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #1890ff, transparent);
top: 20%;
animation: scanLine 2s infinite linear;
}
@keyframes scanLine {
0% {
top: 20%;
}
50% {
top: 80%;
}
100% {
top: 20%;
}
}
/* 扫描框四角 */
.scan-corners {
position: absolute;
width: 100%;
height: 100%;
}
.corner {
position: absolute;
width: 20px;
height: 20px;
border: 3px solid #1890ff;
}
.corner.top-left {
top: -3px;
left: -3px;
border-right: none;
border-bottom: none;
border-radius: 6px 0 0 0;
}
.corner.top-right {
top: -3px;
right: -3px;
border-left: none;
border-bottom: none;
border-radius: 0 6px 0 0;
}
.corner.bottom-left {
bottom: -3px;
left: -3px;
border-right: none;
border-top: none;
border-radius: 0 0 0 6px;
}
.corner.bottom-right {
bottom: -3px;
right: -3px;
border-left: none;
border-top: none;
border-radius: 0 0 6px 0;
}
.scan-instruction {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.scan-hint {
font-size: 14px;
color: #666;
}
/* 手动输入 */
.manual-input {
background: white;
border: 1px dashed #1890ff;
border-radius: 8px;
padding: 12px 16px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
cursor: pointer;
transition: all 0.3s;
}
.manual-input:hover {
background: #f0f5ff;
transform: translateY(-1px);
}
.manual-icon {
font-size: 18px;
}
.manual-text {
font-size: 14px;
color: #1890ff;
font-weight: 500;
}
/* 设备信息卡片 */
.device-section {
padding: 10px 0;
}
.device-card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.device-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.device-icon {
font-size: 32px;
}
.device-info {
flex: 1;
}
.device-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.device-id {
font-size: 14px;
color: #1890ff;
font-weight: 500;
}
.device-status {
font-size: 12px;
font-weight: 600;
padding: 4px 12px;
border-radius: 12px;
}
.device-status.online {
background: rgba(4, 217, 25, 0.1);
color: #04d919;
}
.device-status.offline {
background: rgba(170, 170, 170, 0.1);
color: #aaaaaa;
}
/* 分割线 */
.divider {
height: 1px;
background: linear-gradient(90deg, transparent, #e8e8e8, transparent);
margin: 20px 0;
}
/* 取水量选择 */
.water-amount-section {
margin-bottom: 20px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-left: 4px;
border-left: 4px solid #1890ff;
}
.amount-options {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.amount-option {
background: #f8f9fa;
border: 1px solid #e8e8e8;
border-radius: 8px;
padding: 16px 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s;
}
.amount-option:hover {
transform: translateY(-2px);
border-color: #1890ff;
}
.amount-option.selected {
background: #f0f5ff;
border-color: #1890ff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.2);
}
.amount-value {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 4px;
}
.amount-price {
font-size: 12px;
color: #1890ff;
font-weight: 500;
}
/* 操作按钮 */
.action-buttons {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.action-btn {
padding: 14px;
border: none;
border-radius: 8px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.action-btn.primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
}
.action-btn.secondary {
background: #f0f5ff;
color: #1890ff;
border: 1px solid #d6e4ff;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.action-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
}
/* 底部导航栏 */
.bottom-nav {
height: 60px;
background: white;
border-top: 1px solid #e8e8e8;
display: grid;
grid-template-columns: repeat(3, 1fr);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.nav-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
color: #666;
background: none;
border: none;
padding: 0;
}
.nav-button:hover {
background: #f8f9fa;
}
.nav-button.active {
color: #1890ff;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
}
.nav-text {
font-size: 12px;
font-weight: 500;
}
/* 手动输入弹窗 */
.dialog-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.dialog-content {
background: white;
border-radius: 16px;
width: 320px;
max-width: 90%;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 20px 16px;
border-bottom: 1px solid #e8e8e8;
}
.dialog-header h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.close-btn {
width: 28px;
height: 28px;
border: none;
background: #f5f5f5;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 14px;
color: #666;
}
.dialog-body {
padding: 20px;
}
.device-input {
width: 100%;
padding: 12px;
border: 1px solid #e8e8e8;
border-radius: 6px;
font-size: 14px;
margin-bottom: 8px;
}
.device-input:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.input-hint {
font-size: 12px;
color: #666;
text-align: left;
}
.dialog-footer {
display: flex;
gap: 12px;
padding: 16px 20px 20px;
}
.dialog-btn {
flex: 1;
padding: 12px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.dialog-btn.primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
}
.dialog-btn.secondary {
background: #f5f5f5;
color: #666;
border: 1px solid #e8e8e8;
}
.dialog-btn:hover {
transform: translateY(-1px);
}
.dialog-btn.primary:hover {
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
/* 响应式调整 */
@media (max-width: 420px) {
.scan-page {
width: 100%;
height: 100vh;
}
.main-content {
padding: 16px;
}
.scan-frame {
width: 200px;
height: 200px;
}
.amount-options {
grid-template-columns: 1fr;
}
.action-buttons {
grid-template-columns: 1fr;
}
}
</style>

@ -0,0 +1,761 @@
<template>
<div class="student-login-page">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-title">学生服务平台</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 登录表单 -->
<div class="login-form" v-if="!showRegister">
<div class="welcome-text">
<h2>欢迎回来</h2>
<p>请登录您的账户</p>
</div>
<div class="form-group">
<label for="studentId">学号</label>
<input
type="text"
id="studentId"
v-model="loginForm.studentId"
placeholder="请输入学号"
@keyup.enter="handleLogin"
/>
</div>
<div class="form-group">
<label for="password">密码</label>
<div class="password-input">
<input
:type="showPassword ? 'text' : 'password'"
id="password"
v-model="loginForm.password"
placeholder="请输入密码"
@keyup.enter="handleLogin"
/>
<button
type="button"
class="password-toggle"
@click="showPassword = !showPassword"
>
<span v-if="showPassword"></span>
<span v-else></span>
</button>
</div>
</div>
<div class="form-options">
<label class="remember-me">
<input type="checkbox" v-model="loginForm.rememberMe" />
<span>记住我</span>
</label>
<button class="forgot-password" @click="handleForgotPassword">
忘记密码?
</button>
</div>
<button class="login-btn primary" @click="handleLogin" :disabled="loading">
<span v-if="loading">...</span>
<span v-else></span>
</button>
<div class="register-prompt">
还没有账户
<button class="register-link" @click="showRegister = true">
立即注册
</button>
</div>
</div>
<!-- 注册表单 -->
<div class="register-form" v-else>
<div class="welcome-text">
<h2>创建账户</h2>
<p>注册学生账户</p>
</div>
<div class="form-group">
<label for="regStudentId">学号</label>
<input
type="text"
id="regStudentId"
v-model="registerForm.studentId"
placeholder="请输入学号"
:class="{ 'has-error': errors.studentId }"
/>
<div v-if="errors.studentId" class="error-message">
{{ errors.studentId }}
</div>
</div>
<div class="form-group">
<label for="regName">姓名</label>
<input
type="text"
id="regName"
v-model="registerForm.name"
placeholder="请输入真实姓名"
:class="{ 'has-error': errors.name }"
/>
<div v-if="errors.name" class="error-message">
{{ errors.name }}
</div>
</div>
<div class="form-group">
<label for="regPhone">手机号</label>
<input
type="tel"
id="regPhone"
v-model="registerForm.phone"
placeholder="请输入手机号"
:class="{ 'has-error': errors.phone }"
/>
<div v-if="errors.phone" class="error-message">
{{ errors.phone }}
</div>
</div>
<div class="form-group">
<label for="regPassword">密码</label>
<div class="password-input">
<input
:type="showRegPassword ? 'text' : 'password'"
id="regPassword"
v-model="registerForm.password"
placeholder="请输入密码6-20位"
:class="{ 'has-error': errors.password }"
/>
<button
type="button"
class="password-toggle"
@click="showRegPassword = !showRegPassword"
>
<span v-if="showRegPassword"></span>
<span v-else></span>
</button>
</div>
<div v-if="errors.password" class="error-message">
{{ errors.password }}
</div>
</div>
<div class="form-group">
<label for="regConfirmPassword">确认密码</label>
<div class="password-input">
<input
:type="showConfirmPassword ? 'text' : 'password'"
id="regConfirmPassword"
v-model="registerForm.confirmPassword"
placeholder="请再次输入密码"
:class="{ 'has-error': errors.confirmPassword }"
/>
<button
type="button"
class="password-toggle"
@click="showConfirmPassword = !showConfirmPassword"
>
<span v-if="showConfirmPassword"></span>
<span v-else></span>
</button>
</div>
<div v-if="errors.confirmPassword" class="error-message">
{{ errors.confirmPassword }}
</div>
</div>
<div class="form-group">
<label for="regEmail">邮箱选填</label>
<input
type="email"
id="regEmail"
v-model="registerForm.email"
placeholder="请输入邮箱"
:class="{ 'has-error': errors.email }"
/>
<div v-if="errors.email" class="error-message">
{{ errors.email }}
</div>
</div>
<div class="agreement">
<label>
<input type="checkbox" v-model="registerForm.agreed" />
<span>我已阅读并同意</span>
<button class="agreement-link" @click="showAgreement">
用户协议
</button>
<button class="agreement-link" @click="showPrivacy">
隐私政策
</button>
</label>
<div v-if="errors.agreement" class="error-message">
{{ errors.agreement }}
</div>
</div>
<div class="action-buttons">
<button class="register-btn primary" @click="handleRegister" :disabled="loading">
<span v-if="loading">...</span>
<span v-else></span>
</button>
<button class="register-btn secondary" @click="showRegister = false">
返回登录
</button>
</div>
</div>
</div>
<!-- 底部信息 -->
<div class="footer">
<!-- 这里只保留空白没有文字内容 -->
</div>
</div>
</template>
<!-- 只修改 <script> 部分<template> <style> 保持不变 -->
<script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { authServices } from '@/services/authServices' //
import { onMounted } from 'vue'
onMounted(() => {
//
const token = localStorage.getItem('token')
if (token) {
// token
router.push('/home')
}
//
initRememberedUser()
})
const router = useRouter()
// /
const showRegister = ref(false)
//
const loginForm = reactive({
studentId: '',
password: '',
rememberMe: false
})
//
const registerForm = reactive({
studentId: '',
name: '',
phone: '',
password: '',
confirmPassword: '',
email: '',
agreed: false
})
//
const errors = reactive({
studentId: '',
name: '',
phone: '',
password: '',
confirmPassword: '',
email: '',
agreement: ''
})
//
const loading = ref(false)
const showPassword = ref(false)
const showRegPassword = ref(false)
const showConfirmPassword = ref(false)
//
const validateLogin = () => {
if (!loginForm.studentId.trim()) {
alert('请输入学号')
return false
}
if (!loginForm.password) {
alert('请输入密码')
return false
}
return true
}
const validateRegister = () => {
let isValid = true
Object.keys(errors).forEach(key => errors[key] = '')
if (!registerForm.studentId.trim()) {
errors.studentId = '请输入学号'
isValid = false
}
if (!registerForm.name.trim()) {
errors.name = '请输入姓名'
isValid = false
}
if (!registerForm.phone.trim()) {
errors.phone = '请输入手机号'
isValid = false
}
if (!registerForm.password) {
errors.password = '请输入密码'
isValid = false
} else if (registerForm.password.length < 6) {
errors.password = '密码长度至少6位'
isValid = false
}
if (registerForm.password !== registerForm.confirmPassword) {
errors.confirmPassword = '两次输入的密码不一致'
isValid = false
}
if (!registerForm.agreed) {
errors.agreement = '请阅读并同意用户协议和隐私政策'
isValid = false
}
return isValid
}
//
// StudentLoginPage.vue handleLogin
const handleLogin = async () => {
if (!validateLogin()) return
loading.value = true
try {
console.log('开始学生登录:', loginForm.studentId)
//
const result = await authServices.studentLogin(
loginForm.studentId,
loginForm.password
)
console.log('登录响应:', result)
if (result.code === 200) {
//
const loginData = result.data
//
localStorage.setItem('token', loginData.token)
localStorage.setItem('userId', loginData.userId)
localStorage.setItem('username', loginData.username)
localStorage.setItem('userType', loginData.userType)
localStorage.setItem('studentId', loginForm.studentId)
//
if (loginForm.rememberMe) {
localStorage.setItem('rememberedStudentId', loginForm.studentId)
} else {
localStorage.removeItem('rememberedStudentId')
}
alert('登录成功!')
//
router.push('/home')
} else {
//
alert('登录失败: ' + (result.message || '未知错误'))
}
} catch (error) {
console.error('登录过程异常:', error)
//
if (error.message && error.message.includes('Network Error')) {
alert('网络连接失败,请检查网络或服务器状态')
} else if (error.message && error.message.includes('timeout')) {
alert('请求超时,请稍后重试')
} else if (error.code === 500) {
alert('服务器内部错误,请联系管理员')
} else {
alert('登录失败: ' + (error.message || '未知错误'))
}
} finally {
loading.value = false
}
}
//
// <script setup> handleRegister
const handleRegister = async () => {
if (!validateRegister()) return
loading.value = true
try {
// RegisterRequest
const registerData = {
studentId: registerForm.studentId.trim(),
name: registerForm.name.trim(),
password: registerForm.password,
// RegisterRequestphoneemail
// RegisterRequestUser
}
console.log('前端注册数据:', registerData)
//
const result = await authServices.studentRegister(registerData)
console.log('注册完整响应:', result)
// ResultVO
if (result && result.code === 200) {
//
alert('注册成功!请使用您的学号和密码登录')
showRegister.value = false
//
Object.keys(registerForm).forEach(key => {
if (key !== 'agreed') {
registerForm[key] = ''
}
})
registerForm.agreed = false
//
Object.keys(errors).forEach(key => errors[key] = '')
} else {
//
const errorMsg = result?.message || '注册失败,请稍后重试'
alert('注册失败: ' + errorMsg)
}
} catch (error) {
console.error('注册过程异常:', error)
//
let errorMessage = '注册失败,请稍后重试'
if (error.code === 400) {
errorMessage = error.message || '请求参数错误,请检查填写的信息'
} else if (error.code === 409) {
errorMessage = error.message || '用户已存在'
} else if (error.message) {
// message
if (typeof error.message === 'object' && error.message.message) {
errorMessage = error.message.message
} else if (typeof error.message === 'string') {
errorMessage = error.message
}
}
alert('注册失败: ' + errorMessage)
} finally {
loading.value = false
}
}
//
const handleForgotPassword = () => {
alert('请联系管理员重置密码admin@example.com')
}
const showAgreement = () => {
alert('用户协议内容...')
}
const showPrivacy = () => {
alert('隐私政策内容...')
}
//
const initRememberedUser = () => {
const rememberedStudentId = localStorage.getItem('rememberedStudentId')
if (rememberedStudentId) {
loginForm.studentId = rememberedStudentId
loginForm.rememberMe = true
}
}
//
initRememberedUser()
</script>
<style scoped>
.student-login-page {
width: 375px;
height: 667px;
background: #f8f9fa;
display: flex;
flex-direction: column;
position: relative;
overflow-y: auto;
}
/* 顶部标题栏 */
.header {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
padding: 16px;
color: white;
text-align: center;
}
.header-title {
font-size: 18px;
font-weight: 600;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 24px 16px;
overflow-y: auto;
}
/* 欢迎文本 */
.welcome-text {
text-align: center;
margin-bottom: 32px;
}
.welcome-text h2 {
font-size: 24px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.welcome-text p {
font-size: 14px;
color: #666;
}
/* 表单组 */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #e8e8e8;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
background: white;
}
.form-group input:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-group input.has-error {
border-color: #ff4d4f;
}
.form-group input::placeholder {
color: #999;
}
/* 密码输入框 */
.password-input {
position: relative;
}
.password-toggle {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: #1890ff;
font-size: 12px;
cursor: pointer;
padding: 4px;
}
/* 错误消息 */
.error-message {
color: #ff4d4f;
font-size: 12px;
margin-top: 4px;
}
/* 表单选项 */
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
font-size: 14px;
}
.remember-me {
display: flex;
align-items: center;
gap: 6px;
color: #666;
cursor: pointer;
}
.remember-me input {
width: 16px;
height: 16px;
}
.forgot-password {
background: none;
border: none;
color: #1890ff;
font-size: 14px;
cursor: pointer;
padding: 0;
}
/* 登录/注册按钮 */
.login-btn,
.register-btn {
width: 100%;
padding: 14px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
margin-bottom: 16px;
}
.login-btn.primary,
.register-btn.primary {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: white;
}
.login-btn.secondary,
.register-btn.secondary {
background: white;
color: #1890ff;
border: 1px solid #1890ff;
}
.login-btn:disabled,
.register-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-btn:hover:not(:disabled),
.register-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
/* 注册提示 */
.register-prompt {
text-align: center;
font-size: 14px;
color: #666;
margin-top: 24px;
}
.register-link {
background: none;
border: none;
color: #1890ff;
font-size: 14px;
cursor: pointer;
padding: 0;
font-weight: 500;
}
/* 协议同意 */
.agreement {
margin: 24px 0;
font-size: 12px;
color: #666;
}
.agreement label {
display: flex;
align-items: flex-start;
gap: 6px;
cursor: pointer;
}
.agreement input {
margin-top: 2px;
flex-shrink: 0;
}
.agreement-link {
background: none;
border: none;
color: #1890ff;
padding: 0;
cursor: pointer;
}
/* 注册按钮组 */
.action-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
/* 底部信息 - 保持布局但无内容 */
.footer {
padding: 8px;
text-align: center;
background: white;
border-top: 1px solid #e8e8e8;
}
/* 响应式调整 */
@media (max-width: 420px) {
.student-login-page {
width: 100%;
height: 100vh;
}
.main-content {
padding: 20px 12px;
}
.welcome-text {
margin-bottom: 24px;
}
.welcome-text h2 {
font-size: 22px;
}
.login-btn,
.register-btn {
padding: 12px;
}
.footer {
padding: 4px;
}
}
</style>

@ -0,0 +1,535 @@
<template>
<div class="water-quality-page">
<!-- 顶部标题栏 -->
<div class="header">
<div class="header-title">水质详情</div>
<button class="back-btn" @click="goBack"></button>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 设备信息 -->
<div class="device-info-section">
<h2 class="device-name">{{ deviceInfo.name }}</h2>
<div class="device-details">
<span class="distance">{{ deviceInfo.distance }}</span>
<span class="separator">|</span>
<span class="device-id">ID: {{ deviceInfo.id }}</span>
<span class="separator">|</span>
<span class="device-status" :class="deviceInfo.status">
{{ deviceInfo.statusText }}
</span>
</div>
</div>
<!-- 水质检测标题 -->
<div class="section-title">
<span>TDS水质检测</span>
</div>
<!-- TDS水质数据卡片 -->
<div class="quality-cards">
<div class="quality-card tap-water">
<div class="card-title">自来水TDS</div>
<div class="card-value">{{ deviceInfo.waterQuality.tapWater }}</div>
<div class="card-unit">mg/L</div>
</div>
<div class="quality-card pure-water">
<div class="card-title">纯净水TDS</div>
<div class="card-value">{{ deviceInfo.waterQuality.pureWater }}</div>
<div class="card-unit">mg/L</div>
</div>
<div class="quality-card mineral-water">
<div class="card-title">矿化水TDS</div>
<div class="card-value">{{ deviceInfo.waterQuality.mineralWater }}</div>
<div class="card-unit">mg/L</div>
</div>
</div>
<!-- 滤芯状态标题 -->
<div class="section-title">
<span>滤芯状态</span>
</div>
<!-- 滤芯状态 -->
<div class="filter-status-section">
<!-- 前置滤芯组 -->
<div class="filter-item">
<div class="filter-info">
<div class="filter-name">前置滤芯组</div>
<div class="filter-life">{{ deviceInfo.filters.preFilter.life }}</div>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deviceInfo.filters.preFilter.percentage + '%' }"></div>
</div>
</div>
<!-- 纯水滤芯组 -->
<div class="filter-item">
<div class="filter-info">
<div class="filter-name">纯水滤芯组</div>
<div class="filter-life">{{ deviceInfo.filters.pureFilter.life }}</div>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deviceInfo.filters.pureFilter.percentage + '%' }"></div>
</div>
</div>
<!-- 矿化滤芯组 -->
<div class="filter-item">
<div class="filter-info">
<div class="filter-name">矿化滤芯组</div>
<div class="filter-life">{{ deviceInfo.filters.mineralFilter.life }}</div>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: deviceInfo.filters.mineralFilter.percentage + '%' }"></div>
</div>
</div>
</div>
<!-- 水质说明 -->
<div class="quality-explanation">
<div class="explanation-title">水质说明</div>
<div class="explanation-content">
<div class="explanation-item">
<span class="explanation-label">自来水TDS</span>
<span class="explanation-value">{{ deviceInfo.waterQuality.tapWater }} mg/L</span>
<span class="explanation-status up">(偏高)</span>
</div>
<div class="explanation-item">
<span class="explanation-label">纯净水TDS</span>
<span class="explanation-value">{{ deviceInfo.waterQuality.pureWater }} mg/L</span>
<span class="explanation-status good">(优良)</span>
</div>
<div class="explanation-item">
<span class="explanation-label">矿化水TDS</span>
<span class="explanation-value">{{ deviceInfo.waterQuality.mineralWater }} mg/L</span>
<span class="explanation-status good">(优良)</span>
</div>
</div>
<div class="explanation-text">
TDS值越低水质越纯净纯净水TDS&lt;50为优良矿化水TDS 50-150为适宜范围
</div>
</div>
</div>
<!-- 底部导航栏 -->
<div class="bottom-nav">
<div class="nav-button" @click="goToPage('home')">
<div class="nav-icon">🗺</div>
<div class="nav-text">地图</div>
</div>
<div class="nav-button" @click="goToPage('scan')">
<div class="nav-icon">📷</div>
<div class="nav-text">扫码</div>
</div>
<div class="nav-button" @click="goToPage('profile')">
<div class="nav-icon">👤</div>
<div class="nav-text">我的</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
//
const deviceInfo = reactive({
name: '湖南大学教学楼1F饮水机',
distance: '128m',
id: 'A201',
status: 'online',
statusText: '在线',
waterQuality: {
tapWater: '285',
pureWater: '13',
mineralWater: '87'
},
filters: {
preFilter: {
name: '前置滤芯组',
life: '100%',
percentage: 100
},
pureFilter: {
name: '纯水滤芯组',
life: '80%',
percentage: 80
},
mineralFilter: {
name: '矿化滤芯组',
life: '70%',
percentage: 70
}
}
})
// ID
onMounted(() => {
//
const deviceId = router.currentRoute.value.query.deviceId || 'A201'
// deviceId
console.log('加载设备数据:', deviceId)
})
//
const goBack = () => {
router.back()
}
//
const goToPage = (page) => {
switch(page) {
case 'home':
router.push('/home')
break
case 'scan':
router.push('/scan')
break
case 'profile':
router.push('/profile')
break
}
}
</script>
<style scoped>
.water-quality-page {
width: 375px;
height: 667px;
background: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
/* 顶部标题栏 */
.header {
height: 40px;
background: linear-gradient(135deg, #1156b1 0%, #81d3f8 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
}
.header-title {
font-size: 16px;
font-weight: 600;
color: white;
letter-spacing: 1px;
}
.back-btn {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
}
/* 主要内容区域 */
.main-content {
flex: 1;
padding: 20px 16px;
overflow-y: auto;
}
/* 设备信息 */
.device-info-section {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.device-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.device-details {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.distance {
color: #666;
}
.separator {
color: #ccc;
}
.device-id {
color: #1890ff;
font-weight: 500;
}
.device-status {
font-weight: 600;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
.device-status.online {
background: rgba(4, 217, 25, 0.1);
color: #04d919;
}
.device-status.offline {
background: rgba(170, 170, 170, 0.1);
color: #aaaaaa;
}
/* 分区标题 */
.section-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin: 20px 0 16px;
padding-left: 4px;
border-left: 4px solid #1890ff;
}
/* TDS水质卡片 */
.quality-cards {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.quality-card {
background: white;
border-radius: 10px;
padding: 16px 12px;
text-align: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
border: 1px solid #e8e8e8;
transition: all 0.3s;
}
.quality-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.card-title {
font-size: 12px;
color: #666;
margin-bottom: 8px;
font-weight: 500;
}
.card-value {
font-size: 24px;
font-weight: bold;
color: #1890ff;
margin-bottom: 4px;
}
.card-unit {
font-size: 11px;
color: #999;
}
/* 滤芯状态 */
.filter-status-section {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.filter-item {
margin-bottom: 20px;
}
.filter-item:last-child {
margin-bottom: 0;
}
.filter-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.filter-name {
font-size: 14px;
font-weight: 500;
color: #333;
}
.filter-life {
font-size: 14px;
font-weight: 600;
color: #1890ff;
}
.progress-bar {
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #1890ff, #52c41a);
border-radius: 4px;
transition: width 0.5s ease;
}
/* 水质说明 */
.quality-explanation {
background: #f0f5ff;
border-radius: 8px;
padding: 16px;
margin-top: 20px;
border-left: 4px solid #1890ff;
}
.explanation-title {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.explanation-content {
margin-bottom: 12px;
}
.explanation-item {
display: flex;
align-items: center;
margin-bottom: 8px;
font-size: 13px;
}
.explanation-label {
color: #666;
min-width: 80px;
}
.explanation-value {
color: #333;
font-weight: 500;
margin: 0 8px;
}
.explanation-status {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
}
.explanation-status.good {
background: rgba(4, 217, 25, 0.1);
color: #04d919;
}
.explanation-status.up {
background: rgba(255, 152, 0, 0.1);
color: #ff9800;
}
.explanation-text {
font-size: 12px;
color: #666;
line-height: 1.4;
padding-top: 8px;
border-top: 1px solid rgba(24, 144, 255, 0.2);
}
/* 底部导航栏 */
.bottom-nav {
height: 60px;
background: white;
border-top: 1px solid #e8e8e8;
display: grid;
grid-template-columns: repeat(3, 1fr);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.nav-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s;
color: #666;
background: none;
border: none;
padding: 0;
}
.nav-button:hover {
background: #f8f9fa;
}
.nav-button.active {
color: #1890ff;
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
}
.nav-text {
font-size: 12px;
font-weight: 500;
}
/* 响应式调整 */
@media (max-width: 420px) {
.water-quality-page {
width: 100%;
height: 100vh;
}
.quality-cards {
grid-template-columns: 1fr;
}
.main-content {
padding: 16px 12px;
}
}
</style>

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})

@ -0,0 +1,14 @@
import { fileURLToPath } from 'node:url'
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
import viteConfig from './vite.config'
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
},
}),
)

@ -0,0 +1,38 @@
# MQTT 配置(是否启用 + 连接参数)
mqtt:
enabled: true # 是否启用 MQTT 客户端
jwt:
secret: "789&kLp23$87bnM90!789poI87&90lkJ78*90jhG78!90fdS78%90saD78^90xcV78&90zbN78!这是安全密钥1234567890"
expiration: 86400000
# Spring 核心配置:允许 Bean 定义覆盖(解决 Bean 重复定义冲突)
spring:
main:
allow-bean-definition-overriding: true # 缩进在 spring 下,作为子配置
# 数据库配置
datasource:
url: jdbc:mysql://120.46.151.248:3306/campus_water_management?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: member
password: Hdp11008@
driver-class-name: com.mysql.cj.jdbc.Driver
# JPA 配置
jpa:
hibernate:
ddl-auto: update # 自动更新表结构(开发环境用,生产环境建议改为 none
show-sql: true # 打印 SQL 语句
properties:
hibernate:
format_sql: true # 格式化 SQL 语句
jdbc.lob.non_contextual_creation: true # 解决 LOB 字段创建警告
dialect: org.hibernate.dialect.MySQL8Dialect # MySQL 8 方言
# 服务器编码配置
server:
servlet:
encoding:
charset: UTF-8
enabled: true
force: true
port: 8080

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save