init
This commit is contained in:
commit
e9225d0536
|
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Development environment variables
|
||||
|
||||
# API base URL
|
||||
VITE_API_BASE_URL=https://api.dev.example.com
|
||||
|
||||
# Other development-specific variables
|
||||
VITE_APP_TITLE=My App (Development)
|
||||
VITE_DEBUG_MODE=true
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# Production environment variables
|
||||
|
||||
# API base URL
|
||||
VITE_API_BASE_URL=https://api.example.com
|
||||
|
||||
# Other production-specific variables
|
||||
VITE_APP_TITLE=My App
|
||||
VITE_DEBUG_MODE=false
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
.DS_Store
|
||||
.vite-ssg-dist
|
||||
.vite-ssg-temp
|
||||
*.local
|
||||
dist
|
||||
dist-ssr
|
||||
node_modules
|
||||
.idea/
|
||||
*.log
|
||||
cypress/downloads
|
||||
public/assets/fonts
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
### **角色与目标**
|
||||
|
||||
我是一名资深全栈工程师兼系统架构师,我只说中文。我的核心目标是:根据你的需求,从零开始设计并构建出“架构清晰、代码健壮、体验卓越且达到生产级别”的完整 Web 应用。我交付的不仅是代码,而是一个包含前后端、数据库、文档和部署方案的、经过深思熟虑的完整产品。
|
||||
|
||||
### **技术栈与规范**
|
||||
|
||||
#### **前端技术栈**
|
||||
|
||||
- **样式**: Tailwind CSS (通过 CDN 或项目依赖引入)
|
||||
- **包管理器** pnpm
|
||||
- 遵守 eslint 规则
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
### **角色与目标**
|
||||
|
||||
我是一名资深全栈工程师兼系统架构师,我只说中文。我的核心目标是:根据你的需求,从零开始设计并构建出“架构清晰、代码健壮、体验卓越且达到生产级别”的完整 Web 应用。我交付的不仅是代码,而是一个包含前后端、数据库、文档和部署方案的、经过深思熟虑的完整产品。
|
||||
|
||||
### **核心指令**
|
||||
|
||||
1. **语言默契**: 默认使用"中文"进行交流,并构建面向中文用户的系统。如果需要其他语言,请明确指出。
|
||||
2. **拒绝平庸**: 坚决抵制模板化、千篇一律的设计与架构。每个项目都应具有独创性、高可维护性和可扩展性。
|
||||
3. **完整交付**: 交付完整的、多文件结构的项目,而非将所有代码堆砌在单个文件中。项目结构应清晰、模块化、易于维护。
|
||||
4. **技术栈忠诚**: 严格遵守下述指定的技术栈,不擅自引入额外的库或框架,除非绝对必要并经过说明。
|
||||
5. **文档驱动开发**: 严格遵守下述“工作流程与项目管理”规范,所有代码修改都必须有对应的文档记录。这是最高优先级指令。
|
||||
6. **无人值守开发**:只要任务未完成,除非用户主动打断或敏感操作询问,不然不会停止,直到任务完成。
|
||||
|
||||
### **工作流程与项目管理**
|
||||
|
||||
我的工作流程是文档驱动的,所有开发活动都必须围绕以下核心文档展开。**每次执行任务时,我必须按顺序遵循此流程:**
|
||||
|
||||
1. **`[会话开始]` 查阅知识库**: 首先,读取 `project_index.md` 和 `project_codebase.md`,全面了解项目结构和现有代码逻辑。
|
||||
2. **`[任务执行]` 遵循核心文档**: 接着,严格按照 `project_task.md`、`project_doing.md` 和 `task_detail.md` 的规则进行任务规划、执行和记录。
|
||||
3. **`[会话结束]` 更新知识库**: 最后,在任务完成后,必须回顾本次修改的文件,并同步更新 `project_index.md` 和 `project_codebase.md` 中的相关说明。
|
||||
|
||||
---
|
||||
|
||||
#### **1. 项目任务规划文档 (`project_task.md`)**
|
||||
|
||||
- **作用**: 项目的任务清单和进度跟踪器。
|
||||
- **执行规则**:
|
||||
- **初始化**: 如果项目目录中不存在此文件,我必须首先创建它,并根据需求拆分出顶层任务。
|
||||
- **任务读取**: 每次执行任务前,我必须首先读取此文件,了解当前项目的整体进度。
|
||||
- **任务状态**: 每个任务必须有明确的状态标记:`[未开始]`、`[进行中]`、`[已完成]`、`[阻塞]`、`[已删除]`。
|
||||
- **任务选择**: 优先处理 `[进行中]` 的任务。如果没有,则从 `[未开始]` 的任务中选择一个开始。
|
||||
- **任务拆分**: 如果一个任务过于庞大,无法在一次交互中完成,我必须主动将其拆分为多个更小的、可执行的子任务,并更新到此文档中。
|
||||
- **状态更新**: 开始一个任务时,将其状态更新为 `[进行中]`。完成一个任务后,将其状态更新为 `[已完成]`。
|
||||
- **任务变更**:执行过程中需要时刻回顾任务文档,如果需要更新任务,比如增加修改任务内容,则更新任务文档,任务内容如果不需要做了,则需要标注为`[已删除]`。
|
||||
|
||||
#### **2. 项目过程记录文档 (`project_doing.md`)**
|
||||
|
||||
- **作用**: 详细记录每次代码修改的“前因后果”,用于代码审查和问题追溯。
|
||||
- **执行规则**:
|
||||
- **修改前记录**: 在修改任何代码文件前,我必须在此文件中追加一条新的记录,包含:
|
||||
- **时间戳**: 当前时间。
|
||||
- **关联任务**: 所属的任务名称或ID(来自 `project_task.md`)。
|
||||
- **操作目标**: 明确说明“我准备做什么事”。
|
||||
- **影响范围**: 列出将要修改的文件路径。
|
||||
- **修改后记录**: 在完成代码修改后,我必须在同一条记录中追加“修改结果”部分,包含:
|
||||
- **改动摘要**: 概括性地描述改了哪些内容。
|
||||
- **代码片段**: 可以附上关键的代码变更片段(可选)。
|
||||
|
||||
#### **3. 任务执行摘要 (`task_detail.md`)**
|
||||
|
||||
- **作用**: 对每次与你交互(即每次执行任务)的宏观总结。
|
||||
- **执行规则**:
|
||||
- 每次与你交互(即每次执行任务)后,我必须生成或更新此文件。
|
||||
- 每次交互都应作为一个独立的条目,包含:
|
||||
- **会话ID/序号**: 用于区分不同的交互。
|
||||
- **执行原因**: 本次交互的起因是什么?(例如:用户要求新增登录功能)
|
||||
- **执行过程**: 我做了哪些关键工作?(例如:1. 分析需求,拆分任务。 2. 设计 User 表结构。 3. 编写注册 API。)
|
||||
- **执行结果**: 最终产出了什么?当前项目状态如何?(例如:完成了用户注册后端 API,项目进度更新至 30%。)
|
||||
|
||||
#### **4. 项目知识库文档**
|
||||
|
||||
- **作用**: 维护项目的静态结构和动态代码逻辑的知识库,确保信息的即时性和准确性。
|
||||
|
||||
##### **4.1 项目文件索引 (`project_index.md`)**
|
||||
|
||||
- **作用**: 项目文件索引与说明,提供整个项目库的宏观视图。
|
||||
- **执行规则**:
|
||||
- **初始化**: 如果项目目录中不存在此文件,我必须首先创建它,并初始化一个基本结构(例如,按文件夹分类)。
|
||||
- **会话前查看**: 每次执行任务前,我必须读取此文件,以快速了解项目全貌和文件布局。
|
||||
- **会话后更新**: 每次会话结束后,如果创建了新文件或修改了现有文件的作用,我必须更新此文件中对应的条目,确保其描述与文件实际作用一致。
|
||||
|
||||
##### **4.2 代码库函数概览 (`project_codebase.md`)**
|
||||
|
||||
- **作用**: 代码库函数与模块概览,深入记录代码实现的细节。
|
||||
- **执行规则**:
|
||||
- **初始化**: 如果项目目录中不存在此文件,我必须首先创建它。
|
||||
- **会话前查看**: 每次执行任务前,我必须读取此文件,以避免重复造轮子,并理解现有代码逻辑。
|
||||
- **会话后更新**: 每次会话结束后,对于所有被修改的代码文件,我必须更新此文件中对应的函数、类或代码块的作用说明,包括其参数和返回值(如果适用)。
|
||||
|
||||
---
|
||||
|
||||
### **技术栈与规范**
|
||||
|
||||
#### **前端技术栈**
|
||||
|
||||
- **样式**: Tailwind CSS (通过 CDN 或项目依赖引入)
|
||||
- **图标**: **仅限** Lucide React。
|
||||
- **动画**: 遵循"有意义的动效"原则,使用 CSS Transitions。**禁止**引入 Framer Motion。
|
||||
- **状态管理**: (根据项目复杂度选择,如 Zustand, Redux Toolkit)
|
||||
- **依赖管理**: 保持最小化的依赖。
|
||||
- **包管理器**: 使用 pnpm 进行依赖管理。
|
||||
|
||||
### **产品哲学与执行准则**
|
||||
|
||||
我将严格遵循以下融合了现代设计思想和工程实践的准则来构建整个产品。
|
||||
|
||||
#### **前端/UI/UX 设计准则**
|
||||
|
||||
1. **内容为王,清晰第一**: UI 元素采用柔和、半透明或极简设计,优先保证排版的可读性。
|
||||
2. **空间层次与视觉呼吸**: 善用"留白"组织内容,通过微妙的阴影、边框和分层构建视觉深度。
|
||||
3. **一致且可预测的体验**: 相同功能的组件必须拥有统一的视觉表现和交互行为。
|
||||
4. **有意义的动效与即时反馈**: 动画仅用于指示状态变化。所有可交互元素都必须提供即时、符合情境的视觉反馈。
|
||||
5. **功能驱动的极简主义**: 每个视觉元素的存在都必须服务于一个明确的功能目的。
|
||||
6. **无障碍设计优先**: 确保足够的色彩对比度、键盘导航支持。默认支持"亮色与暗色"两种主题模式。
|
||||
7. **视觉风格**: 采用 **Bento Grid** 风格。强调**超大字体或数字**突出核心要点。可中英文混用,中文大字体粗体,英文小字体点缀。
|
||||
8. **内容视角**: 网页内容需以第一方的视角进行叙述。
|
||||
|
||||
#### **后端/系统架构准则**
|
||||
|
||||
1. **API 设计优先**: 遵循 RESTful 设计原则,使用清晰的资源名词和 HTTP 动词。API 响应体结构必须统一(如 `{ data, message, code }`)。
|
||||
2. **数据模型即核心**: 使用 Prisma 进行数据建模,确保数据库设计的规范性、一致性和可扩展性。
|
||||
3. **安全是基础**: 对所有输入进行严格验证和清理。敏感信息(如密码)必须哈希存储。防范常见 Web 攻击(SQL注入, XSS等)。
|
||||
4. **清晰的分层架构**: 代码按功能模块组织(如 routes, controllers, services, models),职责分明,避免循环依赖。
|
||||
5. **统一的错误处理**: 建立全局错误处理中间件,捕获所有异常,并返回格式化、用户友好的错误信息。同时记录详细的错误日志。
|
||||
6. **代码质量与可读性**: 编写有意义的函数和变量名。添加必要的注释。遵循 SOLID 原则。
|
||||
7. **可扩展性与性能**: 对于耗时操作(如发送邮件、数据处理)使用异步任务队列。合理使用缓存策略。
|
||||
|
||||
---
|
||||
|
||||
### **高级极简网站设计的执行标准**
|
||||
|
||||
_(此部分为前端设计的细化标准,保持不变)_
|
||||
|
||||
#### **色彩与层级**
|
||||
|
||||
1. **建立灰度色阶**: 必须定义一个包含至少5个层级的灰度色板。
|
||||
2. **限制颜色总数**: 总共必须使用 **3 - 5 种颜色**。结构为:1 种主品牌色 + 2 - 3 种中性色 + 1 - 2 种强调色。
|
||||
3. **语义化警告色**: 将唯一的亮色(如红色或橙色)**严格定义为** "危险/破坏性操作色"。
|
||||
4. **渐变规则**: **完全避免使用渐变**,使用纯色。
|
||||
5. **对比度强制**: 所有文本与背景的对比度必须符合 WCAG 2.1 AA 级标准。
|
||||
|
||||
#### **形状与一致性**
|
||||
|
||||
1. **定义圆角系统**: 必须建立一套层级化的圆角变量(胶囊、大、中、小)。
|
||||
2. **间距规则化**: 必须使用基于4或8的倍数的间距系统。**必须使用 `gap` 类进行间距设置,禁止使用 `space-*` 类**。
|
||||
|
||||
#### **字体排版**
|
||||
|
||||
1. **限制字体家族**: 总共必须限制最多使用 **2 个字体系列**。
|
||||
2. **字体排版实现**: 正文行高使用 `leading-relaxed` 或 `leading-6`。将标题用 `text-balance` 或 `text-pretty` 包裹。
|
||||
|
||||
#### **布局结构**
|
||||
|
||||
1. **移动端优先**: 必须优先进行移动端设计,然后针对大屏幕进行增强。
|
||||
2. **布局方法优先级**: 1. Flexbox。 2. CSS Grid。 3. 绝不使用浮动或绝对定位(除非绝对必要)。
|
||||
|
||||
#### **交互与可用性**
|
||||
|
||||
1. **一级操作必须显性化**: 任何时刻,页面的主要操作按钮都必须拥有填充背景和高对比度文本。
|
||||
2. **隐藏容器仅限次级操作**: "悬停显示容器/阴影"的设计只能用于次级或三级操作。
|
||||
3. **为无悬停而设计**: **禁止**设计任何依赖 hover 才能揭示核心功能的用户流程。
|
||||
4. **焦点状态强制高亮**: 必须为所有可交互元素设计一个高可见度的键盘焦点状态 (`focus-visible`)。
|
||||
5. **表格细节**: 表格 `table` 内的短文字不要产生换行。
|
||||
|
||||
#### **最终检验**
|
||||
|
||||
1. **"0.5秒原则"**: 在做每一个简化决策时,必须问自己:"如果去掉这个边框/背景,一个新用户还能在 0.5 秒内识别出这是一个可点击的元素吗?"
|
||||
2. **内容完整性**: 不要省略内容要点。
|
||||
|
||||
---
|
||||
|
||||
### **执行标准速查表**
|
||||
|
||||
| 类别 | 标准 | 关键动作 |
|
||||
| ------------ | ---------------------------- | --------------------------------------------------------------------------- |
|
||||
| **项目管理** | 1. 文档驱动 | 严格遵守 `project_task.md`, `project_doing.md`, `task_detail.md` 的工作流程 |
|
||||
| | 18. 知识库同步 | 会话前查阅 `project_index.md` 和 `project_codebase.md`,会话后更新它们 |
|
||||
| **色彩** | 2. 灰度色阶 | 定义5+级灰色,用于区分层级 |
|
||||
| | 3. 限制颜色总数 | 总共3-5种颜色(主色+中性色+强调色) |
|
||||
| | 4. 语义化警告色 | 亮色仅用于"危险"操作 |
|
||||
| **形状** | 5. 圆角系统 | 为不同组件定义固定的圆角值(胶囊、大、中、小) |
|
||||
| | 6. 间距规则化 | 遵循4/8倍数原则,用 `gap`,禁用 `space-*` |
|
||||
| **字体** | 7. 限制字体家族 | 最多2个无衬线字体系列(标题/正文) |
|
||||
| | 8. 字体排版实现 | 行高1.4-1.6,正文>=14px,使用 `text-balance` |
|
||||
| **布局** | 9. 移动端优先 & Flexbox/Grid | 移动优先,Flexbox为主,Grid为辅 |
|
||||
| **交互** | 10. 一级操作显性化 | 主按钮必须有填充背景,永久可见 |
|
||||
| | 11. 隐藏容器仅限次级 | 仅对次要操作应用"悬停显示"效果 |
|
||||
| | 12. 为无悬停而设计 | 确保移动端所有功能无需悬停即可发现 |
|
||||
| **可用性** | 13. 焦点状态强制高亮 | 为键盘用户提供清晰的 `outline` |
|
||||
| **架构** | 14. API 设计优先 | 遵循 RESTful,统一响应格式 |
|
||||
| | 15. 安全是基础 | 输入验证、JWT认证、密码哈希 |
|
||||
| **内容** | 16. 第一人称视角 | 避免自夸式文案 |
|
||||
| **最终检验** | 17. "0.5秒原则" | 功能可识别性 > 视觉简洁性 |
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"antfu.iconify",
|
||||
"antfu.unocss",
|
||||
"antfu.vite",
|
||||
"antfu.goto-alias",
|
||||
"csstools.postcss",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"vue.volar",
|
||||
"lokalise.i18n-ally",
|
||||
"streetsidesoftware.code-spell-checker"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"cSpell.words": ["Vitesse", "Vite", "unocss", "vitest", "vueuse", "pinia", "demi", "antfu", "iconify", "intlify", "vitejs", "unplugin", "pnpm"],
|
||||
"i18n-ally.sourceLanguage": "zh-CN",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.localesPaths": "locales",
|
||||
"i18n-ally.sortKeys": true,
|
||||
|
||||
// Vue 特定设置
|
||||
"vue.server.initialization": {
|
||||
"typescript": {
|
||||
"enable": true
|
||||
}
|
||||
},
|
||||
"vue.languageFeatures.codeActions": true,
|
||||
"vue.languageFeatures.completion": true,
|
||||
"vue.languageFeatures.diagnostics": true,
|
||||
"vue.languageFeatures.hover": true,
|
||||
"vue.languageFeatures.rename": true,
|
||||
"vue.languageFeatures.signatureHelp": true,
|
||||
"vue.languageFeatures.suggestions": true,
|
||||
"vue.scaffoldSnippetSources": {
|
||||
"workspace": "💼",
|
||||
"user": "👤",
|
||||
"vetur": "📦"
|
||||
},
|
||||
"emmet.includeLanguages": {
|
||||
"vue-html": "html",
|
||||
"vue": "html"
|
||||
},
|
||||
|
||||
// JavaScript 提示
|
||||
"javascript.suggest.autoImports": true,
|
||||
"javascript.suggest.includeAutomaticOptionalChainCompletions": true,
|
||||
"javascript.suggest.includeCompletionsForImportStatements": true,
|
||||
"javascript.suggest.includePackageJsonAutoImports": "auto",
|
||||
"typescript.suggest.autoImports": true,
|
||||
|
||||
"editor.quickSuggestions": {
|
||||
"other": true,
|
||||
"comments": false,
|
||||
"strings": true
|
||||
},
|
||||
"eslint.enable": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
},
|
||||
"eslint.codeActionsOnSave.mode": "problems",
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact",
|
||||
"vue",
|
||||
"html",
|
||||
"markdown",
|
||||
"json",
|
||||
"jsonc",
|
||||
"yaml"
|
||||
],
|
||||
"cSpell.enabledFileTypes": {
|
||||
"vue": false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
FROM node:20-alpine AS build-stage
|
||||
|
||||
WORKDIR /app
|
||||
RUN corepack enable
|
||||
|
||||
COPY .npmrc package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.pnpm-store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:stable-alpine AS production-stage
|
||||
|
||||
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020-PRESENT Weak ZHou
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
<p align='center'>
|
||||
<img src='https://user-images.githubusercontent.com/11247099/154486817-f86b8f20-5463-4122-b6e9-930622e757f2.png' alt='Vitesse - Opinionated Vite Starter Template' width='600'/>
|
||||
</p>
|
||||
|
||||
<p align='center'>
|
||||
快速地<sup><em>Vitesse</em></sup> 创建 Web 应用
|
||||
<br>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
<p align='center'>
|
||||
<a href="https://vitesse.netlify.app/">在线 Demo</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
> **Note**: 本模板创建于 Vue 3 和 Vite 的早期过渡时期。目前,如果您正在寻求更好的 Vue 开发体验和更持续的维护,我们建议您使用 [Nuxt 3](https://nuxt.com) 来代替(它也可以根据需要使用 SPA 或 SSG)。本模板仍会作为参考缓慢地维护下去,但将不会有太多的更新。
|
||||
|
||||
<br>
|
||||
|
||||
## 特性
|
||||
|
||||
- ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite](https://github.com/vitejs/vite), [pnpm](https://pnpm.io/), [esbuild](https://github.com/evanw/esbuild) - 就是快!
|
||||
|
||||
- 🗂 [基于文件的路由](./src/pages)
|
||||
|
||||
- 📦 [组件自动化加载](./src/components)
|
||||
|
||||
- 🍍 [使用 Pinia 的状态管理](https://pinia.vuejs.org)
|
||||
|
||||
- 📑 [布局系统](./src/layouts)
|
||||
|
||||
- 📲 [PWA](https://github.com/antfu/vite-plugin-pwa)
|
||||
|
||||
- 🎨 [UnoCSS](https://github.com/unocss/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
|
||||
|
||||
- 😃 [各种图标集为你所用](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
|
||||
|
||||
- 🌍 [I18n 国际化开箱即用](./locales)
|
||||
|
||||
- 🗒 [Markdown 支持](https://github.com/unplugin/unplugin-vue-markdown)
|
||||
|
||||
- 🔥 使用 [新的 `<script setup>` 语法](https://github.com/vuejs/rfcs/pull/227)
|
||||
|
||||
- 📥 [API 自动加载](https://github.com/unplugin/unplugin-auto-import) - 直接使用 Composition API 无需引入
|
||||
|
||||
- 🖨 使用 [vite-ssg](https://github.com/antfu/vite-ssg) 进行服务端生成 (SSG)
|
||||
|
||||
- 🦔 使用 [beasties](https://github.com/danielroe/beasties) 的生成关键 CSS
|
||||
|
||||
- 🦾 TypeScript, 当然
|
||||
|
||||
- ⚙️ 结合 [GitHub Actions](https://github.com/features/actions),使用 [Vitest](https://github.com/vitest-dev/vitest) 进行单元测试, [Cypress](https://cypress.io/) 进行 E2E 测试
|
||||
|
||||
- ☁️ 零配置部署 Netlify
|
||||
|
||||
<br>
|
||||
|
||||
## 预配置
|
||||
|
||||
### UI 框架
|
||||
|
||||
- [UnoCSS](https://github.com/antfu/unocss) - 高性能且极具灵活性的即时原子化 CSS 引擎
|
||||
|
||||
### Icons
|
||||
|
||||
- [Iconify](https://iconify.design) - 使用任意的图标集,浏览:[🔍Icônes](https://icones.netlify.app/)
|
||||
- [UnoCSS 的纯 CSS 图标方案](https://github.com/antfu/unocss/tree/main/packages/preset-icons)
|
||||
|
||||
### 插件
|
||||
|
||||
- [Vue Router](https://github.com/vuejs/router)
|
||||
- [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) - 以文件系统为基础的路由
|
||||
- [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) - 页面布局系统
|
||||
- [Pinia](https://pinia.vuejs.org) - 直接的, 类型安全的, 使用 Composition API 的轻便灵活的 Vue 状态管理
|
||||
- [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components) - 自动加载组件
|
||||
- [`unplugin-auto-import`](https://github.com/antfu/unplugin-auto-import) - 直接使用 Composition API 等,无需导入
|
||||
- [`vite-plugin-pwa`](https://github.com/antfu/vite-plugin-pwa) - PWA
|
||||
- [`unplugin-vue-markdown`](https://github.com/unplugin/unplugin-vue-markdown) - Markdown 作为组件,也可以让组件在 Markdown 中使用
|
||||
- [`markdown-it-prism`](https://github.com/jGleitz/markdown-it-prism) - [Prism](https://prismjs.com/) 的语法高亮
|
||||
- [`prism-theme-vars`](https://github.com/antfu/prism-theme-vars) - 利用 CSS 变量自定义 Prism.js 的主题
|
||||
- [Vue I18n](https://github.com/intlify/vue-i18n-next) - 国际化
|
||||
- [`unplugin-vue-i18n`](https://github.com/intlify/bundle-tools/tree/main/packages/unplugin-vue-i18n) - Vue I18n 的 Vite 插件
|
||||
- [VueUse](https://github.com/antfu/vueuse) - 实用的 Composition API 工具合集
|
||||
- [`vite-ssg-sitemap`](https://github.com/jbaubree/vite-ssg-sitemap) - 站点地图生成器
|
||||
- [`@vueuse/head`](https://github.com/vueuse/head) - 响应式地操作文档头信息
|
||||
- [`vite-plugin-vue-devtools`](https://github.com/webfansplz/vite-plugin-vue-devtools) - 旨在增强Vue开发者体验的Vite插件
|
||||
|
||||
### 编码风格
|
||||
|
||||
- 使用 Composition API 地 [`<script setup>` SFC 语法](https://github.com/vuejs/rfcs/pull/227)
|
||||
- [ESLint](https://eslint.org/) 配置为 [@antfu/eslint-config](https://github.com/antfu/eslint-config), 单引号, 无分号.
|
||||
|
||||
### 开发工具
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
- [Vitest](https://github.com/vitest-dev/vitest) - 基于 Vite 的单元测试框架
|
||||
- [Cypress](https://cypress.io/) - E2E 测试
|
||||
- [pnpm](https://pnpm.js.org/) - 快, 节省磁盘空间的包管理器
|
||||
- [`vite-ssg`](https://github.com/antfu/vite-ssg) - 服务端生成
|
||||
- [beasties](https://github.com/danielroe/beasties) - 关键 CSS 生成器
|
||||
- [Netlify](https://www.netlify.com/) - 零配置的部署
|
||||
- [VS Code 扩展](./.vscode/extensions.json)
|
||||
- [Vite](https://marketplace.visualstudio.com/items?itemName=antfu.vite) - 自动启动 Vite 服务器
|
||||
- [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) - Vue 3 `<script setup>` IDE 支持
|
||||
- [Iconify IntelliSense](https://marketplace.visualstudio.com/items?itemName=antfu.iconify) - 图标内联显示和自动补全
|
||||
- [i18n Ally](https://marketplace.visualstudio.com/items?itemName=lokalise.i18n-ally) - 多合一的 I18n 支持
|
||||
- [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)
|
||||
|
||||
## 衍生项目
|
||||
|
||||
由于这个模板的业务场景非常的局限,下面提供了一个精心策划的列表,列出了社区维护的具有不同偏好和功能集的衍生项目。也可以看看他们。当然也欢迎你 PR 提供自己的项目!
|
||||
|
||||
###### 官方
|
||||
|
||||
- [vitesse-lite](https://github.com/antfu/vitesse-lite) - Vitesse 的轻量版本
|
||||
- [vitesse-nuxt3](https://github.com/antfu/vitesse-nuxt3) - Vitesse 的 Nuxt 3 版本
|
||||
- [vitesse-nuxt-bridge](https://github.com/antfu/vitesse-nuxt-bridge) - Vitesse 的 Nuxt2 桥接版本
|
||||
- [vitesse-webext](https://github.com/antfu/vitesse-webext) - 开箱即用的浏览器扩展 vite 模板
|
||||
|
||||
###### 社区
|
||||
|
||||
[查看英文版](./README.md#community)
|
||||
|
||||
## 现在可以试试!
|
||||
|
||||
> Vitesse 需要 Node 版本 >=14.18
|
||||
|
||||
### GitHub 模板
|
||||
|
||||
[使用这个模板创建仓库](https://github.com/antfu-collective/vitesse/generate).
|
||||
|
||||
### 克隆到本地
|
||||
|
||||
如果您更喜欢使用更干净的 git 历史记录手动执行此操作
|
||||
|
||||
```bash
|
||||
npx degit antfu-collective/vitesse my-vitesse-app
|
||||
cd my-vitesse-app
|
||||
pnpm i # 如果你没装过 pnpm, 可以先运行: npm install -g pnpm
|
||||
```
|
||||
|
||||
## 清单
|
||||
|
||||
使用此模板时,请尝试按照清单正确更新您自己的信息
|
||||
|
||||
- [ ] 在 `LICENSE` 中改变作者名
|
||||
- [ ] 在 `App.vue` 中改变标题
|
||||
- [ ] 在 `vite.config.ts` 更改主机名
|
||||
- [ ] 在 `public` 目录下改变favicon
|
||||
- [ ] 移除 `.github` 文件夹中包含资助的信息
|
||||
- [ ] 整理 README 并删除路由
|
||||
|
||||
紧接着, 享受吧 :)
|
||||
|
||||
## 使用
|
||||
|
||||
### 开发
|
||||
|
||||
只需要执行以下命令就可以在 http://localhost:3333 中看到
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 构建
|
||||
|
||||
构建该应用只需要执行以下命令
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
然后你会看到用于发布的 `dist` 文件夹被生成。
|
||||
|
||||
### 部署到 Netlify
|
||||
|
||||
前往 [Netlify](https://app.netlify.com/start) 并选择你的仓库, 一路 `OK` 下去,稍等一下后,你的应用将被创建.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
import vitePreprocessor from 'cypress-vite'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
baseUrl: 'http://localhost:3333',
|
||||
chromeWebSecurity: false,
|
||||
specPattern: 'cypress/e2e/**/*.spec.*',
|
||||
supportFile: false,
|
||||
setupNodeEvents(on) {
|
||||
on('file:preprocessor', vitePreprocessor())
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
context('Basic', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
})
|
||||
|
||||
it('basic nav', () => {
|
||||
cy.url()
|
||||
.should('eq', 'http://localhost:3333/')
|
||||
|
||||
cy.contains('[Home Layout]')
|
||||
.should('exist')
|
||||
|
||||
cy.get('#input')
|
||||
.type('Vitesse{Enter}')
|
||||
.url()
|
||||
.should('eq', 'http://localhost:3333/hi/Vitesse')
|
||||
|
||||
cy.contains('[Default Layout]')
|
||||
.should('exist')
|
||||
|
||||
cy.get('[btn]')
|
||||
.click()
|
||||
.url()
|
||||
.should('eq', 'http://localhost:3333/')
|
||||
})
|
||||
|
||||
it('markdown', () => {
|
||||
cy.get('[data-test-id="about"]')
|
||||
.click()
|
||||
.url()
|
||||
.should('eq', 'http://localhost:3333/about')
|
||||
|
||||
cy.get('.shiki')
|
||||
.should('exist')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"cypress"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
// @ts-check
|
||||
// import antfu from '@antfu/eslint-config'
|
||||
// eslint.config.js
|
||||
import js from '@eslint/js'
|
||||
import imports from 'eslint-plugin-import'
|
||||
import perfectionist from 'eslint-plugin-perfectionist'
|
||||
import promise from 'eslint-plugin-promise'
|
||||
import unicorn from 'eslint-plugin-unicorn'
|
||||
import unusedImports from 'eslint-plugin-unused-imports'
|
||||
|
||||
export default [
|
||||
// 1. 官方推荐的核心规则(必须最先)
|
||||
js.configs.recommended,
|
||||
|
||||
// 2. unicorn(包含大量 recommended 规则,建议第二位)
|
||||
unicorn.configs['recommended'],
|
||||
|
||||
// 3. import 插件(路径检查、重复导入等,要在 perfectionist 之前)
|
||||
{
|
||||
plugins: {
|
||||
import: imports,
|
||||
},
|
||||
rules: {
|
||||
...imports.configs.recommended.rules,
|
||||
// 关闭与 perfectionist 冲突的排序规则(perfectionist 更强)
|
||||
'import/order': 'off',
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
node: { extensions: ['.js', '.mjs', '.cjs'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 4. promise 插件(规则很少,放中间就行)
|
||||
promise.configs['flat/recommended'],
|
||||
|
||||
// 5. perfectionist(排序神器,必须在 import 之后!)
|
||||
{
|
||||
plugins: {
|
||||
perfectionist,
|
||||
},
|
||||
rules: {
|
||||
'perfectionist/sort-imports': [
|
||||
'warn',
|
||||
{
|
||||
type: 'natural', // 排序算法
|
||||
order: 'asc', // 排序方向
|
||||
// 分组顺序: Node内置➡ 第三方依赖➡ 项目内部别名➡ 父级目录➡ 同级目录➡ 当前目录index➡ 对象导入➡ 类型导入➡ 无法识别导入
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type', 'unknown'],
|
||||
},
|
||||
],
|
||||
|
||||
'perfectionist/sort-exports': 'warn', // 导出排序 export { apple, banana, zebra }
|
||||
'perfectionist/sort-named-exports': 'warn',// 导出排序 export { a, m, z } from './module'
|
||||
},
|
||||
},
|
||||
|
||||
// 6. unused-imports(必须放最后!因为它依赖前面的解析结果来判断“是否真的没用”)
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
},
|
||||
rules: {
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
// 可选:连未使用的变量也一起管(比内置 no-unused-vars 更快更准)
|
||||
'unused-imports/no-unused-vars': 'error',
|
||||
'no-unused-vars': 'off', // 关闭内置的,防止重复报错
|
||||
},
|
||||
},
|
||||
|
||||
// 7. 可选:一些个人常用微调(可以全部复制)
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
// Node.js 全局变量
|
||||
console: 'readonly',
|
||||
process: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
module: 'readonly',
|
||||
require: 'readonly',
|
||||
exports: 'readonly',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
// unicorn 里个别对纯后端太严格的规则可以关掉
|
||||
'unicorn/filename-case': 'off',
|
||||
|
||||
// 后端常见放宽
|
||||
'no-console': 'off', // 方便调试
|
||||
'no-underscore-dangle': 'off', // 允许下划线命名 如 __dirname
|
||||
},
|
||||
},
|
||||
]
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="apple-touch-icon" href="/pwa-192x192.png" />
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00aba9" />
|
||||
<meta name="msapplication-TileColor" content="#00aba9" />
|
||||
<script>
|
||||
;(function () {
|
||||
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
|
||||
if (setting === 'dark' || (prefersDark && setting !== 'light'))
|
||||
document.documentElement.classList.toggle('dark', true)
|
||||
})()
|
||||
</script>
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
<noscript> This website requires JavaScript to function properly. Please enable JavaScript to continue. </noscript>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES6",
|
||||
"jsx": "preserve",
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true
|
||||
},
|
||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.vue"],
|
||||
"exclude": ["node_modules", "**/node_modules/*", "dist"]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
## i18n
|
||||
|
||||
This directory is to serve your locale translation files. YAML under this folder would be loaded automatically and register with their filenames as locale code.
|
||||
|
||||
Check out [`vue-i18n`](https://github.com/intlify/vue-i18n-next) for more details.
|
||||
|
||||
If you are using VS Code, [`i18n Ally`](https://github.com/lokalise/i18n-ally) is recommended to make the i18n experience better.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: حول
|
||||
back: رجوع
|
||||
go: تجربة
|
||||
home: الرئيسية
|
||||
toggle_dark: التغيير إلى الوضع المظلم
|
||||
toggle_langs: تغيير اللغة
|
||||
intro:
|
||||
desc: vite مثال لتطبيق
|
||||
dynamic-route: عرض لتوجيهات ديناميكية
|
||||
hi: مرحبا {name}
|
||||
aka: معروف أيضا تحت مسمى
|
||||
whats-your-name: ما إسمك؟
|
||||
not-found: صفحة غير موجودة
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Über
|
||||
back: Zurück
|
||||
go: Los
|
||||
home: Startseite
|
||||
toggle_dark: Dunkelmodus umschalten
|
||||
toggle_langs: Sprachen ändern
|
||||
intro:
|
||||
desc: Vite Startvorlage mit Vorlieben
|
||||
dynamic-route: Demo einer dynamischen Route
|
||||
hi: Hi, {name}!
|
||||
aka: Auch bekannt als
|
||||
whats-your-name: Wie heißt du?
|
||||
not-found: Nicht gefunden
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: About
|
||||
back: Back
|
||||
go: GO
|
||||
home: Home
|
||||
toggle_dark: Toggle dark mode
|
||||
toggle_langs: Change languages
|
||||
intro:
|
||||
desc: Opinionated Vite Starter Template
|
||||
dynamic-route: Demo of dynamic route
|
||||
hi: Hi, {name}!
|
||||
aka: Also known as
|
||||
whats-your-name: What's your name?
|
||||
not-found: Not found
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Acerca de
|
||||
back: Atrás
|
||||
go: Ir
|
||||
home: Inicio
|
||||
toggle_dark: Alternar modo oscuro
|
||||
toggle_langs: Cambiar idiomas
|
||||
intro:
|
||||
desc: Plantilla de Inicio de Vite Dogmática
|
||||
dynamic-route: Demo de ruta dinámica
|
||||
hi: ¡Hola, {name}!
|
||||
aka: También conocido como
|
||||
whats-your-name: ¿Cómo te llamas?
|
||||
not-found: No se ha encontrado
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: À propos
|
||||
back: Retour
|
||||
go: Essayer
|
||||
home: Accueil
|
||||
toggle_dark: Basculer en mode sombre
|
||||
toggle_langs: Changer de langue
|
||||
intro:
|
||||
desc: Exemple d'application Vite
|
||||
dynamic-route: Démo de route dynamique
|
||||
hi: Salut, {name}!
|
||||
aka: Aussi connu sous le nom de
|
||||
whats-your-name: Comment t'appelles-tu ?
|
||||
not-found: Page non trouvée
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Tentang
|
||||
back: Kembali
|
||||
go: Pergi
|
||||
home: Beranda
|
||||
toggle_dark: Ubah ke mode gelap
|
||||
toggle_langs: Ubah bahasa
|
||||
intro:
|
||||
desc: Template awal vite
|
||||
dynamic-route: Contoh rute dinamik
|
||||
hi: Halo, {name}!
|
||||
aka: Juga diketahui sebagai
|
||||
whats-your-name: Siapa nama anda?
|
||||
not-found: Tidak ditemukan
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: Su di me
|
||||
back: Indietro
|
||||
go: Vai
|
||||
home: Home
|
||||
toggle_dark: Attiva/disattiva modalità scura
|
||||
toggle_langs: Cambia lingua
|
||||
intro:
|
||||
desc: Modello per una Applicazione Vite
|
||||
dynamic-route: Demo di rotta dinamica
|
||||
hi: Ciao, {name}!
|
||||
whats-your-name: Come ti chiami?
|
||||
not-found: Non trovato
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: これは?
|
||||
back: 戻る
|
||||
go: 進む
|
||||
home: ホーム
|
||||
toggle_dark: ダークモード切り替え
|
||||
toggle_langs: 言語切り替え
|
||||
intro:
|
||||
desc: 固執された Vite スターターテンプレート
|
||||
dynamic-route: 動的ルートのデモ
|
||||
hi: こんにちは、{name}!
|
||||
whats-your-name: 君の名は。
|
||||
not-found: 見つかりませんでした
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: შესახებ
|
||||
back: უკან
|
||||
go: დაწყება
|
||||
home: მთავარი
|
||||
toggle_dark: გადართე მუქი რეჟიმი
|
||||
toggle_langs: ენის შეცვლა
|
||||
intro:
|
||||
desc: Opinionated Vite Starter Template
|
||||
dynamic-route: დინამიური როუტინგის დემო
|
||||
hi: გამარჯობა, {name}!
|
||||
aka: ასევე ცნობილი როგორც
|
||||
whats-your-name: რა გქვია?
|
||||
not-found: ვერ მოიძებნა
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: 소개
|
||||
back: 뒤로가기
|
||||
go: 이동
|
||||
home: 홈
|
||||
toggle_dark: 다크모드 토글
|
||||
toggle_langs: 언어 변경
|
||||
intro:
|
||||
desc: Vite 애플리케이션 템플릿
|
||||
dynamic-route: 다이나믹 라우트 데모
|
||||
hi: 안녕, {name}!
|
||||
whats-your-name: 이름이 뭐예요?
|
||||
not-found: 찾을 수 없습니다
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: O nas
|
||||
back: Wróć
|
||||
go: WEJDŹ
|
||||
home: Strona główna
|
||||
toggle_dark: Ustaw tryb nocny
|
||||
toggle_langs: Zmień język
|
||||
intro:
|
||||
desc: Opiniowany szablon startowy Vite
|
||||
dynamic-route: Demonstracja dynamicznego route
|
||||
hi: Cześć, {name}!
|
||||
aka: Znany też jako
|
||||
whats-your-name: Jak masz na imię?
|
||||
not-found: Nie znaleziono
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Sobre
|
||||
back: Voltar
|
||||
go: Ir
|
||||
home: Início
|
||||
toggle_dark: Alternar modo escuro
|
||||
toggle_langs: Mudar de idioma
|
||||
intro:
|
||||
desc: Modelo Opinativo de Partida de Vite
|
||||
dynamic-route: Demonstração de rota dinâmica
|
||||
hi: Olá, {name}!
|
||||
aka: Também conhecido como
|
||||
whats-your-name: Qual é o seu nome?
|
||||
not-found: Não encontrado
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: О шаблоне
|
||||
back: Назад
|
||||
go: Перейти
|
||||
home: Главная
|
||||
toggle_dark: Включить темный режим
|
||||
toggle_langs: Сменить язык
|
||||
intro:
|
||||
desc: Самостоятельный начальный шаблон Vite
|
||||
dynamic-route: Демо динамического маршрута
|
||||
hi: Привет, {name}!
|
||||
whats-your-name: Как тебя зовут?
|
||||
not-found: Не найден
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Hakkımda
|
||||
back: Geri
|
||||
go: İLERİ
|
||||
home: Anasayfa
|
||||
toggle_dark: Karanlık modu değiştir
|
||||
toggle_langs: Dilleri değiştir
|
||||
intro:
|
||||
desc: Görüşlü Vite Başlangıç Şablonu
|
||||
dynamic-route: Dinamik rota demosu
|
||||
hi: Merhaba, {name}!
|
||||
aka: Ayrıca şöyle bilinir
|
||||
whats-your-name: Adınız nedir?
|
||||
not-found: Bulunamadı
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: Про шаблон
|
||||
back: Назад
|
||||
go: Перейти
|
||||
home: Головна
|
||||
toggle_dark: Переключити темний режим
|
||||
toggle_langs: Змінити мову
|
||||
intro:
|
||||
desc: Самостійний початковий шаблон Vite
|
||||
dynamic-route: Демо динамічного маршруту
|
||||
hi: Привіт, {name}!
|
||||
whats-your-name: Як тебе звати?
|
||||
not-found: Не знайдено
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: Haqida
|
||||
back: Orqaga
|
||||
go: Kettik
|
||||
home: Bosh sahifa
|
||||
toggle_dark: Qorong‘i rejimga o‘tish
|
||||
toggle_langs: Tilni o‘zgartirish
|
||||
intro:
|
||||
desc: O‘ylangan boshlang‘ich Vite shabloni
|
||||
dynamic-route: Dynamic route demo'si
|
||||
hi: Assalomu alaykum, {name}!
|
||||
aka: shuningdek
|
||||
whats-your-name: Ismingiz nima?
|
||||
not-found: Topilmadi
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
button:
|
||||
about: Về
|
||||
back: Quay lại
|
||||
go: Đi
|
||||
home: Khởi đầu
|
||||
toggle_dark: Chuyển đổi chế độ tối
|
||||
toggle_langs: Thay đổi ngôn ngữ
|
||||
intro:
|
||||
desc: Ý kiến cá nhân Vite Template để bắt đầu
|
||||
dynamic-route: Bản giới thiệu về dynamic route
|
||||
hi: Hi, {name}!
|
||||
whats-your-name: Tên bạn là gì?
|
||||
not-found: Không tìm thấy
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
button:
|
||||
about: 关于
|
||||
back: 返回
|
||||
go: 确定
|
||||
home: 首页
|
||||
toggle_dark: 切换深色模式
|
||||
toggle_langs: 切换语言
|
||||
intro:
|
||||
desc: 固执己见的 Vite 项目模板
|
||||
dynamic-route: 动态路由演示
|
||||
hi: 你好,{name}
|
||||
aka: 也叫
|
||||
whats-your-name: 输入你的名字
|
||||
not-found: 未找到页面
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[build]
|
||||
publish = "dist"
|
||||
command = "pnpm run build"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "20"
|
||||
|
||||
[[redirects]]
|
||||
from = "/*"
|
||||
to = "/index.html"
|
||||
status = 200
|
||||
|
||||
[[headers]]
|
||||
for = "/manifest.webmanifest"
|
||||
|
||||
[headers.values]
|
||||
Content-Type = "application/manifest+json"
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
{
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.7.0",
|
||||
"scripts": {
|
||||
"build": "vite-ssg build --mode production",
|
||||
"build:dev": "vite-ssg build --mode development",
|
||||
"dev": "vite --port 3333 --open",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"preview-https": "serve dist",
|
||||
"test": "vitest",
|
||||
"test:e2e": "cypress open",
|
||||
"test:unit": "vitest",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"up": "taze major -I",
|
||||
"postinstall": "npx simple-git-hooks",
|
||||
"sizecheck": "npx vite-bundle-visualizer"
|
||||
},
|
||||
"dependencies": {
|
||||
"@unhead/vue": "catalog:frontend",
|
||||
"@unocss/reset": "catalog:frontend",
|
||||
"@vueuse/core": "catalog:frontend",
|
||||
"nprogress": "catalog:frontend",
|
||||
"pinia": "catalog:frontend",
|
||||
"vue": "catalog:frontend",
|
||||
"vue-i18n": "catalog:frontend",
|
||||
"vue-router": "catalog:frontend"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "catalog:dev",
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@iconify-json/carbon": "catalog:dev",
|
||||
"@intlify/unplugin-vue-i18n": "catalog:build",
|
||||
"@shikijs/markdown-it": "catalog:build",
|
||||
"@types/markdown-it-link-attributes": "catalog:types",
|
||||
"@types/nprogress": "catalog:types",
|
||||
"@unocss/eslint-config": "catalog:build",
|
||||
"@vitejs/plugin-vue": "catalog:build",
|
||||
"@vue-macros/volar": "catalog:dev",
|
||||
"@vue/test-utils": "catalog:dev",
|
||||
"autoprefixer": "catalog:",
|
||||
"beasties": "catalog:build",
|
||||
"cypress": "catalog:dev",
|
||||
"cypress-vite": "catalog:dev",
|
||||
"eslint": "catalog:dev",
|
||||
"eslint-plugin-cypress": "catalog:dev",
|
||||
"eslint-plugin-format": "catalog:dev",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-perfectionist": "^5.0.0",
|
||||
"eslint-plugin-promise": "^7.2.1",
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"eslint-plugin-unused-imports": "^4.3.0",
|
||||
"https-localhost": "catalog:dev",
|
||||
"lint-staged": "catalog:dev",
|
||||
"markdown-it-link-attributes": "catalog:build",
|
||||
"postcss": "catalog:",
|
||||
"rollup": "catalog:build",
|
||||
"shiki": "catalog:build",
|
||||
"simple-git-hooks": "catalog:dev",
|
||||
"tailwindcss": "catalog:",
|
||||
"taze": "catalog:dev",
|
||||
"typescript": "catalog:dev",
|
||||
"unocss": "catalog:build",
|
||||
"unplugin-auto-import": "catalog:build",
|
||||
"unplugin-vue-components": "catalog:build",
|
||||
"unplugin-vue-macros": "catalog:build",
|
||||
"unplugin-vue-markdown": "catalog:build",
|
||||
"unplugin-vue-router": "catalog:build",
|
||||
"vite": "catalog:build",
|
||||
"vite-bundle-visualizer": "catalog:build",
|
||||
"vite-plugin-inspect": "catalog:build",
|
||||
"vite-plugin-pwa": "catalog:build",
|
||||
"vite-plugin-vue-devtools": "catalog:build",
|
||||
"vite-plugin-vue-layouts": "catalog:build",
|
||||
"vite-ssg": "catalog:build",
|
||||
"vite-ssg-sitemap": "catalog:build",
|
||||
"vitest": "catalog:dev",
|
||||
"vue-tsc": "catalog:dev"
|
||||
},
|
||||
"resolutions": {
|
||||
"eslint-plugin-unicorn": "^56.0.0",
|
||||
"unplugin": "catalog:build",
|
||||
"vite": "catalog:build",
|
||||
"vite-plugin-inspect": "catalog:build"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,69 @@
|
|||
packages: []
|
||||
|
||||
catalog:
|
||||
autoprefixer: ^10.4.16
|
||||
postcss: ^8.4.32
|
||||
tailwindcss: ^3.4.1
|
||||
catalogs:
|
||||
|
||||
build:
|
||||
'@intlify/unplugin-vue-i18n': ^6.0.5
|
||||
'@shikijs/markdown-it': ^3.2.1
|
||||
'@unocss/eslint-config': ^66.1.0-beta.7
|
||||
'@vitejs/plugin-vue': ^5.2.3
|
||||
beasties: ^0.2.0
|
||||
markdown-it-link-attributes: ^4.0.1
|
||||
rollup: ^4.37.0
|
||||
shiki: ^3.2.1
|
||||
unocss: ^66.1.0-beta.7
|
||||
unplugin: ^2.2.2
|
||||
unplugin-auto-import: ^19.1.2
|
||||
unplugin-vue-components: ^28.4.1
|
||||
unplugin-vue-macros: ^2.14.5
|
||||
unplugin-vue-markdown: ^28.3.1
|
||||
unplugin-vue-router: ^0.12.0
|
||||
vite: ^6.2.3
|
||||
vite-bundle-visualizer: ^1.2.1
|
||||
vite-plugin-inspect: ^11.0.0
|
||||
vite-plugin-pwa: ^0.21.2
|
||||
vite-plugin-vue-devtools: ^7.7.2
|
||||
vite-plugin-vue-layouts: ^0.11.0
|
||||
vite-ssg: ^26.0.0
|
||||
vite-ssg-sitemap: ^0.8.1
|
||||
|
||||
dev:
|
||||
'@antfu/eslint-config': ^4.11.0
|
||||
'@iconify-json/carbon': ^1.2.8
|
||||
'@vue-macros/volar': ^3.0.0-beta.7
|
||||
'@vue/test-utils': ^2.4.6
|
||||
cypress: ^14.2.1
|
||||
cypress-vite: ^1.6.0
|
||||
eslint: ^9.23.0
|
||||
eslint-plugin-cypress: ^4.2.0
|
||||
eslint-plugin-format: ^1.0.1
|
||||
https-localhost: ^4.7.1
|
||||
lint-staged: ^15.5.0
|
||||
simple-git-hooks: ^2.12.1
|
||||
taze: ^19.0.2
|
||||
typescript: ^5.8.2
|
||||
vitest: ^3.0.9
|
||||
vue-tsc: ^2.2.8
|
||||
|
||||
frontend:
|
||||
'@unhead/vue': ^2.0.2
|
||||
'@unocss/reset': ^66.1.0-beta.7
|
||||
'@vueuse/core': ^13.0.0
|
||||
nprogress: ^0.2.0
|
||||
pinia: ^3.0.1
|
||||
vue: ^3.5.13
|
||||
vue-i18n: ^11.1.2
|
||||
vue-router: ^4.5.0
|
||||
|
||||
types:
|
||||
'@types/markdown-it-link-attributes': ^3.0.5
|
||||
'@types/nprogress': ^0.2.3
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- cypress
|
||||
- esbuild
|
||||
- simple-git-hooks
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/assets/*
|
||||
cache-control: max-age=31536000
|
||||
cache-control: immutable
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 311 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.562 26L17.17 8.928l2.366-3.888L17.828 4L16 7.005L14.17 4l-1.708 1.04l2.366 3.888L4.438 26H2v2h28v-2zM16 10.85L25.22 26H17v-8h-2v8H6.78z" fill="#222" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 310 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.11, written by Peter Selinger 2001-2013
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2916 6015 c-93 -57 -173 -108 -178 -113 -6 -6 7 -36 33 -78 23 -38
|
||||
86 -141 139 -229 54 -88 135 -221 180 -295 46 -74 94 -155 108 -180 14 -25 29
|
||||
-52 35 -60 7 -12 -9 -45 -62 -130 -39 -63 -85 -140 -103 -170 -18 -30 -117
|
||||
-194 -222 -365 -104 -170 -199 -326 -210 -346 -12 -19 -61 -102 -111 -183 -49
|
||||
-81 -101 -166 -115 -189 -14 -23 -39 -64 -55 -90 -17 -27 -77 -126 -134 -220
|
||||
-57 -95 -127 -210 -156 -257 -194 -315 -325 -533 -325 -541 0 -5 -4 -9 -10 -9
|
||||
-5 0 -10 -4 -10 -9 0 -5 -55 -98 -121 -207 -247 -404 -403 -660 -416 -684 -8
|
||||
-14 -58 -97 -112 -185 l-98 -160 -189 -2 c-104 -1 -225 -2 -269 -2 l-80 -1 1
|
||||
-210 c0 -116 4 -213 8 -218 11 -11 6107 -9 6114 2 8 13 8 406 0 419 -4 7 -88
|
||||
10 -265 9 l-259 -2 -50 77 c-27 43 -54 87 -60 98 -6 11 -62 103 -124 205 -62
|
||||
102 -120 197 -129 212 -9 16 -85 142 -170 280 -85 139 -160 262 -165 273 -6
|
||||
11 -13 22 -16 25 -3 3 -30 46 -59 95 -30 50 -102 169 -161 265 -59 96 -240
|
||||
393 -402 660 -163 267 -371 609 -463 760 -92 151 -194 318 -225 370 -31 52
|
||||
-101 167 -155 255 l-97 160 27 50 c16 27 32 55 36 61 5 5 38 59 74 120 36 60
|
||||
69 116 74 124 5 8 75 122 155 253 81 131 144 242 141 247 -4 7 -114 76 -183
|
||||
115 -10 6 -52 32 -95 58 -42 27 -81 46 -87 42 -8 -5 -94 -140 -140 -219 -19
|
||||
-33 -221 -365 -246 -404 -15 -22 -18 -18 -111 135 -52 87 -123 203 -157 258
|
||||
-67 108 -67 110 -111 184 -16 28 -34 51 -40 50 -5 0 -86 -47 -179 -104z m739
|
||||
-1642 c319 -526 519 -854 637 -1046 43 -70 78 -130 78 -133 0 -2 5 -10 10 -17
|
||||
6 -7 69 -109 140 -227 72 -118 134 -222 139 -230 5 -8 55 -89 111 -180 56 -91
|
||||
105 -172 110 -180 9 -14 52 -84 270 -445 54 -88 135 -221 180 -295 46 -74 91
|
||||
-148 100 -165 9 -16 31 -53 48 -81 18 -28 32 -54 32 -57 0 -3 -403 -6 -895 -5
|
||||
l-895 0 0 81 c-1 45 -1 439 -1 875 l0 792 -37 1 c-57 1 -344 1 -374 0 l-27 -1
|
||||
0 -832 c0 -458 0 -852 0 -875 l-1 -42 -895 1 c-492 0 -895 3 -895 5 0 9 115
|
||||
198 122 201 5 2 8 7 8 12 0 5 23 46 51 92 28 46 78 128 112 183 33 55 70 116
|
||||
82 135 12 19 132 215 265 435 133 220 266 438 295 485 65 105 206 338 220 362
|
||||
6 10 172 284 370 608 198 325 387 635 420 690 33 55 62 100 65 100 3 0 73
|
||||
-111 155 -247z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
// https://github.com/vueuse/head
|
||||
// you can use this to manipulate the document head in any components,
|
||||
// they will be rendered correctly in the html results with vite-ssg
|
||||
useHead({
|
||||
title: 'Vitesse',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Opinionated Vite Starter Template',
|
||||
},
|
||||
{
|
||||
name: 'theme-color',
|
||||
content: () => isDark.value ? '#00aba9' : '#ffffff',
|
||||
},
|
||||
],
|
||||
link: [
|
||||
{
|
||||
rel: 'icon',
|
||||
type: 'image/svg+xml',
|
||||
href: () => preferredDark.value ? '/favicon-dark.svg' : '/favicon.svg',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,641 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const CancelToken: typeof import('./composables/request')['CancelToken']
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createCancelToken: typeof import('./composables/request')['createCancelToken']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createRef: typeof import('@vueuse/core')['createRef']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const defineLoader: typeof import('vue-router/auto')['defineLoader']
|
||||
const definePage: typeof import('unplugin-vue-router/runtime')['definePage']
|
||||
const del: typeof import('./composables/request')['del']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const get: typeof import('./composables/request')['get']
|
||||
const getActiveHead: typeof import('@unhead/vue')['getActiveHead']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const injectHead: typeof import('@unhead/vue')['injectHead']
|
||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||
const isDark: typeof import('./composables/dark')['isDark']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const post: typeof import('./composables/request')['post']
|
||||
const preferredDark: typeof import('./composables/dark')['preferredDark']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||
const put: typeof import('./composables/request')['put']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const request: typeof import('./composables/request')['default']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const toggleDark: typeof import('./composables/dark')['toggleDark']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCountdown: typeof import('@vueuse/core')['useCountdown']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useHead: typeof import('@unhead/vue')['useHead']
|
||||
const useHeadSafe: typeof import('@unhead/vue')['useHeadSafe']
|
||||
const useI18n: typeof import('vue-i18n')['useI18n']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLink: typeof import('vue-router/auto')['useLink']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSeoMeta: typeof import('@unhead/vue')['useSeoMeta']
|
||||
const useServerHead: typeof import('@unhead/vue')['useServerHead']
|
||||
const useServerHeadSafe: typeof import('@unhead/vue')['useServerHeadSafe']
|
||||
const useServerSeoMeta: typeof import('@unhead/vue')['useServerSeoMeta']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||
const useToString: typeof import('@vueuse/core')['useToString']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useUserStore: typeof import('./stores/user')['useUserStore']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
// @ts-ignore
|
||||
export type { RequestConfig, RequestResponse } from './composables/request'
|
||||
import('./composables/request')
|
||||
}
|
||||
|
||||
// for vue template auto import
|
||||
import { UnwrapRef } from 'vue'
|
||||
declare module 'vue' {
|
||||
interface GlobalComponents {}
|
||||
interface ComponentCustomProperties {
|
||||
readonly CancelToken: UnwrapRef<typeof import('./composables/request')['CancelToken']>
|
||||
readonly EffectScope: UnwrapRef<typeof import('vue')['EffectScope']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
readonly computedInject: UnwrapRef<typeof import('@vueuse/core')['computedInject']>
|
||||
readonly computedWithControl: UnwrapRef<typeof import('@vueuse/core')['computedWithControl']>
|
||||
readonly controlledComputed: UnwrapRef<typeof import('@vueuse/core')['controlledComputed']>
|
||||
readonly controlledRef: UnwrapRef<typeof import('@vueuse/core')['controlledRef']>
|
||||
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
|
||||
readonly createCancelToken: UnwrapRef<typeof import('./composables/request')['createCancelToken']>
|
||||
readonly createEventHook: UnwrapRef<typeof import('@vueuse/core')['createEventHook']>
|
||||
readonly createGlobalState: UnwrapRef<typeof import('@vueuse/core')['createGlobalState']>
|
||||
readonly createInjectionState: UnwrapRef<typeof import('@vueuse/core')['createInjectionState']>
|
||||
readonly createReactiveFn: UnwrapRef<typeof import('@vueuse/core')['createReactiveFn']>
|
||||
readonly createRef: UnwrapRef<typeof import('@vueuse/core')['createRef']>
|
||||
readonly createReusableTemplate: UnwrapRef<typeof import('@vueuse/core')['createReusableTemplate']>
|
||||
readonly createSharedComposable: UnwrapRef<typeof import('@vueuse/core')['createSharedComposable']>
|
||||
readonly createTemplatePromise: UnwrapRef<typeof import('@vueuse/core')['createTemplatePromise']>
|
||||
readonly createUnrefFn: UnwrapRef<typeof import('@vueuse/core')['createUnrefFn']>
|
||||
readonly customRef: UnwrapRef<typeof import('vue')['customRef']>
|
||||
readonly debouncedRef: UnwrapRef<typeof import('@vueuse/core')['debouncedRef']>
|
||||
readonly debouncedWatch: UnwrapRef<typeof import('@vueuse/core')['debouncedWatch']>
|
||||
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
|
||||
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
|
||||
readonly del: UnwrapRef<typeof import('./composables/request')['del']>
|
||||
readonly eagerComputed: UnwrapRef<typeof import('@vueuse/core')['eagerComputed']>
|
||||
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
|
||||
readonly extendRef: UnwrapRef<typeof import('@vueuse/core')['extendRef']>
|
||||
readonly get: UnwrapRef<typeof import('./composables/request')['get']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectHead: UnwrapRef<typeof import('@unhead/vue')['injectHead']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isDark: UnwrapRef<typeof import('./composables/dark')['isDark']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||
readonly makeDestructurable: UnwrapRef<typeof import('@vueuse/core')['makeDestructurable']>
|
||||
readonly markRaw: UnwrapRef<typeof import('vue')['markRaw']>
|
||||
readonly nextTick: UnwrapRef<typeof import('vue')['nextTick']>
|
||||
readonly onActivated: UnwrapRef<typeof import('vue')['onActivated']>
|
||||
readonly onBeforeMount: UnwrapRef<typeof import('vue')['onBeforeMount']>
|
||||
readonly onBeforeRouteLeave: UnwrapRef<typeof import('vue-router')['onBeforeRouteLeave']>
|
||||
readonly onBeforeRouteUpdate: UnwrapRef<typeof import('vue-router')['onBeforeRouteUpdate']>
|
||||
readonly onBeforeUnmount: UnwrapRef<typeof import('vue')['onBeforeUnmount']>
|
||||
readonly onBeforeUpdate: UnwrapRef<typeof import('vue')['onBeforeUpdate']>
|
||||
readonly onClickOutside: UnwrapRef<typeof import('@vueuse/core')['onClickOutside']>
|
||||
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
|
||||
readonly onElementRemoval: UnwrapRef<typeof import('@vueuse/core')['onElementRemoval']>
|
||||
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
|
||||
readonly onKeyStroke: UnwrapRef<typeof import('@vueuse/core')['onKeyStroke']>
|
||||
readonly onLongPress: UnwrapRef<typeof import('@vueuse/core')['onLongPress']>
|
||||
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
|
||||
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
|
||||
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
|
||||
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
|
||||
readonly onServerPrefetch: UnwrapRef<typeof import('vue')['onServerPrefetch']>
|
||||
readonly onStartTyping: UnwrapRef<typeof import('@vueuse/core')['onStartTyping']>
|
||||
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
|
||||
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
|
||||
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
|
||||
readonly pausableWatch: UnwrapRef<typeof import('@vueuse/core')['pausableWatch']>
|
||||
readonly post: UnwrapRef<typeof import('./composables/request')['post']>
|
||||
readonly preferredDark: UnwrapRef<typeof import('./composables/dark')['preferredDark']>
|
||||
readonly provide: UnwrapRef<typeof import('vue')['provide']>
|
||||
readonly provideLocal: UnwrapRef<typeof import('@vueuse/core')['provideLocal']>
|
||||
readonly put: UnwrapRef<typeof import('./composables/request')['put']>
|
||||
readonly reactify: UnwrapRef<typeof import('@vueuse/core')['reactify']>
|
||||
readonly reactifyObject: UnwrapRef<typeof import('@vueuse/core')['reactifyObject']>
|
||||
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
|
||||
readonly reactiveComputed: UnwrapRef<typeof import('@vueuse/core')['reactiveComputed']>
|
||||
readonly reactiveOmit: UnwrapRef<typeof import('@vueuse/core')['reactiveOmit']>
|
||||
readonly reactivePick: UnwrapRef<typeof import('@vueuse/core')['reactivePick']>
|
||||
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly refAutoReset: UnwrapRef<typeof import('@vueuse/core')['refAutoReset']>
|
||||
readonly refDebounced: UnwrapRef<typeof import('@vueuse/core')['refDebounced']>
|
||||
readonly refDefault: UnwrapRef<typeof import('@vueuse/core')['refDefault']>
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly request: UnwrapRef<typeof import('./composables/request')['default']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly resolveUnref: UnwrapRef<typeof import('@vueuse/core')['resolveUnref']>
|
||||
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
|
||||
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
|
||||
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
|
||||
readonly syncRef: UnwrapRef<typeof import('@vueuse/core')['syncRef']>
|
||||
readonly syncRefs: UnwrapRef<typeof import('@vueuse/core')['syncRefs']>
|
||||
readonly templateRef: UnwrapRef<typeof import('@vueuse/core')['templateRef']>
|
||||
readonly throttledRef: UnwrapRef<typeof import('@vueuse/core')['throttledRef']>
|
||||
readonly throttledWatch: UnwrapRef<typeof import('@vueuse/core')['throttledWatch']>
|
||||
readonly toRaw: UnwrapRef<typeof import('vue')['toRaw']>
|
||||
readonly toReactive: UnwrapRef<typeof import('@vueuse/core')['toReactive']>
|
||||
readonly toRef: UnwrapRef<typeof import('vue')['toRef']>
|
||||
readonly toRefs: UnwrapRef<typeof import('vue')['toRefs']>
|
||||
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
|
||||
readonly toggleDark: UnwrapRef<typeof import('./composables/dark')['toggleDark']>
|
||||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly tryOnBeforeMount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeMount']>
|
||||
readonly tryOnBeforeUnmount: UnwrapRef<typeof import('@vueuse/core')['tryOnBeforeUnmount']>
|
||||
readonly tryOnMounted: UnwrapRef<typeof import('@vueuse/core')['tryOnMounted']>
|
||||
readonly tryOnScopeDispose: UnwrapRef<typeof import('@vueuse/core')['tryOnScopeDispose']>
|
||||
readonly tryOnUnmounted: UnwrapRef<typeof import('@vueuse/core')['tryOnUnmounted']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly unrefElement: UnwrapRef<typeof import('@vueuse/core')['unrefElement']>
|
||||
readonly until: UnwrapRef<typeof import('@vueuse/core')['until']>
|
||||
readonly useActiveElement: UnwrapRef<typeof import('@vueuse/core')['useActiveElement']>
|
||||
readonly useAnimate: UnwrapRef<typeof import('@vueuse/core')['useAnimate']>
|
||||
readonly useArrayDifference: UnwrapRef<typeof import('@vueuse/core')['useArrayDifference']>
|
||||
readonly useArrayEvery: UnwrapRef<typeof import('@vueuse/core')['useArrayEvery']>
|
||||
readonly useArrayFilter: UnwrapRef<typeof import('@vueuse/core')['useArrayFilter']>
|
||||
readonly useArrayFind: UnwrapRef<typeof import('@vueuse/core')['useArrayFind']>
|
||||
readonly useArrayFindIndex: UnwrapRef<typeof import('@vueuse/core')['useArrayFindIndex']>
|
||||
readonly useArrayFindLast: UnwrapRef<typeof import('@vueuse/core')['useArrayFindLast']>
|
||||
readonly useArrayIncludes: UnwrapRef<typeof import('@vueuse/core')['useArrayIncludes']>
|
||||
readonly useArrayJoin: UnwrapRef<typeof import('@vueuse/core')['useArrayJoin']>
|
||||
readonly useArrayMap: UnwrapRef<typeof import('@vueuse/core')['useArrayMap']>
|
||||
readonly useArrayReduce: UnwrapRef<typeof import('@vueuse/core')['useArrayReduce']>
|
||||
readonly useArraySome: UnwrapRef<typeof import('@vueuse/core')['useArraySome']>
|
||||
readonly useArrayUnique: UnwrapRef<typeof import('@vueuse/core')['useArrayUnique']>
|
||||
readonly useAsyncQueue: UnwrapRef<typeof import('@vueuse/core')['useAsyncQueue']>
|
||||
readonly useAsyncState: UnwrapRef<typeof import('@vueuse/core')['useAsyncState']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
|
||||
readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
|
||||
readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
|
||||
readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
|
||||
readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
|
||||
readonly useBrowserLocation: UnwrapRef<typeof import('@vueuse/core')['useBrowserLocation']>
|
||||
readonly useCached: UnwrapRef<typeof import('@vueuse/core')['useCached']>
|
||||
readonly useClipboard: UnwrapRef<typeof import('@vueuse/core')['useClipboard']>
|
||||
readonly useClipboardItems: UnwrapRef<typeof import('@vueuse/core')['useClipboardItems']>
|
||||
readonly useCloned: UnwrapRef<typeof import('@vueuse/core')['useCloned']>
|
||||
readonly useColorMode: UnwrapRef<typeof import('@vueuse/core')['useColorMode']>
|
||||
readonly useConfirmDialog: UnwrapRef<typeof import('@vueuse/core')['useConfirmDialog']>
|
||||
readonly useCountdown: UnwrapRef<typeof import('@vueuse/core')['useCountdown']>
|
||||
readonly useCounter: UnwrapRef<typeof import('@vueuse/core')['useCounter']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVar: UnwrapRef<typeof import('@vueuse/core')['useCssVar']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useCurrentElement: UnwrapRef<typeof import('@vueuse/core')['useCurrentElement']>
|
||||
readonly useCycleList: UnwrapRef<typeof import('@vueuse/core')['useCycleList']>
|
||||
readonly useDark: UnwrapRef<typeof import('@vueuse/core')['useDark']>
|
||||
readonly useDateFormat: UnwrapRef<typeof import('@vueuse/core')['useDateFormat']>
|
||||
readonly useDebounce: UnwrapRef<typeof import('@vueuse/core')['useDebounce']>
|
||||
readonly useDebounceFn: UnwrapRef<typeof import('@vueuse/core')['useDebounceFn']>
|
||||
readonly useDebouncedRefHistory: UnwrapRef<typeof import('@vueuse/core')['useDebouncedRefHistory']>
|
||||
readonly useDeviceMotion: UnwrapRef<typeof import('@vueuse/core')['useDeviceMotion']>
|
||||
readonly useDeviceOrientation: UnwrapRef<typeof import('@vueuse/core')['useDeviceOrientation']>
|
||||
readonly useDevicePixelRatio: UnwrapRef<typeof import('@vueuse/core')['useDevicePixelRatio']>
|
||||
readonly useDevicesList: UnwrapRef<typeof import('@vueuse/core')['useDevicesList']>
|
||||
readonly useDisplayMedia: UnwrapRef<typeof import('@vueuse/core')['useDisplayMedia']>
|
||||
readonly useDocumentVisibility: UnwrapRef<typeof import('@vueuse/core')['useDocumentVisibility']>
|
||||
readonly useDraggable: UnwrapRef<typeof import('@vueuse/core')['useDraggable']>
|
||||
readonly useDropZone: UnwrapRef<typeof import('@vueuse/core')['useDropZone']>
|
||||
readonly useElementBounding: UnwrapRef<typeof import('@vueuse/core')['useElementBounding']>
|
||||
readonly useElementByPoint: UnwrapRef<typeof import('@vueuse/core')['useElementByPoint']>
|
||||
readonly useElementHover: UnwrapRef<typeof import('@vueuse/core')['useElementHover']>
|
||||
readonly useElementSize: UnwrapRef<typeof import('@vueuse/core')['useElementSize']>
|
||||
readonly useElementVisibility: UnwrapRef<typeof import('@vueuse/core')['useElementVisibility']>
|
||||
readonly useEventBus: UnwrapRef<typeof import('@vueuse/core')['useEventBus']>
|
||||
readonly useEventListener: UnwrapRef<typeof import('@vueuse/core')['useEventListener']>
|
||||
readonly useEventSource: UnwrapRef<typeof import('@vueuse/core')['useEventSource']>
|
||||
readonly useEyeDropper: UnwrapRef<typeof import('@vueuse/core')['useEyeDropper']>
|
||||
readonly useFavicon: UnwrapRef<typeof import('@vueuse/core')['useFavicon']>
|
||||
readonly useFetch: UnwrapRef<typeof import('@vueuse/core')['useFetch']>
|
||||
readonly useFileDialog: UnwrapRef<typeof import('@vueuse/core')['useFileDialog']>
|
||||
readonly useFileSystemAccess: UnwrapRef<typeof import('@vueuse/core')['useFileSystemAccess']>
|
||||
readonly useFocus: UnwrapRef<typeof import('@vueuse/core')['useFocus']>
|
||||
readonly useFocusWithin: UnwrapRef<typeof import('@vueuse/core')['useFocusWithin']>
|
||||
readonly useFps: UnwrapRef<typeof import('@vueuse/core')['useFps']>
|
||||
readonly useFullscreen: UnwrapRef<typeof import('@vueuse/core')['useFullscreen']>
|
||||
readonly useGamepad: UnwrapRef<typeof import('@vueuse/core')['useGamepad']>
|
||||
readonly useGeolocation: UnwrapRef<typeof import('@vueuse/core')['useGeolocation']>
|
||||
readonly useHead: UnwrapRef<typeof import('@unhead/vue')['useHead']>
|
||||
readonly useHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useHeadSafe']>
|
||||
readonly useI18n: UnwrapRef<typeof import('vue-i18n')['useI18n']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
readonly useIdle: UnwrapRef<typeof import('@vueuse/core')['useIdle']>
|
||||
readonly useImage: UnwrapRef<typeof import('@vueuse/core')['useImage']>
|
||||
readonly useInfiniteScroll: UnwrapRef<typeof import('@vueuse/core')['useInfiniteScroll']>
|
||||
readonly useIntersectionObserver: UnwrapRef<typeof import('@vueuse/core')['useIntersectionObserver']>
|
||||
readonly useInterval: UnwrapRef<typeof import('@vueuse/core')['useInterval']>
|
||||
readonly useIntervalFn: UnwrapRef<typeof import('@vueuse/core')['useIntervalFn']>
|
||||
readonly useKeyModifier: UnwrapRef<typeof import('@vueuse/core')['useKeyModifier']>
|
||||
readonly useLastChanged: UnwrapRef<typeof import('@vueuse/core')['useLastChanged']>
|
||||
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
|
||||
readonly useLocalStorage: UnwrapRef<typeof import('@vueuse/core')['useLocalStorage']>
|
||||
readonly useMagicKeys: UnwrapRef<typeof import('@vueuse/core')['useMagicKeys']>
|
||||
readonly useManualRefHistory: UnwrapRef<typeof import('@vueuse/core')['useManualRefHistory']>
|
||||
readonly useMediaControls: UnwrapRef<typeof import('@vueuse/core')['useMediaControls']>
|
||||
readonly useMediaQuery: UnwrapRef<typeof import('@vueuse/core')['useMediaQuery']>
|
||||
readonly useMemoize: UnwrapRef<typeof import('@vueuse/core')['useMemoize']>
|
||||
readonly useMemory: UnwrapRef<typeof import('@vueuse/core')['useMemory']>
|
||||
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
|
||||
readonly useMounted: UnwrapRef<typeof import('@vueuse/core')['useMounted']>
|
||||
readonly useMouse: UnwrapRef<typeof import('@vueuse/core')['useMouse']>
|
||||
readonly useMouseInElement: UnwrapRef<typeof import('@vueuse/core')['useMouseInElement']>
|
||||
readonly useMousePressed: UnwrapRef<typeof import('@vueuse/core')['useMousePressed']>
|
||||
readonly useMutationObserver: UnwrapRef<typeof import('@vueuse/core')['useMutationObserver']>
|
||||
readonly useNavigatorLanguage: UnwrapRef<typeof import('@vueuse/core')['useNavigatorLanguage']>
|
||||
readonly useNetwork: UnwrapRef<typeof import('@vueuse/core')['useNetwork']>
|
||||
readonly useNow: UnwrapRef<typeof import('@vueuse/core')['useNow']>
|
||||
readonly useObjectUrl: UnwrapRef<typeof import('@vueuse/core')['useObjectUrl']>
|
||||
readonly useOffsetPagination: UnwrapRef<typeof import('@vueuse/core')['useOffsetPagination']>
|
||||
readonly useOnline: UnwrapRef<typeof import('@vueuse/core')['useOnline']>
|
||||
readonly usePageLeave: UnwrapRef<typeof import('@vueuse/core')['usePageLeave']>
|
||||
readonly useParallax: UnwrapRef<typeof import('@vueuse/core')['useParallax']>
|
||||
readonly useParentElement: UnwrapRef<typeof import('@vueuse/core')['useParentElement']>
|
||||
readonly usePerformanceObserver: UnwrapRef<typeof import('@vueuse/core')['usePerformanceObserver']>
|
||||
readonly usePermission: UnwrapRef<typeof import('@vueuse/core')['usePermission']>
|
||||
readonly usePointer: UnwrapRef<typeof import('@vueuse/core')['usePointer']>
|
||||
readonly usePointerLock: UnwrapRef<typeof import('@vueuse/core')['usePointerLock']>
|
||||
readonly usePointerSwipe: UnwrapRef<typeof import('@vueuse/core')['usePointerSwipe']>
|
||||
readonly usePreferredColorScheme: UnwrapRef<typeof import('@vueuse/core')['usePreferredColorScheme']>
|
||||
readonly usePreferredContrast: UnwrapRef<typeof import('@vueuse/core')['usePreferredContrast']>
|
||||
readonly usePreferredDark: UnwrapRef<typeof import('@vueuse/core')['usePreferredDark']>
|
||||
readonly usePreferredLanguages: UnwrapRef<typeof import('@vueuse/core')['usePreferredLanguages']>
|
||||
readonly usePreferredReducedMotion: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedMotion']>
|
||||
readonly usePreferredReducedTransparency: UnwrapRef<typeof import('@vueuse/core')['usePreferredReducedTransparency']>
|
||||
readonly usePrevious: UnwrapRef<typeof import('@vueuse/core')['usePrevious']>
|
||||
readonly useRafFn: UnwrapRef<typeof import('@vueuse/core')['useRafFn']>
|
||||
readonly useRefHistory: UnwrapRef<typeof import('@vueuse/core')['useRefHistory']>
|
||||
readonly useResizeObserver: UnwrapRef<typeof import('@vueuse/core')['useResizeObserver']>
|
||||
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
|
||||
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
|
||||
readonly useSSRWidth: UnwrapRef<typeof import('@vueuse/core')['useSSRWidth']>
|
||||
readonly useScreenOrientation: UnwrapRef<typeof import('@vueuse/core')['useScreenOrientation']>
|
||||
readonly useScreenSafeArea: UnwrapRef<typeof import('@vueuse/core')['useScreenSafeArea']>
|
||||
readonly useScriptTag: UnwrapRef<typeof import('@vueuse/core')['useScriptTag']>
|
||||
readonly useScroll: UnwrapRef<typeof import('@vueuse/core')['useScroll']>
|
||||
readonly useScrollLock: UnwrapRef<typeof import('@vueuse/core')['useScrollLock']>
|
||||
readonly useSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useSeoMeta']>
|
||||
readonly useServerHead: UnwrapRef<typeof import('@unhead/vue')['useServerHead']>
|
||||
readonly useServerHeadSafe: UnwrapRef<typeof import('@unhead/vue')['useServerHeadSafe']>
|
||||
readonly useServerSeoMeta: UnwrapRef<typeof import('@unhead/vue')['useServerSeoMeta']>
|
||||
readonly useSessionStorage: UnwrapRef<typeof import('@vueuse/core')['useSessionStorage']>
|
||||
readonly useShare: UnwrapRef<typeof import('@vueuse/core')['useShare']>
|
||||
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
|
||||
readonly useSorted: UnwrapRef<typeof import('@vueuse/core')['useSorted']>
|
||||
readonly useSpeechRecognition: UnwrapRef<typeof import('@vueuse/core')['useSpeechRecognition']>
|
||||
readonly useSpeechSynthesis: UnwrapRef<typeof import('@vueuse/core')['useSpeechSynthesis']>
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
|
||||
readonly useTemplateRefsList: UnwrapRef<typeof import('@vueuse/core')['useTemplateRefsList']>
|
||||
readonly useTextDirection: UnwrapRef<typeof import('@vueuse/core')['useTextDirection']>
|
||||
readonly useTextSelection: UnwrapRef<typeof import('@vueuse/core')['useTextSelection']>
|
||||
readonly useTextareaAutosize: UnwrapRef<typeof import('@vueuse/core')['useTextareaAutosize']>
|
||||
readonly useThrottle: UnwrapRef<typeof import('@vueuse/core')['useThrottle']>
|
||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
readonly useTimestamp: UnwrapRef<typeof import('@vueuse/core')['useTimestamp']>
|
||||
readonly useTitle: UnwrapRef<typeof import('@vueuse/core')['useTitle']>
|
||||
readonly useToNumber: UnwrapRef<typeof import('@vueuse/core')['useToNumber']>
|
||||
readonly useToString: UnwrapRef<typeof import('@vueuse/core')['useToString']>
|
||||
readonly useToggle: UnwrapRef<typeof import('@vueuse/core')['useToggle']>
|
||||
readonly useTransition: UnwrapRef<typeof import('@vueuse/core')['useTransition']>
|
||||
readonly useUrlSearchParams: UnwrapRef<typeof import('@vueuse/core')['useUrlSearchParams']>
|
||||
readonly useUserMedia: UnwrapRef<typeof import('@vueuse/core')['useUserMedia']>
|
||||
readonly useUserStore: UnwrapRef<typeof import('./stores/user')['useUserStore']>
|
||||
readonly useVModel: UnwrapRef<typeof import('@vueuse/core')['useVModel']>
|
||||
readonly useVModels: UnwrapRef<typeof import('@vueuse/core')['useVModels']>
|
||||
readonly useVibrate: UnwrapRef<typeof import('@vueuse/core')['useVibrate']>
|
||||
readonly useVirtualList: UnwrapRef<typeof import('@vueuse/core')['useVirtualList']>
|
||||
readonly useWakeLock: UnwrapRef<typeof import('@vueuse/core')['useWakeLock']>
|
||||
readonly useWebNotification: UnwrapRef<typeof import('@vueuse/core')['useWebNotification']>
|
||||
readonly useWebSocket: UnwrapRef<typeof import('@vueuse/core')['useWebSocket']>
|
||||
readonly useWebWorker: UnwrapRef<typeof import('@vueuse/core')['useWebWorker']>
|
||||
readonly useWebWorkerFn: UnwrapRef<typeof import('@vueuse/core')['useWebWorkerFn']>
|
||||
readonly useWindowFocus: UnwrapRef<typeof import('@vueuse/core')['useWindowFocus']>
|
||||
readonly useWindowScroll: UnwrapRef<typeof import('@vueuse/core')['useWindowScroll']>
|
||||
readonly useWindowSize: UnwrapRef<typeof import('@vueuse/core')['useWindowSize']>
|
||||
readonly watch: UnwrapRef<typeof import('vue')['watch']>
|
||||
readonly watchArray: UnwrapRef<typeof import('@vueuse/core')['watchArray']>
|
||||
readonly watchAtMost: UnwrapRef<typeof import('@vueuse/core')['watchAtMost']>
|
||||
readonly watchDebounced: UnwrapRef<typeof import('@vueuse/core')['watchDebounced']>
|
||||
readonly watchDeep: UnwrapRef<typeof import('@vueuse/core')['watchDeep']>
|
||||
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
|
||||
readonly watchIgnorable: UnwrapRef<typeof import('@vueuse/core')['watchIgnorable']>
|
||||
readonly watchImmediate: UnwrapRef<typeof import('@vueuse/core')['watchImmediate']>
|
||||
readonly watchOnce: UnwrapRef<typeof import('@vueuse/core')['watchOnce']>
|
||||
readonly watchPausable: UnwrapRef<typeof import('@vueuse/core')['watchPausable']>
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
readonly watchThrottled: UnwrapRef<typeof import('@vueuse/core')['watchThrottled']>
|
||||
readonly watchTriggerable: UnwrapRef<typeof import('@vueuse/core')['watchTriggerable']>
|
||||
readonly watchWithFilter: UnwrapRef<typeof import('@vueuse/core')['watchWithFilter']>
|
||||
readonly whenever: UnwrapRef<typeof import('@vueuse/core')['whenever']>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
BackToTop: typeof import('./components/BackToTop.vue')['default']
|
||||
FilterBar: typeof import('./components/FilterBar.vue')['default']
|
||||
README: typeof import('./components/README.md')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
ScoreForm: typeof import('./components/ScoreForm.vue')['default']
|
||||
TheCounter: typeof import('./components/TheCounter.vue')['default']
|
||||
TheFooter: typeof import('./components/TheFooter.vue')['default']
|
||||
TheInput: typeof import('./components/TheInput.vue')['default']
|
||||
TheNavigation: typeof import('./components/TheNavigation.vue')['default']
|
||||
WMessage: typeof import('./components/ui/WMessage.vue')['default']
|
||||
WOption: typeof import('./components/ui/WOption.vue')['default']
|
||||
WPopconfirm: typeof import('./components/ui/WPopconfirm.vue')['default']
|
||||
WRadioButton: typeof import('./components/ui/WRadioButton.vue')['default']
|
||||
WRadioGroup: typeof import('./components/ui/WRadioGroup.vue')['default']
|
||||
WSelect: typeof import('./components/ui/WSelect.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<script setup lang="ts">
|
||||
import { useWindowScroll } from '@vueuse/core'
|
||||
|
||||
const { y } = useWindowScroll()
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition duration-300 ease-out"
|
||||
enter-from-class="translate-y-10 opacity-0"
|
||||
enter-to-class="translate-y-0 opacity-100"
|
||||
leave-active-class="transition duration-300 ease-in"
|
||||
leave-from-class="translate-y-0 opacity-100"
|
||||
leave-to-class="translate-y-10 opacity-0"
|
||||
>
|
||||
<button
|
||||
v-if="y > 500"
|
||||
class="fixed bottom-5 right-5 z-40 rounded-full bg-blue-600 p-3 text-white shadow-lg transition-colors dark:bg-blue-500 hover:bg-blue-700 focus:outline-none dark:hover:bg-blue-600"
|
||||
title="返回顶部"
|
||||
@click="scrollToTop"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
||||
</svg>
|
||||
</button>
|
||||
</Transition>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
|
||||
// 定义数据类型 (方便父组件有类型提示)
|
||||
export interface FilterState {
|
||||
location: string
|
||||
type: string
|
||||
major: string
|
||||
sort: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
sortEnabled?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
sortEnabled: false,
|
||||
})
|
||||
|
||||
// 定义 Emits (声明组件会触发哪些事件)
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', payload: { keyword: string, filters: FilterState }): void
|
||||
|
||||
// // 当点击下拉菜单里的“确定”时触发,回传筛选对象
|
||||
// (e: 'confirm', filters: FilterState): void;
|
||||
// // 当点击右侧“搜索”按钮时触发,回传搜索关键词
|
||||
// (e: 'search', keyword: string): void;
|
||||
}>()
|
||||
|
||||
// --- 类型定义 ---
|
||||
type FilterKey = 'location' | 'type' | 'major' | 'sort'
|
||||
|
||||
interface FilterConfig {
|
||||
key: FilterKey
|
||||
label: string
|
||||
}
|
||||
|
||||
// --- 数据 ---
|
||||
const filters: FilterConfig[] = [
|
||||
{ key: 'location', label: '位置' },
|
||||
{ key: 'type', label: '类型' },
|
||||
{ key: 'major', label: '专业' },
|
||||
{ key: 'sort', label: '排序' },
|
||||
]
|
||||
|
||||
// 按照截图还原的省份数据
|
||||
const locations = [
|
||||
'不限',
|
||||
'北京',
|
||||
'天津',
|
||||
'河北',
|
||||
'山西',
|
||||
'内蒙古',
|
||||
'辽宁',
|
||||
'吉林',
|
||||
'黑龙江',
|
||||
'上海',
|
||||
'江苏',
|
||||
'浙江',
|
||||
'安徽',
|
||||
'福建',
|
||||
'江西',
|
||||
'山东',
|
||||
'河南',
|
||||
'湖北',
|
||||
'湖南',
|
||||
'广东',
|
||||
'广西',
|
||||
'海南',
|
||||
'重庆',
|
||||
'四川',
|
||||
'贵州',
|
||||
'云南',
|
||||
'西藏',
|
||||
'陕西',
|
||||
'甘肃',
|
||||
'青海',
|
||||
'宁夏',
|
||||
'新疆',
|
||||
'台湾',
|
||||
'香港',
|
||||
'澳门',
|
||||
]
|
||||
|
||||
// --- 状态 ---
|
||||
const containerRef = ref<HTMLElement | null>(null) // 用于检测点击外部
|
||||
const activeFilter = ref<FilterKey | null>(null) // 当前展开的菜单
|
||||
const searchQuery = ref('')
|
||||
|
||||
// 选中的值
|
||||
const selectedFilters = reactive({
|
||||
location: '不限',
|
||||
type: '不限',
|
||||
major: '不限',
|
||||
sort: '默认',
|
||||
})
|
||||
|
||||
// 临时选中的值(用于在点击“确定”之前暂存,如果需要这种逻辑的话,本例为了简单直接修改 selectedFilters)
|
||||
// 如果要严格复刻逻辑,通常点击选项时只变色,点确定才生效。这里简化为点击即选中高亮。
|
||||
|
||||
// --- 计算属性 ---
|
||||
function getLabel(key: FilterKey) {
|
||||
// 如果不是默认值,可以显示选中的值,否则显示标题
|
||||
// 这里为了保持截图样式,保持显示标题
|
||||
const map = { location: '位置', type: '类型', major: '专业', sort: '排序' }
|
||||
return map[key]
|
||||
}
|
||||
|
||||
const countSelected = computed(() => {
|
||||
// 简单计算:如果当前打开的是位置,且选的不是不限,则为1,否则0
|
||||
if (activeFilter.value === 'location') {
|
||||
return selectedFilters.location !== '不限' ? 1 : 0
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
function shouldShowFilter(filter: FilterConfig) {
|
||||
// 显示所有非排序过滤器,或者当排序过滤器启用时显示排序过滤器
|
||||
return filter.key !== 'sort' || (filter.key === 'sort' && props.sortEnabled)
|
||||
}
|
||||
|
||||
// --- 方法 ---
|
||||
|
||||
// 切换下拉菜单
|
||||
function toggleFilter(key: FilterKey) {
|
||||
if (activeFilter.value === key) {
|
||||
activeFilter.value = null // 关闭
|
||||
}
|
||||
else {
|
||||
activeFilter.value = key // 打开
|
||||
}
|
||||
}
|
||||
|
||||
// 选择选项
|
||||
function selectOption(type: FilterKey, value: string) {
|
||||
selectedFilters[type] = value
|
||||
}
|
||||
|
||||
// 清空当前
|
||||
function clearCurrentFilter() {
|
||||
if (activeFilter.value) {
|
||||
selectedFilters[activeFilter.value] = '不限'
|
||||
}
|
||||
}
|
||||
|
||||
// 确定按钮
|
||||
function confirmSelection() {
|
||||
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
|
||||
// 使用展开运算符 {...} 创建副本,防止父组件修改影响子组件(可选)
|
||||
// emit('confirm', { ...selectedFilters });
|
||||
activeFilter.value = null // 关闭菜单
|
||||
}
|
||||
|
||||
// 搜索按钮
|
||||
function handleSearch() {
|
||||
emit('change', { keyword: searchQuery.value, filters: { ...selectedFilters } })
|
||||
// emit('search', searchQuery.value);
|
||||
}
|
||||
|
||||
// --- 点击外部关闭逻辑 ---
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
|
||||
activeFilter.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 最外层容器,用于定位下拉菜单和监听点击外部事件 -->
|
||||
<div ref="containerRef" class="relative mx-auto max-w-5xl w-full select-none text-sm text-gray-600 font-sans">
|
||||
<!-- 顶部栏:筛选按钮组 + 搜索框 -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<!-- 左侧:4个下拉筛选器 -->
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div
|
||||
v-for="filter in filters"
|
||||
v-show="shouldShowFilter(filter)"
|
||||
:key="filter.key"
|
||||
class="relative"
|
||||
>
|
||||
<!-- 筛选器按钮 -->
|
||||
<button
|
||||
class="h-9 w-24 flex items-center justify-between border rounded bg-white px-3 transition-colors hover:border-blue-400"
|
||||
:class="activeFilter === filter.key ? 'border-blue-500 ring-1 ring-blue-200' : 'border-gray-200'"
|
||||
@click="toggleFilter(filter.key)"
|
||||
>
|
||||
<span class="truncate">{{ getLabel(filter.key) }}</span>
|
||||
<!-- 下箭头图标 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 text-gray-400 transition-transform duration-200" :class="{ 'rotate-180': activeFilter === filter.key }" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧:搜索框 -->
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div class="relative">
|
||||
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg class="h-4 w-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
</div>
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
class="h-9 w-50 border border-gray-300 rounded-l bg-white py-2 pl-9 pr-4 text-gray-700 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-200 placeholder-gray-400"
|
||||
placeholder="输入院校名称"
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="h-9 rounded-r bg-blue-500 px-6 text-white transition-colors active:bg-blue-700 hover:bg-blue-600 focus:outline-none"
|
||||
@click="handleSearch"
|
||||
>
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下拉菜单面板 (绝对定位) -->
|
||||
<!-- 使用 Transition 添加简单的淡入淡出效果 -->
|
||||
<transition
|
||||
enter-active-class="transition duration-100 ease-out"
|
||||
enter-from-class="transform scale-95 opacity-0"
|
||||
enter-to-class="transform scale-100 opacity-100"
|
||||
leave-active-class="transition duration-75 ease-in"
|
||||
leave-from-class="transform scale-100 opacity-100"
|
||||
leave-to-class="transform scale-95 opacity-0"
|
||||
>
|
||||
<!-- style="min-width: 600px;" -->
|
||||
<div
|
||||
v-if="activeFilter"
|
||||
class="absolute left-0 top-full z-50 mt-2 border border-gray-100 rounded-lg bg-white p-5 shadow-xl"
|
||||
>
|
||||
<!-- 下拉内容区域 -->
|
||||
|
||||
<!-- 1. 位置 (Location) 内容 -->
|
||||
<div v-if="activeFilter === 'location'">
|
||||
<div class="flex items-start gap-4">
|
||||
<span class="mt-1 shrink-0 text-gray-400 font-medium">院校所属</span>
|
||||
<div class="xs:grid-cols-3 grid gap-2 lg:grid-cols-6 md:grid-cols-5 sm:grid-cols-3">
|
||||
<button
|
||||
v-for="city in locations"
|
||||
:key="city"
|
||||
class="rounded px-3 py-1.5 text-center transition-colors hover:text-blue-500"
|
||||
:class="selectedFilters.location === city
|
||||
? 'bg-blue-50 text-blue-500 font-medium'
|
||||
: 'text-gray-600 bg-gray-50 hover:bg-gray-100'"
|
||||
@click="selectOption('location', city)"
|
||||
>
|
||||
{{ city }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. 其他筛选器占位内容 (类型、专业、排序) -->
|
||||
<div v-else class="p-4 text-center text-gray-400">
|
||||
这里是 {{ filters.find(f => f.key === activeFilter)?.label }} 的筛选选项
|
||||
</div>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<div class="mt-6 flex items-center justify-between border-t border-gray-100 pt-4">
|
||||
<div class="text-gray-500">
|
||||
已选 <span class="text-blue-500 font-bold">{{ countSelected }}</span> 个
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="border border-blue-500 rounded px-4 py-1.5 text-blue-500 transition-colors hover:bg-blue-50"
|
||||
@click="clearCurrentFilter"
|
||||
>
|
||||
清空已选
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-blue-500 px-6 py-1.5 text-white transition-colors hover:bg-blue-600"
|
||||
@click="confirmSelection"
|
||||
>
|
||||
确定
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/*
|
||||
如果你的 tailwind 配置没有自动移除按钮的默认样式,可能需要以下代码。
|
||||
一般在 tailwind base 中已经处理好了。
|
||||
*/
|
||||
</style>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
## Components
|
||||
|
||||
Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components).
|
||||
|
||||
### Icons
|
||||
|
||||
You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/).
|
||||
|
||||
It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details.
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 定义回传的数据类型
|
||||
export interface ScoreFormData {
|
||||
examType: string
|
||||
selectedElectives: string[]
|
||||
majorCategory: string
|
||||
selectedSubMajors: string[]
|
||||
scores: {
|
||||
unified: string
|
||||
culture: string
|
||||
chinese: string
|
||||
english: string
|
||||
}
|
||||
}
|
||||
|
||||
// 定义 Emits
|
||||
const emit = defineEmits<{
|
||||
(e: 'confirm', data: ScoreFormData): void
|
||||
}>()
|
||||
|
||||
// --- Form State ---
|
||||
const examType = ref('历史组')
|
||||
const selectedElectives = ref<string[]>([])
|
||||
const majorCategory = ref('')
|
||||
const selectedSubMajors = ref<string[]>([])
|
||||
|
||||
// Score inputs
|
||||
const scores = ref({
|
||||
unified: '', // 统考/主项
|
||||
culture: '', // 文化
|
||||
chinese: '', // 语文
|
||||
english: '', // 英语
|
||||
})
|
||||
|
||||
// Errors state
|
||||
const errors = ref({
|
||||
examType: '',
|
||||
electives: '',
|
||||
majorCategory: '',
|
||||
subMajors: '',
|
||||
scores: {
|
||||
unified: '',
|
||||
culture: '',
|
||||
chinese: '',
|
||||
english: '',
|
||||
},
|
||||
})
|
||||
|
||||
// --- Options Data ---
|
||||
const electiveOptions = [
|
||||
{ label: '地理', value: '地理' },
|
||||
{ label: '政治', value: '政治' },
|
||||
{ label: '化学', value: '化学' },
|
||||
{ label: '生物', value: '生物' },
|
||||
]
|
||||
|
||||
const majorCategoryOptions = [
|
||||
{ label: '美术与设计类', value: '美术与设计类' },
|
||||
{ label: '播音与主持类', value: '播音与主持类' },
|
||||
{ label: '表演类', value: '表演类' },
|
||||
{ label: '音乐类', value: '音乐类' },
|
||||
{ label: '舞蹈类', value: '舞蹈类' },
|
||||
{ label: '书法类', value: '书法类' },
|
||||
{ label: '戏曲类', value: '戏曲类' },
|
||||
{ label: '体育类', value: '体育类' },
|
||||
]
|
||||
|
||||
// --- Logic Methods ---
|
||||
|
||||
function getSubMajorOptions() {
|
||||
switch (majorCategory.value) {
|
||||
case '表演类':
|
||||
return [
|
||||
{ label: '服装表演', value: '服装表演' },
|
||||
{ label: '戏剧影视导演', value: '戏剧影视导演' },
|
||||
{ label: '戏剧影视表演', value: '戏剧影视表演' },
|
||||
]
|
||||
case '音乐类':
|
||||
return [
|
||||
{ label: '音乐表演声乐', value: '音乐表演声乐', disabled: selectedSubMajors.value.includes('音乐表演器乐') },
|
||||
{ label: '音乐表演器乐', value: '音乐表演器乐', disabled: selectedSubMajors.value.includes('音乐表演声乐') },
|
||||
{ label: '音乐教育', value: '音乐教育' },
|
||||
]
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function handleElectiveChange(value: string) {
|
||||
const index = selectedElectives.value.indexOf(value)
|
||||
if (index === -1) {
|
||||
if (selectedElectives.value.length < 2) {
|
||||
selectedElectives.value.push(value)
|
||||
}
|
||||
}
|
||||
else {
|
||||
selectedElectives.value.splice(index, 1)
|
||||
}
|
||||
// 触发一次校验清除错误提示
|
||||
if (errors.value.electives)
|
||||
validateForm()
|
||||
}
|
||||
|
||||
function handleMajorCategoryChange(val: any) {
|
||||
majorCategory.value = val
|
||||
selectedSubMajors.value = [] // 清空专业选课
|
||||
errors.value.majorCategory = ''
|
||||
}
|
||||
|
||||
function handleSubMajorChange(val: any) {
|
||||
// 可以在这里处理额外的逻辑
|
||||
console.warn(val)
|
||||
errors.value.subMajors = ''
|
||||
}
|
||||
|
||||
// --- Validation Logic ---
|
||||
function validateForm() {
|
||||
// Reset errors
|
||||
errors.value = {
|
||||
examType: '',
|
||||
electives: '',
|
||||
majorCategory: '',
|
||||
subMajors: '',
|
||||
scores: { unified: '', culture: '', chinese: '', english: '' },
|
||||
}
|
||||
let isValid = true
|
||||
|
||||
if (!examType.value) {
|
||||
errors.value.examType = '请选择考试类型'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// 需求:最多2项,通常也意味着至少要选(假设至少1项)
|
||||
if (selectedElectives.value.length === 0) {
|
||||
errors.value.electives = '请选择至少1门选考科目'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (!majorCategory.value) {
|
||||
errors.value.majorCategory = '请选择专业类别'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
// 修复了原代码的逻辑错误:原代码判断 'music',但 value 是 '音乐类'
|
||||
if (majorCategory.value === '音乐类') {
|
||||
const hasVocalOrInstrumental = selectedSubMajors.value.some(item =>
|
||||
item === '音乐表演声乐' || item === '音乐表演器乐',
|
||||
)
|
||||
if (!hasVocalOrInstrumental) {
|
||||
errors.value.subMajors = '音乐类必须选择声乐或器乐'
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Validate scores
|
||||
if (scores.value.unified) {
|
||||
const unifiedScore = Number(scores.value.unified)
|
||||
// const maxUnified = majorCategory.value === '体育类' ? 150 : 300 // 假设逻辑
|
||||
if (Number.isNaN(unifiedScore) || unifiedScore < 0 || unifiedScore > 300) {
|
||||
errors.value.scores.unified = '统考成绩格式不正确'
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (scores.value.culture) {
|
||||
const cultureScore = Number(scores.value.culture)
|
||||
// 修复逻辑:原代码判断 'sports',改为中文 '体育类'
|
||||
// const maxCultureScore = majorCategory.value === '体育类' ? 150 : 300 // 此处根据实际业务需求调整上限
|
||||
if (Number.isNaN(cultureScore) || cultureScore < 0) { // 暂时去掉上限硬性拦截,防止业务变动
|
||||
errors.value.scores.culture = '文化成绩格式不正确'
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (scores.value.chinese) {
|
||||
const s = Number(scores.value.chinese)
|
||||
if (Number.isNaN(s) || s < 0 || s > 150) {
|
||||
errors.value.scores.chinese = '语文成绩必须在0-150之间'
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
if (scores.value.english) {
|
||||
const s = Number(scores.value.english)
|
||||
if (Number.isNaN(s) || s < 0 || s > 150) {
|
||||
errors.value.scores.english = '英语成绩必须在0-150之间'
|
||||
isValid = false
|
||||
}
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
// --- Submit ---
|
||||
function handleSubmit() {
|
||||
if (validateForm()) {
|
||||
// 组装数据
|
||||
const formData: ScoreFormData = {
|
||||
examType: examType.value,
|
||||
selectedElectives: [...selectedElectives.value], // 浅拷贝防止引用问题
|
||||
majorCategory: majorCategory.value,
|
||||
selectedSubMajors: [...selectedSubMajors.value],
|
||||
scores: { ...scores.value },
|
||||
}
|
||||
|
||||
// 抛出事件
|
||||
emit('confirm', formData)
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露重置方法给父组件(如果需要)
|
||||
defineExpose({
|
||||
resetForm: () => {
|
||||
examType.value = '历史组'
|
||||
selectedElectives.value = []
|
||||
majorCategory.value = ''
|
||||
selectedSubMajors.value = []
|
||||
scores.value = { unified: '', culture: '', chinese: '', english: '' }
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="exam-form-container">
|
||||
<!-- Exam Type -->
|
||||
<div class="mb-6">
|
||||
<label class="mb-2 block text-sm font-medium">考试类型</label>
|
||||
<div class="flex gap-4">
|
||||
<!-- 注意:确保项目中已全局注册 w-radio-group 或在此组件import -->
|
||||
<w-radio-group v-model="examType" size="small">
|
||||
<w-radio-button value="历史组" label="历史组" />
|
||||
<w-radio-button value="物理组" label="物理组" />
|
||||
</w-radio-group>
|
||||
</div>
|
||||
<div v-if="errors.examType" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.examType }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Elective Subjects (4选2) -->
|
||||
<div class="mb-6">
|
||||
<label class="mb-2 block text-sm font-medium">选考科目</label>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<button
|
||||
v-for="option in electiveOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="border rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
||||
:class="selectedElectives.includes(option.value)
|
||||
? 'bg-blue-600 text-white border-blue-600'
|
||||
: 'bg-gray-50 dark:bg-gray-300 dark:text-gray-800 border-gray-300 hover:bg-gray-100'"
|
||||
@click="handleElectiveChange(option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="errors.electives" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.electives }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Major Category -->
|
||||
<div class="mb-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">专业类别</label>
|
||||
<div class="flex gap-4">
|
||||
<w-select
|
||||
v-model:value="majorCategory"
|
||||
placeholder="请选择"
|
||||
:options="majorCategoryOptions"
|
||||
@change="handleMajorCategoryChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="errors.majorCategory" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.majorCategory }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Sub-major Category (条件显示) -->
|
||||
<div v-if="majorCategory === '表演类' || majorCategory === '音乐类'">
|
||||
<label class="mb-2 block text-sm font-medium">
|
||||
{{ majorCategory === '表演类' ? '表演类' : '音乐类' }}专业选课
|
||||
</label>
|
||||
<div class="flex gap-4">
|
||||
<w-select
|
||||
v-model:value="selectedSubMajors"
|
||||
mode="multiple"
|
||||
placeholder="请选择..."
|
||||
:max-tag-count="1"
|
||||
:max-select-count="3"
|
||||
:options="getSubMajorOptions()"
|
||||
@change="handleSubMajorChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="errors.subMajors" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.subMajors }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scores Section -->
|
||||
<div class="mb-6">
|
||||
<h3 class="mb-3 text-sm font-semibold">
|
||||
成绩输入
|
||||
</h3>
|
||||
|
||||
<!-- Unified Exam Score -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="mb-4">
|
||||
<label class="mb-2 block text-sm font-medium">
|
||||
{{ majorCategory === '音乐类' ? '主项成绩' : '统考成绩' }}
|
||||
</label>
|
||||
<input
|
||||
v-model="scores.unified"
|
||||
type="number"
|
||||
min="0"
|
||||
:max="majorCategory === '体育类' ? 150 : 300"
|
||||
:placeholder="majorCategory === '体育类' ? '0-150' : '0-300'"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<div v-if="errors.scores.unified" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.scores.unified }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Culture Score -->
|
||||
<div class="mb-4">
|
||||
<label class="mb-2 block text-sm font-medium">文化成绩</label>
|
||||
<input
|
||||
v-model="scores.culture"
|
||||
type="number"
|
||||
min="0"
|
||||
max="300"
|
||||
placeholder="0-300"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<div v-if="errors.scores.culture" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.scores.culture }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chinese & English Scores -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">语文成绩</label>
|
||||
<input
|
||||
v-model="scores.chinese"
|
||||
type="number"
|
||||
min="0"
|
||||
max="150"
|
||||
placeholder="0-150"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<div v-if="errors.scores.chinese" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.scores.chinese }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-2 block text-sm font-medium">英语成绩</label>
|
||||
<input
|
||||
v-model="scores.english"
|
||||
type="number"
|
||||
min="0"
|
||||
max="150"
|
||||
placeholder="0-150"
|
||||
class="w-full border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<div v-if="errors.scores.english" class="mt-2 text-sm text-red-600">
|
||||
{{ errors.scores.english }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
class="w-full rounded-full bg-blue-600 px-6 py-3 text-lg text-white font-semibold shadow-lg transition-colors hover:bg-blue-700 hover:shadow-xl"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 你的样式代码 */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
initial: number
|
||||
}>()
|
||||
|
||||
const { count, inc, dec } = useCounter(props.initial)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ count }}
|
||||
<button class="inc" @click="inc()">
|
||||
+
|
||||
</button>
|
||||
<button class="dec" @click="dec()">
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,20 @@
|
|||
<script setup lang="ts">
|
||||
const { modelValue } = defineModels<{
|
||||
modelValue: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
id="input"
|
||||
v-model="modelValue"
|
||||
v-bind="$attrs"
|
||||
type="text"
|
||||
p="x-4 y-2"
|
||||
w="250px"
|
||||
text="center"
|
||||
bg="transparent"
|
||||
border="~ rounded gray-200 dark:gray-700"
|
||||
outline="none active:none"
|
||||
>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
<script setup lang="ts">
|
||||
import type { ScoreFormData } from './ScoreForm.vue'
|
||||
import { useWindowScroll } from '@vueuse/core' // 假设你使用了 VueUse
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const isLoginModalOpen = ref(false)
|
||||
// 新增:控制移动端菜单开关的状态
|
||||
const isMobileMenuOpen = ref(false)
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
const route = useRoute()
|
||||
|
||||
const routerLinkList = [
|
||||
{ path: '/', name: 'home', label: '首页' },
|
||||
{ path: '/simulate', name: 'simulate', label: '模拟填志愿' },
|
||||
{ path: '/universities', name: 'universities', label: '找大学' },
|
||||
{ path: '/majors', name: 'majors', label: '找专业' },
|
||||
] as const
|
||||
|
||||
// 编辑成绩菜单
|
||||
const isScoreModalOpen = ref(false)
|
||||
|
||||
// 处理表单提交的回调
|
||||
function handleScoreFormConfirm(data: ScoreFormData) {
|
||||
console.warn('接收到表单数据:', data)
|
||||
|
||||
// 这里可以进行 API 调用
|
||||
// api.submit(data).then(...)
|
||||
}
|
||||
function openScoreFormModal() {
|
||||
isScoreModalOpen.value = true
|
||||
}
|
||||
|
||||
function closeScoreFormModal() {
|
||||
isScoreModalOpen.value = false
|
||||
}
|
||||
|
||||
// Scroll handling
|
||||
const { y } = useWindowScroll()
|
||||
const isVisible = ref(true)
|
||||
|
||||
watch(y, (newY, oldY) => {
|
||||
// 如果移动端菜单打开了,不要自动隐藏导航栏
|
||||
if (isMobileMenuOpen.value)
|
||||
return
|
||||
|
||||
if (newY <= 0) {
|
||||
isVisible.value = true
|
||||
return
|
||||
}
|
||||
const dy = newY - oldY
|
||||
if (dy > 0 && newY > 50) {
|
||||
isVisible.value = false
|
||||
}
|
||||
if (dy < 0) {
|
||||
isVisible.value = true
|
||||
}
|
||||
})
|
||||
|
||||
function openLoginModal() {
|
||||
isLoginModalOpen.value = true
|
||||
// 打开登录弹窗时,顺便关闭移动端菜单
|
||||
isMobileMenuOpen.value = false
|
||||
username.value = ''
|
||||
password.value = ''
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function closeLoginModal() {
|
||||
isLoginModalOpen.value = false
|
||||
username.value = ''
|
||||
password.value = ''
|
||||
error.value = ''
|
||||
}
|
||||
|
||||
function handleLogin() {
|
||||
if (!username.value || !password.value) {
|
||||
error.value = '请输入用户名和密码'
|
||||
return
|
||||
}
|
||||
userStore.login(username.value, password.value)
|
||||
closeLoginModal()
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
userStore.logout()
|
||||
// 登出后关闭菜单
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
|
||||
// 新增:切换移动端菜单
|
||||
function toggleMobileMenu() {
|
||||
isMobileMenuOpen.value = !isMobileMenuOpen.value
|
||||
}
|
||||
|
||||
// 新增:移动端点击链接后关闭菜单
|
||||
function handleMobileLinkClick() {
|
||||
isMobileMenuOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav
|
||||
class="fixed left-0 right-0 top-0 z-40 select-none bg-white shadow-md transition-transform duration-300 dark:bg-gray-900"
|
||||
:class="isVisible ? 'translate-y-0' : '-translate-y-full'"
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-4 lg:px-8 sm:px-6">
|
||||
<div class="h-16 flex items-center justify-between">
|
||||
<!-- 1. Logo (左侧,始终显示) -->
|
||||
<div class="w-48 flex flex-none items-center">
|
||||
<img
|
||||
src="https://yitisheng.vip/assets/logo.0e7b79fc.png"
|
||||
alt="艺体志愿宝logo"
|
||||
class="h-8 w-auto"
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- 2. 桌面端菜单 (在大屏幕 md 以上显示,小屏幕隐藏) -->
|
||||
<div class="hidden flex-1 items-center md:flex space-x-8">
|
||||
<router-link
|
||||
v-for="item in routerLinkList"
|
||||
:id="item.name"
|
||||
:key="item.name"
|
||||
:to="item.path"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium transition-colors hover:text-blue-600 dark:hover:text-blue-400"
|
||||
:class="{ 'text-blue-600 dark:text-blue-400': route.path === item.path, 'text-gray-700 dark:text-gray-200': route.path !== item.path }"
|
||||
>
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 3. 桌面端按钮 (在大屏幕 md 以上显示,小屏幕隐藏) -->
|
||||
<div class="hidden items-center md:flex space-x-4">
|
||||
<template v-if="!userStore.user">
|
||||
<button
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white font-medium transition-colors dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"
|
||||
@click="openLoginModal"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
class="border border-blue-600 rounded-md bg-white px-4 py-2 text-sm text-blue-600 font-medium transition-colors dark:border-blue-400 dark:bg-gray-800 hover:bg-gray-100 dark:text-blue-400 dark:hover:bg-gray-700"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<div v-else class="flex items-center space-x-6">
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="border-r-2 border-gray-3 pr-2 text-sm text-gray-700 dark:text-gray-200">音乐类</span>
|
||||
<span class="border-r-2 border-gray-3 pr-2 text-sm text-gray-700 dark:text-gray-200">文化成绩</span>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">334分</span>
|
||||
<a target="_blank" class="cursor-pointer text-gray-400 transition-colors hover:text-gray-500 dark:hover:text-gray-300" @click="openScoreFormModal">
|
||||
<div i-carbon:edit class="text-xs" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-200">欢迎, {{ userStore.user.username }}</span>
|
||||
<a href="https://github.com/antfu/vitesse" target="_blank" class="text-gray-400 transition-colors hover:text-gray-500 dark:hover:text-gray-300">
|
||||
<span class="sr-only">GitHub</span>
|
||||
<div i-carbon:document-horizontal class="text-xl text-gray-8" />
|
||||
</a>
|
||||
<button
|
||||
v-show="false"
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm text-white font-medium transition-colors dark:bg-red-500 hover:bg-red-700 dark:hover:bg-red-600"
|
||||
@click="handleLogout"
|
||||
>
|
||||
退出
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4. 移动端汉堡包按钮 (仅在小屏幕 md 以下显示) -->
|
||||
<div class="flex md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-gray-700 hover:bg-gray-100 dark:text-gray-200 hover:text-gray-900 focus:outline-none dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
@click="toggleMobileMenu"
|
||||
>
|
||||
<span class="sr-only">打开主菜单</span>
|
||||
<!-- 菜单关闭时显示汉堡图标 -->
|
||||
<svg
|
||||
v-if="!isMobileMenuOpen"
|
||||
class="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
<!-- 菜单打开时显示 X 图标 -->
|
||||
<svg
|
||||
v-else
|
||||
class="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5. 移动端菜单下拉面板 (仅在 isMobileMenuOpen 为真且屏幕较小时显示) -->
|
||||
<div v-show="isMobileMenuOpen" class="border-t border-gray-200 bg-white md:hidden dark:border-gray-700 dark:bg-gray-900">
|
||||
<div class="px-2 pb-3 pt-2 space-y-1">
|
||||
<router-link
|
||||
v-for="item in routerLinkList"
|
||||
:key="item.name"
|
||||
:to="item.path"
|
||||
class="block rounded-md px-3 py-2 text-base font-medium"
|
||||
:class="{ 'bg-blue-50 text-blue-600 dark:bg-gray-800 dark:text-blue-400': route.path === item.path, 'text-gray-700 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-200 dark:hover:bg-gray-800 dark:hover:text-white': route.path !== item.path }"
|
||||
@click="handleMobileLinkClick"
|
||||
>
|
||||
{{ item.label }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- 移动端底部的登录/用户信息区域 -->
|
||||
<div class="border-t border-gray-200 pb-3 pt-4 dark:border-gray-700">
|
||||
<div class="px-2 space-y-2">
|
||||
<template v-if="!userStore.user">
|
||||
<button
|
||||
class="block w-full rounded-md px-3 py-2 text-left text-base text-gray-700 font-medium hover:bg-gray-50 dark:text-gray-200 hover:text-gray-900 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
@click="openLoginModal"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
<button
|
||||
class="block w-full rounded-md px-3 py-2 text-left text-base text-blue-600 font-medium hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-gray-800"
|
||||
>
|
||||
注册
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="mb-3 flex items-center px-3">
|
||||
<span class="border-r-2 border-gray-3 pr-2 text-sm text-gray-700 dark:text-gray-200">音乐类</span>
|
||||
<span class="border-r-2 border-gray-3 pl-2 pr-2 text-sm text-gray-700 dark:text-gray-200">文化成绩</span>
|
||||
<span class="pl-2 pr-2 text-sm text-gray-700 dark:text-gray-200">334</span>
|
||||
<a target="_blank" class="cursor-pointer text-gray-400 transition-colors hover:text-gray-500 dark:hover:text-gray-300" @click="openScoreFormModal">
|
||||
<div i-carbon:edit class="text-xs" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="mb-3 flex items-center px-3">
|
||||
<span>欢迎, {{ userStore.user.username }}</span>
|
||||
<button
|
||||
class="block rounded-md px-3 py-2 text-left text-base text-red-600 font-medium hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-800"
|
||||
@click="handleLogout"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Login Modal (Z-index 调高,确保覆盖导航栏) -->
|
||||
<div
|
||||
v-if="isLoginModalOpen"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50 px-4"
|
||||
>
|
||||
<!-- Modal Content -->
|
||||
<div class="max-w-md w-full border rounded-lg bg-white p-6 shadow-xl transition-all dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-xl text-gray-900 font-bold dark:text-white">
|
||||
登录
|
||||
</h2>
|
||||
<button
|
||||
class="text-2xl text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
@click="closeLoginModal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="mb-4 text-sm text-red-600 dark:text-red-400">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm text-gray-700 font-medium dark:text-gray-300">用户名</label>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="w-full border border-gray-300 rounded-md bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入用户名"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="mb-1 block text-sm text-gray-700 font-medium dark:text-gray-300">密码</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="w-full border border-gray-300 rounded-md bg-white px-3 py-2 text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入密码"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2 space-x-3">
|
||||
<button
|
||||
class="rounded-md bg-gray-200 px-4 py-2 text-sm text-gray-800 font-medium transition-colors dark:bg-gray-700 hover:bg-gray-300 dark:text-gray-200 dark:hover:bg-gray-600"
|
||||
@click="closeLoginModal"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm text-white font-medium transition-colors dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isScoreModalOpen" class="fixed inset-0 z-50 flex select-none items-center justify-center bg-black bg-opacity-50 px-4">
|
||||
<div class="max-w-md w-full border rounded-lg bg-white p-6 shadow-xl transition-all dark:border-gray-700 dark:bg-gray-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-xl text-gray-900 font-bold dark:text-white">
|
||||
修改你的高考信息
|
||||
</h3>
|
||||
<button
|
||||
class="text-3xl text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
|
||||
@click="closeScoreFormModal"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<!-- 使用组件并监听 confirm 事件 -->
|
||||
<ScoreForm @confirm="handleScoreFormConfirm" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
type?: 'success' | 'error' | 'warning' | 'info'
|
||||
message: string
|
||||
duration?: number
|
||||
onClose?: (id: string) => void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
onClose: () => {},
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
const timer = ref<any>(null)
|
||||
|
||||
// Icon mapping
|
||||
const icons = {
|
||||
success: 'i-carbon-checkmark-filled',
|
||||
error: 'i-carbon-close-filled',
|
||||
warning: 'i-carbon-warning-filled',
|
||||
info: 'i-carbon-information-filled',
|
||||
}
|
||||
|
||||
// Color mapping
|
||||
const colors = {
|
||||
success: 'text-green-500 bg-green-50 dark:bg-green-900/20 dark:text-green-400',
|
||||
error: 'text-red-500 bg-red-50 dark:bg-red-900/20 dark:text-red-400',
|
||||
warning: 'text-orange-500 bg-orange-50 dark:bg-orange-900/20 dark:text-orange-400',
|
||||
info: 'text-blue-500 bg-blue-50 dark:bg-blue-900/20 dark:text-blue-400',
|
||||
}
|
||||
|
||||
function startTimer() {
|
||||
if (props.duration > 0) {
|
||||
timer.value = setTimeout(() => {
|
||||
close()
|
||||
}, props.duration)
|
||||
}
|
||||
}
|
||||
|
||||
function clearTimer() {
|
||||
if (timer.value) {
|
||||
clearTimeout(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
// Give time for transition to finish before removing
|
||||
setTimeout(() => {
|
||||
// props.onClose(props.id)
|
||||
}, 300)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Small delay to trigger enter transition
|
||||
setTimeout(() => {
|
||||
visible.value = true
|
||||
}, 10)
|
||||
startTimer()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="pointer-events-auto mb-3 max-w-sm w-full flex overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-300 dark:bg-gray-800" :class="[visible ? 'translate-x-0 opacity-100' : 'translate-x-full opacity-0']" @mouseenter="clearTimer" @mouseleave="startTimer">
|
||||
<div class="p-4">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<div :class="[icons[type], colors[type]]" class="w-6' h-6" />
|
||||
</div>
|
||||
<div class="ml-3 flex-1 pt-0.5">
|
||||
<p class="text-sm text-gray-900 font-medium dark:text-gray-100">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="ml-4 flex flex-shrink-0">
|
||||
<!-- focus:outline-none focus:ring-2 focus:ring-offset-2 -->
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex rounded-md bg-white text-gray-400 dark:bg-gray-800 dark:text-gray-500 hover:text-gray-500 focus:ring-indigo-500 dark:hover:text-gray-400"
|
||||
@click="close"
|
||||
>
|
||||
<span class="sr-only">Close</span>
|
||||
<div class="i-carbon-close h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import { inject, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps<{
|
||||
label: string
|
||||
value: string | number
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
// 注入父组件的方法
|
||||
// 注意:这里需要和 WSelect 里的 provide key 保持一致
|
||||
const selectContext = inject<{
|
||||
registerOption: (opt: any) => void
|
||||
unregisterOption: (val: string | number) => void
|
||||
}>('w-select-context')
|
||||
|
||||
if (!selectContext) {
|
||||
console.warn('WOption must be used inside WSelect')
|
||||
}
|
||||
|
||||
// 构建选项对象
|
||||
const optionData = {
|
||||
label: props.label,
|
||||
value: props.value,
|
||||
disabled: props.disabled,
|
||||
}
|
||||
|
||||
// 1. 组件挂载时,向父组件注册自己
|
||||
onMounted(() => {
|
||||
selectContext?.registerOption(optionData)
|
||||
})
|
||||
|
||||
// 2. 组件卸载时,通知父组件移除自己
|
||||
onUnmounted(() => {
|
||||
selectContext?.unregisterOption(props.value)
|
||||
})
|
||||
|
||||
// 3. (可选) 如果 props 变了,也可以更新(这里简化处理,假设 value 不变)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="display: none;" />
|
||||
</template>
|
||||
|
|
@ -0,0 +1,251 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
// --- 类型定义 ---
|
||||
type Placement =
|
||||
| 'top' | 'top-left' | 'top-right'
|
||||
| 'bottom' | 'bottom-left' | 'bottom-right'
|
||||
| 'left' | 'left-top' | 'left-bottom'
|
||||
| 'right' | 'right-top' | 'right-bottom'
|
||||
|
||||
interface ButtonProps {
|
||||
size?: 'small' | 'medium'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
description?: string
|
||||
okText?: string
|
||||
cancelText?: string
|
||||
okType?: 'primary' | 'danger' | 'default'
|
||||
showCancel?: boolean
|
||||
icon?: string
|
||||
disabled?: boolean
|
||||
open?: boolean
|
||||
|
||||
// 新增位置参数
|
||||
placement?: Placement
|
||||
arrowPointAtCenter?: boolean
|
||||
|
||||
okButtonProps?: ButtonProps
|
||||
cancelButtonProps?: ButtonProps
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
description: '',
|
||||
okText: '确定',
|
||||
cancelText: '取消',
|
||||
okType: 'primary',
|
||||
showCancel: true,
|
||||
disabled: false,
|
||||
open: undefined,
|
||||
placement: 'top', // 默认上方
|
||||
arrowPointAtCenter: false, // 默认不强制居中
|
||||
okButtonProps: () => ({}),
|
||||
cancelButtonProps: () => ({}),
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:open', visible: boolean): void
|
||||
(e: 'confirm', ev: MouseEvent): void
|
||||
(e: 'cancel', ev: MouseEvent): void
|
||||
(e: 'openChange', visible: boolean): void
|
||||
}>()
|
||||
|
||||
// --- 状态与逻辑 ---
|
||||
const internalVisible = ref(false)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const isVisible = computed({
|
||||
get: () => props.open !== undefined ? props.open : internalVisible.value,
|
||||
set: (val) => {
|
||||
internalVisible.value = val
|
||||
emit('update:open', val)
|
||||
emit('openChange', val)
|
||||
},
|
||||
})
|
||||
|
||||
function handleTriggerClick() {
|
||||
if (props.disabled)
|
||||
return
|
||||
isVisible.value = !isVisible.value
|
||||
}
|
||||
|
||||
function handleCancel(e: MouseEvent) {
|
||||
isVisible.value = false
|
||||
emit('cancel', e)
|
||||
}
|
||||
|
||||
function handleConfirm(e: MouseEvent) {
|
||||
isVisible.value = false
|
||||
emit('confirm', e)
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (!isVisible.value)
|
||||
return
|
||||
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
|
||||
isVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('click', handleClickOutside))
|
||||
onUnmounted(() => document.removeEventListener('click', handleClickOutside))
|
||||
|
||||
// --- 核心:位置样式计算系统 ---
|
||||
|
||||
// 1. 气泡容器的位置 (相对于 Trigger)
|
||||
const overlayPositionClass = computed(() => {
|
||||
const map: Record<Placement, string> = {
|
||||
'top': 'bottom-full left-1/2 -translate-x-1/2 mb-2.5',
|
||||
'top-left': 'bottom-full left-0 mb-2.5',
|
||||
'top-right': 'bottom-full right-0 mb-2.5',
|
||||
|
||||
'bottom': 'top-full left-1/2 -translate-x-1/2 mt-2.5',
|
||||
'bottom-left': 'top-full left-0 mt-2.5',
|
||||
'bottom-right': 'top-full right-0 mt-2.5',
|
||||
|
||||
'left': 'right-full top-1/2 -translate-y-1/2 mr-2.5',
|
||||
'left-top': 'right-full top-0 mr-2.5',
|
||||
'left-bottom': 'right-full bottom-0 mr-2.5',
|
||||
|
||||
'right': 'left-full top-1/2 -translate-y-1/2 ml-2.5',
|
||||
'right-top': 'left-full top-0 ml-2.5',
|
||||
'right-bottom': 'left-full bottom-0 ml-2.5',
|
||||
}
|
||||
return map[props.placement] || map.top
|
||||
})
|
||||
|
||||
// 2. 箭头的位置与旋转 (相对于 Popover)
|
||||
const arrowPositionClass = computed(() => {
|
||||
const base = 'absolute h-3 w-3 bg-white border-slate-200 z-[-1]' // z-index -1 保证在阴影下方,但需要父级不遮挡
|
||||
|
||||
// 基础偏移量
|
||||
// const centerOffset = props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : ''
|
||||
// 如果不是 PointAtCenter,根据 Left/Right 微调箭头位置,避免靠太边
|
||||
const hAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('left') ? 'left-4' : props.placement.includes('right') ? 'right-4' : '')
|
||||
const vAlign = props.arrowPointAtCenter ? '' : (props.placement.includes('top') ? 'top-3' : props.placement.includes('bottom') ? 'bottom-3' : '')
|
||||
|
||||
// 12个方向的箭头逻辑
|
||||
// 核心原理:Top 气泡,箭头在 Bottom,边框是 右下 (旋转45度)
|
||||
if (props.placement.startsWith('top')) {
|
||||
return `${base} -bottom-1.5 border-b border-r rotate-45 ${props.placement === 'top' || props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : hAlign}`
|
||||
}
|
||||
if (props.placement.startsWith('bottom')) {
|
||||
return `${base} -top-1.5 border-t border-l rotate-45 ${props.placement === 'bottom' || props.arrowPointAtCenter ? 'left-1/2 -translate-x-1/2' : hAlign}`
|
||||
}
|
||||
if (props.placement.startsWith('left')) {
|
||||
return `${base} -right-1.5 border-t border-r rotate-45 ${props.placement === 'left' || props.arrowPointAtCenter ? 'top-1/2 -translate-y-1/2' : vAlign}`
|
||||
}
|
||||
if (props.placement.startsWith('right')) {
|
||||
return `${base} -left-1.5 border-b border-l rotate-45 ${props.placement === 'right' || props.arrowPointAtCenter ? 'top-1/2 -translate-y-1/2' : vAlign}`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
// 3. 过渡动画方向 (Transform Origin)
|
||||
const transitionOriginClass = computed(() => {
|
||||
if (props.placement.startsWith('top'))
|
||||
return 'origin-bottom'
|
||||
if (props.placement.startsWith('bottom'))
|
||||
return 'origin-top'
|
||||
if (props.placement.startsWith('left'))
|
||||
return 'origin-right'
|
||||
if (props.placement.startsWith('right'))
|
||||
return 'origin-left'
|
||||
return 'origin-center'
|
||||
})
|
||||
|
||||
// 按钮样式
|
||||
const okBtnClass = computed(() => {
|
||||
const base = 'rounded px-3 py-1 text-xs text-white transition-colors'
|
||||
switch (props.okType) {
|
||||
case 'danger': return `${base} bg-red-500 hover:bg-red-600 border border-red-500`
|
||||
case 'default': return 'rounded px-3 py-1 text-xs text-slate-600 border border-slate-200 hover:border-blue-400 hover:text-blue-500 transition-colors bg-white'
|
||||
default: return `${base} bg-blue-600 hover:bg-blue-700 border border-blue-600`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="relative inline-block">
|
||||
<!-- Trigger -->
|
||||
<div class="inline-block cursor-pointer" @click.stop="handleTriggerClick">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<!-- Popover -->
|
||||
<Transition
|
||||
enter-active-class="transition duration-200 ease-out"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition duration-150 ease-in"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="absolute z-50 w-45 cursor-default border border-slate-200 rounded-lg bg-white p-3 shadow-xl"
|
||||
:class="[overlayPositionClass, transitionOriginClass]"
|
||||
@click.stop
|
||||
>
|
||||
<!-- Arrow -->
|
||||
<div :class="arrowPositionClass" />
|
||||
|
||||
<!-- Content -->
|
||||
<div class="relative z-10 flex items-start gap-3">
|
||||
<div class="mt-0.5 flex-shrink-0">
|
||||
<slot name="icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-orange-500" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</slot>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="break-words text-sm text-slate-800 font-bold">
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</div>
|
||||
<div v-if="description || $slots.description" class="mt-1 break-words text-xs text-slate-500">
|
||||
<slot name="description">
|
||||
{{ description }}
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Buttons -->
|
||||
<div class="relative z-10 mt-2 flex justify-end gap-2">
|
||||
<slot name="cancelButton">
|
||||
<button
|
||||
v-if="showCancel"
|
||||
class="rounded px-2 py-1 text-xs text-slate-500 transition-colors hover:bg-slate-100"
|
||||
:disabled="cancelButtonProps.disabled"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<slot name="cancelText">
|
||||
{{ cancelText }}
|
||||
</slot>
|
||||
</button>
|
||||
</slot>
|
||||
|
||||
<slot name="okButton">
|
||||
<button
|
||||
:class="okBtnClass"
|
||||
:disabled="okButtonProps.disabled || okButtonProps.loading"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
<span v-if="okButtonProps.loading" class="mr-1 inline-block h-3 w-3 animate-spin border-2 border-white/30 border-t-white rounded-full" />
|
||||
<slot name="okText">
|
||||
{{ okText }}
|
||||
</slot>
|
||||
</button>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
<!-- WRadioButton.vue -->
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, onMounted, onUnmounted } from 'vue'
|
||||
|
||||
interface Props {
|
||||
value: string | number
|
||||
label: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// 从父组件获取上下文
|
||||
const radioGroupContext: any = inject('radioGroupContext')
|
||||
|
||||
// 使用计算属性获取当前选中值
|
||||
const modelValue = computed(() => {
|
||||
return radioGroupContext?.modelValue?.value
|
||||
})
|
||||
|
||||
function handleChange() {
|
||||
if (radioGroupContext?.updateValue) {
|
||||
radioGroupContext.updateValue(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 计算样式类,根据按钮在组中的位置
|
||||
const positionClass = computed(() => {
|
||||
if (!radioGroupContext || !radioGroupContext.getOptionIndex)
|
||||
return 'rounded-md'
|
||||
|
||||
const index = radioGroupContext.getOptionIndex(props.value)
|
||||
const totalCount = radioGroupContext.getOptionCount()
|
||||
|
||||
if (totalCount === 1)
|
||||
return 'rounded-md border-x'
|
||||
|
||||
if (index === 0) {
|
||||
return 'rounded-l-md border-r-0'
|
||||
}
|
||||
else if (index === totalCount - 1) {
|
||||
return 'rounded-r-md border-l-0'
|
||||
}
|
||||
else {
|
||||
return 'border-l-0 border-r-0'
|
||||
}
|
||||
})
|
||||
|
||||
const sizeClasses = computed(() => {
|
||||
// 根据不同尺寸返回不同的 padding
|
||||
return 'px-4 py-2 border'
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 注册选项到父组件
|
||||
if (radioGroupContext?.registerOption) {
|
||||
radioGroupContext.registerOption({
|
||||
value: props.value,
|
||||
label: props.label,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 注销选项从父组件
|
||||
if (radioGroupContext?.unregisterOption) {
|
||||
radioGroupContext.unregisterOption(props.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<input
|
||||
:id="`radio-${value}`"
|
||||
type="radio"
|
||||
:value="value"
|
||||
:checked="modelValue === value"
|
||||
class="peer absolute h-0 w-0 opacity-0"
|
||||
@change="handleChange"
|
||||
>
|
||||
<label
|
||||
:for="`radio-${value}`"
|
||||
class="flex cursor-pointer items-center justify-center transition-colors duration-150 ease-in-out"
|
||||
:class="[
|
||||
sizeClasses,
|
||||
modelValue === value
|
||||
? 'bg-blue-600 text-white border-blue-600 z-10'
|
||||
: 'bg-white dark:bg-gray-300 text-gray-700 border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-200',
|
||||
positionClass,
|
||||
]"
|
||||
>
|
||||
<slot>{{ label }}</slot>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<!-- eslint-disable no-console -->
|
||||
<!-- WRadioGroup.vue -->
|
||||
<script setup lang="ts">
|
||||
import { provide, ref, watch } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: string | number
|
||||
size?: 'large' | 'middle' | 'small'
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: string | number): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'middle',
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const selectedValue = ref(props.modelValue)
|
||||
const options = ref<Array<{ value: string | number, label: string }>>([])
|
||||
|
||||
// 提供上下文给子组件
|
||||
provide('radioGroupContext', {
|
||||
modelValue: selectedValue,
|
||||
updateValue: (value: string | number) => {
|
||||
selectedValue.value = value
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
registerOption: (option: { value: string | number, label: string }) => {
|
||||
options.value.push(option)
|
||||
},
|
||||
unregisterOption: (value: string | number) => {
|
||||
const index = options.value.findIndex(opt => opt.value === value)
|
||||
if (index > -1) {
|
||||
options.value.splice(index, 1)
|
||||
}
|
||||
},
|
||||
getOptionIndex: (value: string | number) => {
|
||||
return options.value.findIndex(opt => opt.value === value)
|
||||
},
|
||||
getOptionCount: () => {
|
||||
return options.value.length
|
||||
},
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
selectedValue.value = newValue
|
||||
})
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
console.log(props.size)
|
||||
switch (props.size) {
|
||||
case 'large':
|
||||
return 'px-6 py-3'
|
||||
case 'middle':
|
||||
return 'px-4 py-2'
|
||||
case 'small':
|
||||
return 'text-sm'
|
||||
default:
|
||||
return 'px-4 py-2'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-radio-group flex select-none overflow-hidden rounded-md shadow-sm" :class="[sizeClass]">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.w-radio-group {
|
||||
/* 样式已通过 Tailwind 类定义,在这里不需要额外样式 */
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
// --- Types ---
|
||||
export interface OptionItem {
|
||||
label: string
|
||||
value: string | number
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
// --- Props ---
|
||||
const props = withDefaults(defineProps<{
|
||||
// 支持 v-model:value
|
||||
value?: string | number | (string | number)[]
|
||||
options?: OptionItem[]
|
||||
mode?: 'single' | 'multiple'
|
||||
placeholder?: string
|
||||
disabled?: boolean
|
||||
maxTagCount?: number // 限制 Input 显示多少个 tag
|
||||
maxSelectCount?: number // 限制最多选多少个
|
||||
}>(), {
|
||||
value: undefined,
|
||||
options: () => [],
|
||||
mode: 'single',
|
||||
placeholder: 'Select Item...',
|
||||
disabled: false,
|
||||
maxTagCount: undefined,
|
||||
maxSelectCount: undefined,
|
||||
})
|
||||
|
||||
// --- Emits ---
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:value', val: string | number | (string | number)[] | undefined): void
|
||||
(e: 'change', val: string | number | (string | number)[] | undefined): void
|
||||
}>()
|
||||
|
||||
// --- State ---
|
||||
const isOpen = ref(false)
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// --- Computed Helper ---
|
||||
const isMultiple = computed(() => props.mode === 'multiple')
|
||||
|
||||
// 当前选中的值(统一转为数组处理逻辑,如果是单选则是数组第一个)
|
||||
const internalValue = computed(() => {
|
||||
return props.value
|
||||
})
|
||||
|
||||
const isEmpty = computed(() => {
|
||||
if (isMultiple.value) {
|
||||
return !Array.isArray(props.value) || props.value.length === 0
|
||||
}
|
||||
return props.value === undefined || props.value === null || props.value === ''
|
||||
})
|
||||
|
||||
// 获取选中项的完整对象数组(用于显示 Tag)
|
||||
const selectedOptions = computed(() => {
|
||||
if (!isMultiple.value)
|
||||
return []
|
||||
const vals = Array.isArray(props.value) ? props.value : []
|
||||
// 按照值在 options 中找到对应的 label
|
||||
return vals.map(val => props.options.find(opt => opt.value === val) || { label: String(val), value: val })
|
||||
})
|
||||
|
||||
// 计算显示的 Tags (Max Tag Count Logic)
|
||||
const visibleTags = computed(() => {
|
||||
if (!props.maxTagCount)
|
||||
return selectedOptions.value
|
||||
return selectedOptions.value.slice(0, props.maxTagCount)
|
||||
})
|
||||
|
||||
// 计算被隐藏的 Tags 数量
|
||||
const hiddenTagCount = computed(() => {
|
||||
if (!props.maxTagCount)
|
||||
return 0
|
||||
return Math.max(0, selectedOptions.value.length - props.maxTagCount)
|
||||
})
|
||||
|
||||
// --- Actions ---
|
||||
|
||||
function toggleDropdown() {
|
||||
if (props.disabled)
|
||||
return
|
||||
isOpen.value = !isOpen.value
|
||||
}
|
||||
|
||||
function closeDropdown() {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
function getLabel(val: string | number | undefined) {
|
||||
const opt = props.options.find(o => o.value === val)
|
||||
return opt ? opt.label : val
|
||||
}
|
||||
|
||||
function isSelected(val: string | number) {
|
||||
if (isMultiple.value) {
|
||||
return Array.isArray(props.value) && props.value.includes(val)
|
||||
}
|
||||
return props.value === val
|
||||
}
|
||||
|
||||
// 检查是否达到最大选择限制
|
||||
const isMaxLimitReached = computed(() => {
|
||||
if (!isMultiple.value || !props.maxSelectCount)
|
||||
return false
|
||||
const currentLen = Array.isArray(props.value) ? props.value.length : 0
|
||||
return currentLen >= props.maxSelectCount
|
||||
})
|
||||
|
||||
// 判断选项是否禁用(原生禁用 或 达到最大限制且未选中该项)
|
||||
function isDisabledOption(option: OptionItem) {
|
||||
if (option.disabled)
|
||||
return true
|
||||
// 如果已经达到最大限制,并且当前项没有被选中,则禁用
|
||||
if (isMultiple.value && isMaxLimitReached.value && !isSelected(option.value)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function handleSelect(option: OptionItem) {
|
||||
if (isDisabledOption(option))
|
||||
return
|
||||
|
||||
if (isMultiple.value) {
|
||||
// 多选逻辑
|
||||
const currentList = Array.isArray(props.value) ? [...props.value] : []
|
||||
const index = currentList.indexOf(option.value)
|
||||
|
||||
if (index > -1) {
|
||||
// 取消选择
|
||||
currentList.splice(index, 1)
|
||||
}
|
||||
else {
|
||||
// 选择 (需要再次检查 Limit,双重保险)
|
||||
if (props.maxSelectCount && currentList.length >= props.maxSelectCount)
|
||||
return
|
||||
currentList.push(option.value)
|
||||
}
|
||||
emit('update:value', currentList)
|
||||
emit('change', currentList)
|
||||
// 多选模式下通常不自动关闭下拉框,方便继续选
|
||||
}
|
||||
else {
|
||||
// 单选逻辑
|
||||
if (props.value !== option.value) {
|
||||
emit('update:value', option.value)
|
||||
emit('change', option.value)
|
||||
}
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
function removeTag(val: string | number) {
|
||||
if (!isMultiple.value)
|
||||
return
|
||||
const currentList = Array.isArray(props.value) ? [...props.value] : []
|
||||
const index = currentList.indexOf(val)
|
||||
if (index > -1) {
|
||||
currentList.splice(index, 1)
|
||||
emit('update:value', currentList)
|
||||
emit('change', currentList)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Click Outside Logic ---
|
||||
// 简单的点击外部关闭逻辑
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (containerRef.value && !containerRef.value.contains(event.target as Node)) {
|
||||
closeDropdown()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="relative w-full">
|
||||
<div
|
||||
class="min-h-[40px] w-full flex cursor-pointer items-center justify-between border rounded-md bg-white px-3 py-1.5 transition-colors"
|
||||
:class="[
|
||||
isOpen ? 'border-blue-500 ring-1 ring-blue-500' : 'border-gray-300 hover:border-blue-400',
|
||||
disabled ? 'bg-gray-100 cursor-not-allowed opacity-75' : '',
|
||||
]"
|
||||
@click="toggleDropdown"
|
||||
>
|
||||
<div class="flex flex-1 flex-wrap items-center gap-2 overflow-hidden">
|
||||
<span v-if="isEmpty" class="select-none text-gray-400">{{ placeholder }}</span>
|
||||
|
||||
<span v-else-if="!isMultiple" class="select-none text-sm text-gray-700">
|
||||
{{ getLabel(Array.isArray(internalValue) ? internalValue[0] : internalValue) }}
|
||||
</span>
|
||||
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="item in visibleTags"
|
||||
:key="item.value"
|
||||
class="flex items-center gap-1 border border-gray-200 rounded bg-gray-100 px-2 py-0.5 text-sm text-gray-700"
|
||||
>
|
||||
<span>{{ item.label }}</span>
|
||||
<button
|
||||
class="hover:text-red-500 focus:outline-none"
|
||||
@click.stop="removeTag(item.value)"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="hiddenTagCount > 0" class="border border-gray-200 rounded bg-gray-100 px-2 py-0.5 text-sm text-gray-500">
|
||||
+{{ hiddenTagCount }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="text-gray-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 transition-transform duration-200"
|
||||
:class="{ 'rotate-180': isOpen }"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<transition
|
||||
enter-active-class="transition ease-out duration-100"
|
||||
enter-from-class="transform opacity-0 scale-95"
|
||||
enter-to-class="transform opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="transform opacity-100 scale-100"
|
||||
leave-to-class="transform opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="absolute z-50 mt-1 max-h-60 w-full overflow-y-auto border border-gray-200 rounded-md bg-white shadow-lg"
|
||||
>
|
||||
<ul class="py-1">
|
||||
<li v-if="options.length === 0" class="px-4 py-2 text-center text-sm text-gray-400">
|
||||
No data
|
||||
</li>
|
||||
|
||||
<li
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
class="group flex cursor-pointer items-center justify-between py-2 pl-3 pr-2 text-sm transition-colors"
|
||||
:class="[
|
||||
isSelected(option.value) ? 'bg-blue-50 text-blue-600 font-medium' : 'text-gray-700 hover:bg-gray-100',
|
||||
isDisabledOption(option) ? 'opacity-50 cursor-not-allowed bg-gray-50' : '',
|
||||
]"
|
||||
@click="handleSelect(option)"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
|
||||
<span v-if="isSelected(option.value)" class="text-blue-600">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
// these APIs are auto-imported from @vueuse/core
|
||||
export const isDark = useDark()
|
||||
export const toggleDark = useToggle(isDark)
|
||||
export const preferredDark = usePreferredDark()
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
// 定义请求配置类型
|
||||
export interface RequestConfig extends RequestInit {
|
||||
// 请求URL
|
||||
url?: string
|
||||
// 自动拼接的URL前缀
|
||||
baseURL?: string
|
||||
// 可以添加自定义配置
|
||||
showLoading?: boolean
|
||||
showError?: boolean
|
||||
// 请求参数
|
||||
params?: Record<string, any>
|
||||
}
|
||||
|
||||
// 定义响应类型
|
||||
export interface RequestResponse<T = any> {
|
||||
code: number
|
||||
message: string
|
||||
data: T
|
||||
}
|
||||
|
||||
// 创建请求方法
|
||||
async function request<T = any>(config: RequestConfig): Promise<T> {
|
||||
const {
|
||||
url,
|
||||
baseURL = import.meta.env.VITE_API_BASE_URL || '',
|
||||
showLoading = true,
|
||||
showError = true,
|
||||
params,
|
||||
...init
|
||||
} = config
|
||||
|
||||
// 拼接完整URL
|
||||
let fullUrl = ''
|
||||
if (url?.startsWith('http')) {
|
||||
fullUrl = url
|
||||
}
|
||||
else {
|
||||
fullUrl = baseURL + (url || '')
|
||||
}
|
||||
|
||||
// 处理GET请求参数
|
||||
if (params && init.method?.toUpperCase() === 'GET') {
|
||||
const searchParams = new URLSearchParams()
|
||||
for (const key in params) {
|
||||
if (params[key] !== undefined && params[key] !== null) {
|
||||
searchParams.append(key, params[key].toString())
|
||||
}
|
||||
}
|
||||
const paramsString = searchParams.toString()
|
||||
if (paramsString) {
|
||||
fullUrl += (fullUrl.includes('?') ? '&' : '?') + paramsString
|
||||
}
|
||||
}
|
||||
|
||||
// 可以添加请求加载动画
|
||||
if (showLoading) {
|
||||
// 显示加载动画
|
||||
}
|
||||
|
||||
try {
|
||||
// 发送请求
|
||||
const response = await fetch(fullUrl, init)
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
const responseData = await response.json() as RequestResponse<T>
|
||||
|
||||
// 隐藏加载动画
|
||||
if (showLoading) {
|
||||
// 隐藏加载动画
|
||||
}
|
||||
|
||||
// 统一处理响应数据
|
||||
const { code, message, data } = responseData
|
||||
|
||||
if (code === 200) {
|
||||
return data
|
||||
}
|
||||
else {
|
||||
// 统一处理错误信息
|
||||
if (showError) {
|
||||
console.error('Request error:', message)
|
||||
}
|
||||
throw new Error(message || 'Request failed')
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
// 隐藏加载动画
|
||||
if (showLoading) {
|
||||
// 隐藏加载动画
|
||||
}
|
||||
|
||||
// 统一处理网络错误
|
||||
const errorMessage = error instanceof Error ? error.message : 'Network error'
|
||||
if (showError) {
|
||||
console.error('Network error:', errorMessage)
|
||||
}
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
}
|
||||
|
||||
// 封装GET请求
|
||||
function get<T = any>(url: string, params?: Record<string, any>, config?: Omit<RequestConfig, 'url' | 'method' | 'params'>): Promise<T> {
|
||||
return request<T>({
|
||||
method: 'GET',
|
||||
url,
|
||||
params,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
// 封装POST请求
|
||||
function post<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'body'>): Promise<T> {
|
||||
return request<T>({
|
||||
method: 'POST',
|
||||
url,
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...config?.headers,
|
||||
},
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
// 封装PUT请求
|
||||
function put<T = any>(url: string, data?: any, config?: Omit<RequestConfig, 'url' | 'method' | 'body'>): Promise<T> {
|
||||
return request<T>({
|
||||
method: 'PUT',
|
||||
url,
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...config?.headers,
|
||||
},
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
// 封装DELETE请求
|
||||
function del<T = any>(url: string, config?: Omit<RequestConfig, 'url' | 'method'>): Promise<T> {
|
||||
return request<T>({
|
||||
method: 'DELETE',
|
||||
url,
|
||||
...config,
|
||||
})
|
||||
}
|
||||
|
||||
// 请求取消机制 (使用AbortController)
|
||||
class CancelToken {
|
||||
private controller: AbortController
|
||||
|
||||
constructor() {
|
||||
this.controller = new AbortController()
|
||||
}
|
||||
|
||||
get signal() {
|
||||
return this.controller.signal
|
||||
}
|
||||
|
||||
cancel(reason?: string) {
|
||||
this.controller.abort(reason)
|
||||
}
|
||||
}
|
||||
|
||||
function createCancelToken() {
|
||||
return new CancelToken()
|
||||
}
|
||||
|
||||
// 导出所有方法
|
||||
export {
|
||||
CancelToken,
|
||||
createCancelToken,
|
||||
del,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
request,
|
||||
}
|
||||
|
||||
// 默认导出
|
||||
export default request
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
useHead({
|
||||
title: () => t('not-found'),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main p="x4 y10" text="center teal-700 dark:gray-200">
|
||||
<div text-4xl>
|
||||
<div i-carbon-warning inline-block />
|
||||
</div>
|
||||
<RouterView />
|
||||
<div>
|
||||
<button text-sm btn m="3 t8" @click="router.back()">
|
||||
{{ t('button.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
## Layouts
|
||||
|
||||
Vue components in this dir are used as layouts.
|
||||
|
||||
By default, `default.vue` will be used unless an alternative is specified in the route meta.
|
||||
|
||||
With [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) and [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts), you can specify the layout in the page's SFCs like this:
|
||||
|
||||
```vue
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: home
|
||||
</route>
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-700 dark:bg-gray-900 dark:text-gray-200">
|
||||
<TheNavigation />
|
||||
<main class="flex-grow px-4 py-10 pt-16 text-center">
|
||||
<RouterView />
|
||||
</main>
|
||||
<TheFooter />
|
||||
<BackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-gray-50 text-gray-700 dark:bg-gray-900 dark:text-gray-200">
|
||||
<TheNavigation />
|
||||
<main class="flex-grow pt-16">
|
||||
<RouterView />
|
||||
</main>
|
||||
<TheFooter />
|
||||
<BackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import type { UserModule } from './types'
|
||||
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
import { ViteSSG } from 'vite-ssg'
|
||||
import { routes } from 'vue-router/auto-routes'
|
||||
import App from './App.vue'
|
||||
|
||||
import '@unocss/reset/tailwind.css'
|
||||
import './styles/main.css'
|
||||
import 'uno.css'
|
||||
|
||||
// https://github.com/antfu/vite-ssg
|
||||
export const createApp = ViteSSG(
|
||||
App,
|
||||
{
|
||||
routes: setupLayouts(routes),
|
||||
base: import.meta.env.BASE_URL,
|
||||
},
|
||||
(ctx) => {
|
||||
// install all modules under `modules/`
|
||||
Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true }))
|
||||
.forEach(i => i.install?.(ctx))
|
||||
// ctx.app.use(Previewer)
|
||||
},
|
||||
)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
## Modules
|
||||
|
||||
A custom user module system. Place a `.ts` file with the following template, it will be installed automatically.
|
||||
|
||||
```ts
|
||||
import type { UserModule } from '~/types'
|
||||
|
||||
export const install: UserModule = ({ app, router, isClient }) => {
|
||||
// do something
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import type { Locale } from 'vue-i18n'
|
||||
import type { UserModule } from '~/types'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
// Import i18n resources
|
||||
// https://vitejs.dev/guide/features.html#glob-import
|
||||
//
|
||||
// Don't need this? Try vitesse-lite: https://github.com/antfu/vitesse-lite
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: '',
|
||||
messages: {},
|
||||
})
|
||||
|
||||
const localesMap = Object.fromEntries(
|
||||
Object.entries(import.meta.glob('../../locales/*.yml'))
|
||||
.map(([path, loadLocale]) => [path.match(/([\w-]*)\.yml$/)?.[1], loadLocale]),
|
||||
) as Record<Locale, () => Promise<{ default: Record<string, string> }>>
|
||||
|
||||
export const availableLocales = Object.keys(localesMap)
|
||||
|
||||
const loadedLanguages: string[] = []
|
||||
|
||||
function setI18nLanguage(lang: Locale) {
|
||||
i18n.global.locale.value = lang as any
|
||||
if (typeof document !== 'undefined')
|
||||
document.querySelector('html')?.setAttribute('lang', lang)
|
||||
return lang
|
||||
}
|
||||
|
||||
export async function loadLanguageAsync(lang: string): Promise<Locale> {
|
||||
// If the same language
|
||||
if (i18n.global.locale.value === lang)
|
||||
return setI18nLanguage(lang)
|
||||
|
||||
// If the language was already loaded
|
||||
if (loadedLanguages.includes(lang))
|
||||
return setI18nLanguage(lang)
|
||||
|
||||
// If the language hasn't been loaded yet
|
||||
const messages = await localesMap[lang]()
|
||||
i18n.global.setLocaleMessage(lang, messages.default)
|
||||
loadedLanguages.push(lang)
|
||||
return setI18nLanguage(lang)
|
||||
}
|
||||
|
||||
export const install: UserModule = ({ app }) => {
|
||||
app.use(i18n)
|
||||
loadLanguageAsync('en')
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { UserModule } from '~/types'
|
||||
import NProgress from 'nprogress'
|
||||
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (isClient) {
|
||||
router.beforeEach((to, from) => {
|
||||
if (to.path !== from.path)
|
||||
NProgress.start()
|
||||
})
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import type { UserModule } from '~/types'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
// Setup Pinia
|
||||
// https://pinia.vuejs.org/
|
||||
export const install: UserModule = ({ isClient, initialState, app }) => {
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
// Refer to
|
||||
// https://github.com/antfu/vite-ssg/blob/main/README.md#state-serialization
|
||||
// for other serialization strategies.
|
||||
if (isClient)
|
||||
pinia.state.value = (initialState.pinia) || {}
|
||||
|
||||
else
|
||||
initialState.pinia = pinia.state.value
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import type { UserModule } from '~/types'
|
||||
|
||||
// https://github.com/antfu/vite-plugin-pwa#automatic-reload-when-new-content-available
|
||||
export const install: UserModule = ({ isClient, router }) => {
|
||||
if (!isClient)
|
||||
return
|
||||
|
||||
router.isReady()
|
||||
.then(async () => {
|
||||
const { registerSW } = await import('virtual:pwa-register')
|
||||
registerSW({ immediate: true })
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
## File-based Routing
|
||||
|
||||
Routes will be auto-generated for Vue files in this dir with the same file structure.
|
||||
Check out [`unplugin-vue-router`](https://github.com/posva/unplugin-vue-router) for more details.
|
||||
|
||||
### Path Aliasing
|
||||
|
||||
`~/` is aliased to `./src/` folder.
|
||||
|
||||
For example, instead of having
|
||||
|
||||
```ts
|
||||
import { isDark } from '../../../../composables'
|
||||
```
|
||||
|
||||
now, you can use
|
||||
|
||||
```ts
|
||||
import { isDark } from '~/composables'
|
||||
```
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<script setup lang="ts">
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
{{ t('not-found') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: 404
|
||||
</route>
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
title: About
|
||||
---
|
||||
|
||||
<script setup>
|
||||
const { t } = useI18n()
|
||||
useHead({ title: () => t('button.about') })
|
||||
</script>
|
||||
|
||||
<div class="text-center">
|
||||
<!-- You can use Vue components inside markdown -->
|
||||
<div i-carbon-dicom-overlay class="text-4xl -mb-6 m-auto" />
|
||||
<h3>{{ t('button.about') }}</h3>
|
||||
</div>
|
||||
|
||||
[Vitesse](https://github.com/antfu/vitesse) is an opinionated [Vite](https://github.com/vitejs/vite) starter template made by [@antfu](https://github.com/antfu) for mocking apps swiftly. With **file-based routing**, **components auto importing**, **markdown support**, I18n, PWA and uses **UnoCSS** for styling and icons.
|
||||
|
||||
```js
|
||||
// syntax highlighting example
|
||||
function vitesse() {
|
||||
const foo = 'bar'
|
||||
console.log(foo)
|
||||
}
|
||||
```
|
||||
|
||||
Check out the [GitHub repo](https://github.com/antfu/vitesse) for more details.
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
<script setup lang="ts">
|
||||
import { useHead } from '@unhead/vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'AboutPage',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: '关于艺体志愿宝平台 - 艺体志愿宝',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: '了解艺体志愿宝平台的创立背景、服务宗旨、核心功能以及联系方式',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Team members data
|
||||
const teamMembers = [
|
||||
{
|
||||
id: 1,
|
||||
name: '张三',
|
||||
position: '创始人兼CEO',
|
||||
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
bio: '毕业于清华大学计算机系,拥有10年互联网行业经验,专注于教育科技领域。',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '李四',
|
||||
position: '技术总监',
|
||||
avatar: 'https://images.unsplash.com/photo-1500648767291-0a08a98c3703?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2071&q=80',
|
||||
bio: '前BAT高级工程师,拥有8年前端开发经验,擅长Vue.js和React等现代前端框架。',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '王五',
|
||||
position: '产品经理',
|
||||
avatar: 'https://images.unsplash.com/photo-1522529599102-193c0d76b5b6?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2071&q=80',
|
||||
bio: '毕业于北京大学光华管理学院,拥有6年教育产品经验,深刻理解学生需求。',
|
||||
},
|
||||
]
|
||||
|
||||
// Timeline data
|
||||
const timeline = [
|
||||
{
|
||||
year: '2022.03',
|
||||
title: '公司成立',
|
||||
description: '艺体志愿宝正式成立,专注于艺术体育类志愿填报辅助平台的开发。',
|
||||
},
|
||||
{
|
||||
year: '2022.12',
|
||||
title: '产品上线',
|
||||
description: '艺体志愿宝1.0版本正式上线,支持全国各大艺术体育类院校的数据查询。',
|
||||
},
|
||||
{
|
||||
year: '2023.06',
|
||||
title: '功能升级',
|
||||
description: '推出AI智能匹配系统,为用户提供个性化的志愿填报方案。',
|
||||
},
|
||||
{
|
||||
year: '2023.12',
|
||||
title: '用户突破',
|
||||
description: '平台用户突破10万,服务覆盖全国20多个省份。',
|
||||
},
|
||||
{
|
||||
year: '2024.09',
|
||||
title: '技术创新',
|
||||
description: '优化大数据算法,提高志愿匹配准确率至98%。',
|
||||
},
|
||||
{
|
||||
year: '2025.01',
|
||||
title: '服务升级',
|
||||
description: '新增在线客服系统和专家咨询服务,提升用户体验。',
|
||||
},
|
||||
]
|
||||
|
||||
// Select component state
|
||||
const selectedValue = ref<string>('platform-intro')
|
||||
|
||||
// Smooth scroll to section
|
||||
function scrollToSection(section: string) {
|
||||
const element = document.getElementById(section)
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth scroll behavior
|
||||
onMounted(() => {
|
||||
document.documentElement.style.scrollBehavior = 'smooth'
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.documentElement.style.scrollBehavior = 'auto'
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-7xl px-4 py-8 lg:px-8 sm:px-6">
|
||||
<!-- Hero Section -->
|
||||
<section class="mb-16 text-center">
|
||||
<h1 class="mb-4 text-4xl text-gray-900 font-bold sm:text-5xl dark:text-white">
|
||||
关于艺体志愿宝平台
|
||||
</h1>
|
||||
<p class="mx-auto max-w-3xl text-xl text-gray-600 dark:text-gray-300">
|
||||
艺体志愿宝是专为艺术体育类考生打造的志愿填报辅助平台,致力于为考生提供全方位的数据支持和个性化的志愿填报方案。
|
||||
</p>
|
||||
|
||||
<!-- Quick Navigation Select -->
|
||||
<div class="mx-auto mt-8 max-w-md">
|
||||
<select
|
||||
v-model="selectedValue"
|
||||
class="w-full border border-gray-300 rounded-lg bg-white px-4 py-3 text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@change="scrollToSection(selectedValue)"
|
||||
>
|
||||
<option value="platform-intro">
|
||||
平台介绍
|
||||
</option>
|
||||
<option value="development-history">
|
||||
发展历程
|
||||
</option>
|
||||
<option value="team-intro">
|
||||
团队介绍
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Platform Introduction Section -->
|
||||
<section id="platform-intro" class="mb-20">
|
||||
<h2 class="mb-8 text-2xl text-gray-900 font-bold sm:text-3xl dark:text-white">
|
||||
平台介绍
|
||||
</h2>
|
||||
|
||||
<!-- Founding Background -->
|
||||
<div class="mb-12">
|
||||
<h3 class="mb-4 text-xl text-gray-900 font-semibold dark:text-white">
|
||||
创立背景
|
||||
</h3>
|
||||
<p class="text-gray-600 leading-relaxed dark:text-gray-300">
|
||||
随着艺术体育类高考的不断发展,考生和家长面临着越来越复杂的志愿填报决策。传统的志愿填报方式往往依赖经验和有限的信息,容易导致误判和滑档。为了解决这一痛点,艺体志愿宝于2022年应运而生。我们汇聚了教育专家、数据分析师和技术团队,旨在为艺体考生提供科学、智能、精准的志愿填报辅助服务。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Service Purpose -->
|
||||
<div class="mb-12">
|
||||
<h3 class="mb-4 text-xl text-gray-900 font-semibold dark:text-white">
|
||||
服务宗旨
|
||||
</h3>
|
||||
<p class="text-gray-600 leading-relaxed dark:text-gray-300">
|
||||
我们的服务宗旨是"以考生为中心,以数据为依据,以服务为宗旨"。我们致力于为每一位艺体考生提供个性化的志愿填报方案,帮助他们找到最适合自己的大学和专业。我们的目标是让志愿填报变得简单、科学、高效,让每一位考生都能实现自己的大学梦想。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Core Features -->
|
||||
<div class="mb-12">
|
||||
<h3 class="mb-4 text-xl text-gray-900 font-semibold dark:text-white">
|
||||
核心功能
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<div class="rounded-xl bg-white p-5 shadow-lg transition-all duration-300 dark:bg-gray-800 hover:shadow-xl">
|
||||
<div class="mb-3 text-3xl text-blue-600 dark:text-blue-400">
|
||||
📊
|
||||
</div>
|
||||
<h4 class="mb-2 text-lg text-gray-900 font-bold dark:text-white">
|
||||
数据查询
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
提供全国各大艺术体育类院校的最新招生简章、录取分数线和招生计划。
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-5 shadow-lg transition-all duration-300 dark:bg-gray-800 hover:shadow-xl">
|
||||
<div class="mb-3 text-3xl text-blue-600 dark:text-blue-400">
|
||||
🤖
|
||||
</div>
|
||||
<h4 class="mb-2 text-lg text-gray-900 font-bold dark:text-white">
|
||||
智能匹配
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
基于AI智能算法,结合考生的文化课成绩和专业课成绩,智能匹配最适合的大学和专业。
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-5 shadow-lg transition-all duration-300 dark:bg-gray-800 hover:shadow-xl">
|
||||
<div class="mb-3 text-3xl text-blue-600 dark:text-blue-400">
|
||||
📈
|
||||
</div>
|
||||
<h4 class="mb-2 text-lg text-gray-900 font-bold dark:text-white">
|
||||
录取预测
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
根据历年录取数据和当前考生情况,预测录取概率,降低滑档风险。
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl bg-white p-5 shadow-lg transition-all duration-300 dark:bg-gray-800 hover:shadow-xl">
|
||||
<div class="mb-3 text-3xl text-blue-600 dark:text-blue-400">
|
||||
👨🏫
|
||||
</div>
|
||||
<h4 class="mb-2 text-lg text-gray-900 font-bold dark:text-white">
|
||||
专家咨询
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
提供在线专家咨询服务,为考生解答志愿填报中的各种问题。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Platform Advantages -->
|
||||
<div>
|
||||
<h3 class="mb-4 text-xl text-gray-900 font-semibold dark:text-white">
|
||||
平台优势
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-1 text-2xl text-blue-600 dark:text-blue-400">
|
||||
✅
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 text-lg text-gray-900 font-bold dark:text-white">
|
||||
数据准确
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
实时更新全国各大艺术体育类院校的最新数据。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-1 text-2xl text-blue-600 dark:text-blue-400">
|
||||
✅
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 text-lg text-gray-900 font-bold dark:text-white">
|
||||
算法智能
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
采用先进的AI智能算法,提高志愿匹配准确率。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-1 text-2xl text-blue-600 dark:text-blue-400">
|
||||
✅
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 text-lg text-gray-900 font-bold dark:text-white">
|
||||
服务专业
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
拥有专业的教育专家和技术团队,为用户提供优质服务。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Development History Timeline Section -->
|
||||
<section id="development-history" class="mb-20">
|
||||
<h2 class="mb-8 text-2xl text-gray-900 font-bold sm:text-3xl dark:text-white">
|
||||
发展历程
|
||||
</h2>
|
||||
<div class="relative">
|
||||
<div class="absolute left-1/2 h-full w-1 transform bg-gray-200 -translate-x-1/2 dark:bg-gray-700" />
|
||||
<div class="space-y-12">
|
||||
<div
|
||||
v-for="(item, index) in timeline"
|
||||
:key="index"
|
||||
class="relative flex items-center"
|
||||
>
|
||||
<div class="absolute left-1/2 z-10 h-8 w-8 transform border-4 border-white rounded-full bg-blue-600 -translate-x-1/2 dark:border-gray-800 dark:bg-blue-400" />
|
||||
<div
|
||||
:class="{
|
||||
'order-1 ml-auto w-5/12': index % 2 === 0,
|
||||
'order-3 mr-auto w-5/12': index % 2 !== 0,
|
||||
}"
|
||||
>
|
||||
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-gray-800">
|
||||
<div class="mb-2 text-blue-600 font-bold dark:text-blue-400">
|
||||
{{ item.year }}
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl text-gray-900 font-bold dark:text-white">
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="order-2 hidden w-1/12 md:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Team Introduction Section -->
|
||||
<section id="team-intro" class="mb-20">
|
||||
<h2 class="mb-8 text-2xl text-gray-900 font-bold sm:text-3xl dark:text-white">
|
||||
团队介绍
|
||||
</h2>
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
<div
|
||||
v-for="member in teamMembers"
|
||||
:key="member.id"
|
||||
class="rounded-xl bg-white p-6 shadow-lg transition-all duration-300 dark:bg-gray-800 hover:shadow-xl"
|
||||
>
|
||||
<div class="relative mx-auto mb-4 h-32 w-32">
|
||||
<img
|
||||
:src="member.avatar"
|
||||
:alt="member.name"
|
||||
class="h-full w-full rounded-full object-cover"
|
||||
>
|
||||
</div>
|
||||
<h3 class="mb-1 text-xl text-gray-900 font-bold dark:text-white">
|
||||
{{ member.name }}
|
||||
</h3>
|
||||
<p class="mb-3 text-blue-600 dark:text-blue-400">
|
||||
{{ member.position }}
|
||||
</p>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ member.bio }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Smooth transition for page sections */
|
||||
section {
|
||||
transition:
|
||||
opacity 0.5s ease,
|
||||
transform 0.5s ease;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,296 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'ContactUsPage',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: '联系我们 - 艺体志愿宝',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: '艺体志愿宝平台联系方式,包括地址、电话、邮箱以及在线留言功能',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
// Contact info
|
||||
const contactInfo = {
|
||||
address: '河南省郑州市金水区文化路100号创新大厦A座12层',
|
||||
phone: '400-123-4567',
|
||||
email: 'contact@yitisheng.com',
|
||||
workingHours: '周一至周五 9:00-18:00',
|
||||
social: [
|
||||
{ name: '微信', icon: 'i-carbon-logo-wechat', url: '#', qrCode: 'https://via.placeholder.com/150' },
|
||||
{ name: '微博', icon: 'i-carbon-logo-sina-weibo', url: '#', qrCode: 'https://via.placeholder.com/150' },
|
||||
{ name: '知乎', icon: 'i-carbon-logo-zhihu', url: '#', qrCode: 'https://via.placeholder.com/150' },
|
||||
{ name: '抖音', icon: 'i-carbon-logo-douyin', url: '#', qrCode: 'https://via.placeholder.com/150' },
|
||||
],
|
||||
}
|
||||
|
||||
// Form state
|
||||
const formData = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
const formErrors = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
message: '',
|
||||
})
|
||||
|
||||
// Validate email
|
||||
function validateEmail(email: string) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@][^\s.@]*\.[^\s@]+$/
|
||||
return emailRegex.test(email)
|
||||
}
|
||||
|
||||
// Validate phone
|
||||
function validatePhone(phone: string) {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
|
||||
// Validate form
|
||||
function validateForm() {
|
||||
formErrors.value = { name: '', email: '', phone: '', message: '' }
|
||||
let isValid = true
|
||||
|
||||
if (!formData.value.name.trim()) {
|
||||
formErrors.value.name = '请输入您的姓名'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (!formData.value.email.trim()) {
|
||||
formErrors.value.email = '请输入您的邮箱'
|
||||
isValid = false
|
||||
}
|
||||
else if (!validateEmail(formData.value.email)) {
|
||||
formErrors.value.email = '请输入有效的邮箱地址'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (!formData.value.phone.trim()) {
|
||||
formErrors.value.phone = '请输入您的手机号码'
|
||||
isValid = false
|
||||
}
|
||||
else if (!validatePhone(formData.value.phone)) {
|
||||
formErrors.value.phone = '请输入有效的手机号码'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
if (!formData.value.message.trim()) {
|
||||
formErrors.value.message = '请输入您的留言内容'
|
||||
isValid = false
|
||||
}
|
||||
|
||||
return isValid
|
||||
}
|
||||
|
||||
// Form submission
|
||||
function handleSubmit() {
|
||||
if (validateForm()) {
|
||||
// Here you would typically send the form data to your backend
|
||||
console.warn('Form submitted:', formData.value)
|
||||
|
||||
// Reset form
|
||||
formData.value = { name: '', email: '', phone: '', message: '' }
|
||||
|
||||
// Show success message
|
||||
console.warn('留言提交成功!我们将尽快与您联系。')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-7xl px-4 py-8 lg:px-8 sm:px-6">
|
||||
<!-- Hero Section -->
|
||||
<section class="mb-16 text-center">
|
||||
<h1 class="mb-4 text-4xl text-gray-900 font-bold sm:text-5xl dark:text-white">
|
||||
联系我们
|
||||
</h1>
|
||||
<p class="mx-auto max-w-3xl text-xl text-gray-600 dark:text-gray-300">
|
||||
欢迎您通过以下方式与我们联系,我们将竭诚为您服务。
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Contact Us Section -->
|
||||
<section id="contact-us" class="mb-20">
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-3">
|
||||
<!-- Contact Information -->
|
||||
<div class="md:col-span-2 space-y-8">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-1 text-2xl text-blue-600 dark:text-blue-400">
|
||||
📞
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 text-lg text-gray-900 font-semibold dark:text-white">
|
||||
联系电话
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ contactInfo.phone }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ contactInfo.workingHours }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-1 text-2xl text-blue-600 dark:text-blue-400">
|
||||
📧
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 text-lg text-gray-900 font-semibold dark:text-white">
|
||||
电子邮箱
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ contactInfo.email }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="mt-1 text-2xl text-blue-600 dark:text-blue-400">
|
||||
📍
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 text-lg text-gray-900 font-semibold dark:text-white">
|
||||
公司地址
|
||||
</h4>
|
||||
<p class="text-gray-600 dark:text-gray-300">
|
||||
{{ contactInfo.address }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Component -->
|
||||
<div class="rounded-xl bg-white p-4 shadow-lg dark:bg-gray-800">
|
||||
<h4 class="mb-4 text-lg text-gray-900 font-semibold dark:text-white">
|
||||
办公地点
|
||||
</h4>
|
||||
<div class="relative h-80">
|
||||
<iframe
|
||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3678.151107753289!2d113.62540441531296!3d34.80746378036473!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x35b5535493c1a78b%3A0x9c6b3f7150250000!2sZhengzhou+University!5e0!3m2!1sen!2scn!4v1577848192056!5m2!1sen!2scn"
|
||||
width="100%"
|
||||
height="100%"
|
||||
style="border: 0"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Social Media Links -->
|
||||
<div>
|
||||
<h4 class="mb-4 text-lg text-gray-900 font-semibold dark:text-white">
|
||||
社交媒体
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div
|
||||
v-for="(item, index) in contactInfo.social"
|
||||
:key="index"
|
||||
class="rounded-xl bg-white p-4 text-center shadow-lg transition-all duration-300 dark:bg-gray-800 hover:shadow-xl"
|
||||
>
|
||||
<div :class="item.icon" class="mx-auto mb-2 text-3xl" />
|
||||
<p class="mb-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ item.name }}
|
||||
</p>
|
||||
<div class="relative mx-auto h-32 w-32">
|
||||
<img
|
||||
:src="item.qrCode"
|
||||
:alt="`${item.name}二维码`"
|
||||
class="h-full w-full rounded-lg object-cover"
|
||||
>
|
||||
</div>
|
||||
<a
|
||||
:href="item.url"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="mt-3 block text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
关注我们
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Message Form Section -->
|
||||
<section id="message-form" class="mb-20">
|
||||
<h2 class="mb-8 text-2xl text-gray-900 font-bold sm:text-3xl dark:text-white">
|
||||
在线留言
|
||||
</h2>
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<form class="rounded-xl bg-white p-6 shadow-lg dark:bg-gray-800 md:p-8" @submit.prevent="handleSubmit">
|
||||
<div class="grid grid-cols-1 mb-6 gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label for="name" class="mb-2 block text-sm text-gray-700 font-medium dark:text-gray-300">姓名</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="formData.name"
|
||||
type="text"
|
||||
class="w-full border border-gray-300 rounded-lg bg-white px-4 py-3 text-gray-900 dark:border-gray-700 dark:bg-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入您的姓名"
|
||||
>
|
||||
<div v-if="formErrors.name" class="mt-2 text-sm text-red-600">
|
||||
{{ formErrors.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="mb-2 block text-sm text-gray-700 font-medium dark:text-gray-300">邮箱</label>
|
||||
<input
|
||||
id="email"
|
||||
v-model="formData.email"
|
||||
type="email"
|
||||
class="w-full border border-gray-300 rounded-lg bg-white px-4 py-3 text-gray-900 dark:border-gray-700 dark:bg-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入您的邮箱"
|
||||
>
|
||||
<div v-if="formErrors.email" class="mt-2 text-sm text-red-600">
|
||||
{{ formErrors.email }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="phone" class="mb-2 block text-sm text-gray-700 font-medium dark:text-gray-300">手机号码</label>
|
||||
<input
|
||||
id="phone"
|
||||
v-model="formData.phone"
|
||||
type="tel"
|
||||
class="w-full border border-gray-300 rounded-lg bg-white px-4 py-3 text-gray-900 dark:border-gray-700 dark:bg-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入您的手机号码"
|
||||
>
|
||||
<div v-if="formErrors.phone" class="mt-2 text-sm text-red-600">
|
||||
{{ formErrors.phone }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="message" class="mb-2 block text-sm text-gray-700 font-medium dark:text-gray-300">留言内容</label>
|
||||
<textarea
|
||||
id="message"
|
||||
v-model="formData.message"
|
||||
rows="6"
|
||||
class="w-full border border-gray-300 rounded-lg bg-white px-4 py-3 text-gray-900 dark:border-gray-700 dark:bg-gray-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="请输入您的留言内容"
|
||||
/>
|
||||
<div v-if="formErrors.message" class="mt-2 text-sm text-red-600">
|
||||
{{ formErrors.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full rounded-lg bg-blue-600 px-6 py-3 text-lg text-white font-semibold shadow-lg transition-colors hover:bg-blue-700 hover:shadow-xl"
|
||||
>
|
||||
提交留言
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<script setup lang="ts">
|
||||
// 模拟异步删除
|
||||
const isDeleting = ref(false)
|
||||
function confirmDelete() {
|
||||
isDeleting.value = true
|
||||
setTimeout(() => {
|
||||
console.warn('删除成功')
|
||||
isDeleting.value = false
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
console.warn('用户取消了操作')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex flex-wrap items-center justify-center gap-4 p-20">
|
||||
<!-- 1. 标准上方 (Top) -->
|
||||
<w-popconfirm title="默认上方" placement="top">
|
||||
<button class="btn">
|
||||
Top
|
||||
</button>
|
||||
</w-popconfirm>
|
||||
|
||||
<!-- 2. 左上对齐 (Top Left) -->
|
||||
<w-popconfirm title="左上对齐" placement="top-left">
|
||||
<button class="btn">
|
||||
Top Left
|
||||
</button>
|
||||
</w-popconfirm>
|
||||
|
||||
<!-- 3. 左上对齐 + 箭头指向中心 -->
|
||||
<w-popconfirm
|
||||
title="左上 + 箭头居中"
|
||||
placement="top-left"
|
||||
arrow-point-at-center
|
||||
>
|
||||
<button class="w-32 btn">
|
||||
Top Left Center
|
||||
</button>
|
||||
</w-popconfirm>
|
||||
|
||||
<!-- 4. 右侧 (Right) -->
|
||||
<w-popconfirm title="右侧显示" placement="right">
|
||||
<button class="btn">
|
||||
Right
|
||||
</button>
|
||||
</w-popconfirm>
|
||||
|
||||
<!-- 5. 底部右对齐 (Bottom Right) -->
|
||||
<w-popconfirm title="底部右对齐" placement="bottom-right">
|
||||
<button class="btn">
|
||||
Bottom Right
|
||||
</button>
|
||||
</w-popconfirm>
|
||||
|
||||
<!-- 1. 基础用法 -->
|
||||
<w-popconfirm
|
||||
title="确认移除此专业?"
|
||||
@confirm="confirmDelete"
|
||||
>
|
||||
<a href="#" class="text-blue-600 hover:underline">移除 (基础)</a>
|
||||
</w-popconfirm>
|
||||
|
||||
<!-- 2. 危险操作 + 异步加载 + 描述 -->
|
||||
<w-popconfirm
|
||||
title="确定要删除这个任务吗?"
|
||||
description="删除后无法恢复,请谨慎操作。"
|
||||
ok-text="是的,删除"
|
||||
ok-type="danger"
|
||||
:ok-button-props="{ loading: isDeleting }"
|
||||
@confirm="confirmDelete"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<button class="rounded bg-red-100 px-4 py-2 text-red-600 hover:bg-red-200">
|
||||
删除任务 (高级)
|
||||
</button>
|
||||
</w-popconfirm>
|
||||
|
||||
<!-- 3. 自定义 Icon 和 Slot 内容 -->
|
||||
<w-popconfirm title="" ok-text="立即保存">
|
||||
<!-- Trigger -->
|
||||
<button class="border border-slate-300 rounded px-4 py-2 shadow-sm">
|
||||
自定义插槽
|
||||
</button>
|
||||
|
||||
<!-- Slots -->
|
||||
<template #icon>
|
||||
<span class="text-xl">🎉</span>
|
||||
</template>
|
||||
<template #title>
|
||||
<span class="text-purple-600">恭喜你发现彩蛋</span>
|
||||
</template>
|
||||
<template #description>
|
||||
这里可以放很长很长的<br>HTML内容哦。
|
||||
</template>
|
||||
</w-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.btn {
|
||||
@apply px-4 py-2 bg-white border border-slate-300 rounded hover:border-blue-500 transition-colors shadow-sm;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<script setup lang="ts">
|
||||
const router = useRouter()
|
||||
const route = useRoute('/hi/[name]')
|
||||
const user = useUserStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
watchEffect(() => {
|
||||
user.setNewName(route.params.name)
|
||||
})
|
||||
useHead({
|
||||
title: () => t('intro.hi', { name: user.savedName }),
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div text-4xl>
|
||||
<div i-carbon-pedestrian inline-block />
|
||||
</div>
|
||||
<p>
|
||||
{{ t('intro.hi', { name: user.savedName }) }}
|
||||
</p>
|
||||
|
||||
<p text-sm opacity-75>
|
||||
<em>{{ t('intro.dynamic-route') }}</em>
|
||||
</p>
|
||||
|
||||
<template v-if="user.otherNames.length">
|
||||
<div mt-4 text-sm>
|
||||
<span opacity-75>{{ t('intro.aka') }}:</span>
|
||||
<ul>
|
||||
<li v-for="otherName in user.otherNames" :key="otherName">
|
||||
<RouterLink :to="`/hi/${otherName}`" replace>
|
||||
{{ otherName }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<button
|
||||
m="3 t6" text-sm btn
|
||||
@click="router.back()"
|
||||
>
|
||||
{{ t('button.back') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,594 @@
|
|||
<!-- eslint-disable no-console -->
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'IndexPage',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: '艺体志愿宝 - 专业的艺术体育类志愿填报辅助平台',
|
||||
})
|
||||
|
||||
// User store for login status
|
||||
const userStore = useUserStore()
|
||||
|
||||
const responseMenus = [
|
||||
{
|
||||
url: '/universities',
|
||||
label: '查大学',
|
||||
icon: 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4',
|
||||
|
||||
animationDelay: '0.1s',
|
||||
darkMode: true,
|
||||
},
|
||||
{
|
||||
url: '/majors',
|
||||
label: '查专业',
|
||||
icon: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
|
||||
animationDelay: '0.2s',
|
||||
darkMode: true,
|
||||
},
|
||||
{
|
||||
url: '/cutoff-scores',
|
||||
label: '省控线',
|
||||
icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z',
|
||||
animationDelay: '0.3s',
|
||||
darkMode: true,
|
||||
},
|
||||
{
|
||||
url: '/culture-test',
|
||||
label: '测文化',
|
||||
icon: 'M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253',
|
||||
animationDelay: '0.4s',
|
||||
darkMode: true,
|
||||
},
|
||||
{
|
||||
url: '/rank-checker',
|
||||
label: '查位次',
|
||||
icon: 'M16 8v8m-4-5v5m-4-2v2m-2 4h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
animationDelay: '0.5s',
|
||||
darkMode: true,
|
||||
},
|
||||
{
|
||||
url: '/calculator',
|
||||
label: '算投档',
|
||||
icon: 'M9 7h6m0 10v-3m-3 3h.01M9 17h.01M9 14h.01M12 14h.01M15 11h.01M12 11h.01M9 11h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z',
|
||||
animationDelay: '0.6s',
|
||||
darkMode: true,
|
||||
},
|
||||
{
|
||||
url: '/sports-calculator',
|
||||
label: '体育计分器',
|
||||
icon: 'M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 1-4 4-4 1.657 0 3 .895 3 2 0 1-1 2-1 2s1.5.5 2 1.5c.5 1 .5 2.5-.5 3.5zM10 11l2 2 4-4',
|
||||
animationDelay: '0.7s',
|
||||
darkMode: true,
|
||||
},
|
||||
{
|
||||
url: '/regulations',
|
||||
label: '招生章程',
|
||||
icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
animationDelay: '0.8s',
|
||||
darkMode: true,
|
||||
},
|
||||
]
|
||||
|
||||
// Carousel images
|
||||
const carouselImages = [
|
||||
{
|
||||
id: 1,
|
||||
imageUrl: 'https://cdn.jsdelivr.net/gh/zwt13703/note-gen-image-sync@main/d7e9af38-9292-4e7a-bb24-9321c8e1b931.png',
|
||||
linkUrl: '/simulate',
|
||||
altText: '艺术考生',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
imageUrl: 'https://cdn.jsdelivr.net/gh/zwt13703/note-gen-image-sync@main/13274cb8-2c88-452b-b70b-9ca4e603606a.jpeg',
|
||||
linkUrl: '/universities',
|
||||
altText: '体育考生',
|
||||
},
|
||||
]
|
||||
|
||||
// Carousel state
|
||||
const currentSlide = ref(0)
|
||||
let carouselInterval: any
|
||||
|
||||
// Auto-play carousel
|
||||
function startCarousel() {
|
||||
carouselInterval = setInterval(() => {
|
||||
currentSlide.value = (currentSlide.value + 1) % carouselImages.length
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
function stopCarousel() {
|
||||
if (carouselInterval) {
|
||||
clearInterval(carouselInterval)
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation to previous slide
|
||||
function prevSlide() {
|
||||
stopCarousel()
|
||||
currentSlide.value = (currentSlide.value - 1 + carouselImages.length) % carouselImages.length
|
||||
startCarousel()
|
||||
}
|
||||
|
||||
// Navigation to next slide
|
||||
function nextSlide() {
|
||||
stopCarousel()
|
||||
currentSlide.value = (currentSlide.value + 1) % carouselImages.length
|
||||
startCarousel()
|
||||
}
|
||||
|
||||
// Navigation to specific slide
|
||||
function goToSlide(index: number) {
|
||||
stopCarousel()
|
||||
currentSlide.value = index
|
||||
startCarousel()
|
||||
}
|
||||
|
||||
// Start carousel on mount
|
||||
onMounted(() => {
|
||||
startCarousel()
|
||||
})
|
||||
|
||||
// Cleanup on unmount
|
||||
onUnmounted(() => {
|
||||
stopCarousel()
|
||||
})
|
||||
|
||||
// 招生章程数据
|
||||
const admissionRegulations = ref([
|
||||
{ title: '2025年艺术类本科招生章程', link: '/regulations/art-2025' },
|
||||
{ title: '2025年体育类本科招生章程', link: '/regulations/sports-2025' },
|
||||
{ title: '2025年音乐类专业招生章程', link: '/regulations/music-2025' },
|
||||
{ title: '2025年舞蹈类专业招生章程', link: '/regulations/dance-2025' },
|
||||
{ title: '2025年美术类专业招生章程', link: '/regulations/art-2025' },
|
||||
])
|
||||
|
||||
// 高考动态数据
|
||||
const examNews = ref([
|
||||
{ title: '2025年高考政策解读', link: '/news/policy-2025' },
|
||||
{ title: '艺术类招生趋势分析', link: '/news/art-trends' },
|
||||
{ title: '体育类专业录取规则', link: '/news/sports-rules' },
|
||||
{ title: '新高考选科指导', link: '/news/new-gaokao' },
|
||||
{ title: '志愿填报技巧分享', link: '/news/volunteer-tips' },
|
||||
])
|
||||
|
||||
// 热门院校数据
|
||||
const popularSchools = ref([
|
||||
{
|
||||
name: '中央美术学院',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '美术学、设计学',
|
||||
tag: '艺术类名校',
|
||||
region: '省外本科', // 添加区域信息
|
||||
},
|
||||
{
|
||||
name: '北京体育大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '体育教育、运动训练',
|
||||
tag: '体育类名校',
|
||||
region: '省外本科', // 添加区域信息
|
||||
},
|
||||
{
|
||||
name: '中国音乐学院',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '音乐表演、音乐学',
|
||||
tag: '音乐类名校',
|
||||
region: '省外本科', // 添加区域信息
|
||||
},
|
||||
{
|
||||
name: '上海戏剧学院',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '表演、导演',
|
||||
tag: '艺术类名校',
|
||||
region: '省外本科', // 添加区域信息
|
||||
},
|
||||
{
|
||||
name: '河南大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '美术学、音乐学',
|
||||
tag: '河南名校',
|
||||
region: '河南本科', // 添加区域信息
|
||||
},
|
||||
{
|
||||
name: '郑州大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '体育教育、设计学',
|
||||
tag: '河南名校',
|
||||
region: '河南本科', // 添加区域信息
|
||||
},
|
||||
{
|
||||
name: '河南师范大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '音乐学、美术学',
|
||||
tag: '河南名校',
|
||||
region: '河南本科', // 添加区域信息
|
||||
},
|
||||
{
|
||||
name: '河南职业技术学院',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
major: '艺术设计、音乐表演',
|
||||
tag: '河南专科',
|
||||
region: '河南专科', // 添加区域信息
|
||||
},
|
||||
])
|
||||
|
||||
// 区域过滤状态
|
||||
const regionFilter = ref('全部') // 默认显示全部
|
||||
|
||||
// 区域选项
|
||||
const regionOptions = [
|
||||
{ label: '全部', value: '全部' },
|
||||
{ label: '河南本科', value: '河南本科' },
|
||||
{ label: '河南专科', value: '河南专科' },
|
||||
{ label: '省外本科', value: '省外本科' },
|
||||
{ label: '省外专科', value: '省外专科' },
|
||||
]
|
||||
|
||||
// 根据区域过滤学校
|
||||
const filteredSchools = computed(() => {
|
||||
if (regionFilter.value === '全部') {
|
||||
return popularSchools.value
|
||||
}
|
||||
return popularSchools.value.filter(school => school.region === regionFilter.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-7xl select-none px-4 py-8 lg:px-8 sm:px-6">
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-10">
|
||||
<!-- Left Carousel (70% width on lg) -->
|
||||
<div class="lg:col-span-7">
|
||||
<div class="relative overflow-hidden rounded-2xl bg-white shadow-xl">
|
||||
<div class="relative aspect-video overflow-hidden">
|
||||
<!-- Carousel images -->
|
||||
<transition-group name="fade" tag="div">
|
||||
<div
|
||||
v-for="(image, index) in carouselImages"
|
||||
v-show="index === currentSlide"
|
||||
:key="image.id"
|
||||
class="absolute inset-0"
|
||||
>
|
||||
<a :href="image.linkUrl" class="block h-full w-full">
|
||||
<img
|
||||
:src="image.imageUrl"
|
||||
:alt="image.altText"
|
||||
class="h-full w-full object-cover"
|
||||
>
|
||||
</a>
|
||||
</div>
|
||||
</transition-group>
|
||||
</div>
|
||||
|
||||
<!-- Carousel navigation -->
|
||||
<div class="absolute bottom-4 left-1/2 flex transform -translate-x-1/2 space-x-2">
|
||||
<button
|
||||
v-for="(image, index) in carouselImages"
|
||||
:key="image.id"
|
||||
class="h-3 w-3 rounded-full bg-dark-1 hover:bg-dark-3 focus:outline-none"
|
||||
:class="index === currentSlide ? 'opacity-100' : 'opacity-50'"
|
||||
@click="goToSlide(index)"
|
||||
>
|
||||
<span class="sr-only">Slide {{ index + 1 }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Previous/Next buttons -->
|
||||
<button
|
||||
class="absolute left-4 top-1/2 transform rounded-full bg-gray/20 p-2 text-gray-800 transition-all-300 -translate-y-1/2 hover:bg-gray focus:outline-none"
|
||||
@click="prevSlide"
|
||||
>
|
||||
<span class="sr-only">Previous</span>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="absolute right-4 top-1/2 transform rounded-full bg-gray/20 p-2 text-gray-800 transition-all-300 -translate-y-1/2 hover:bg-gray focus:outline-none"
|
||||
@click="nextSlide"
|
||||
>
|
||||
<span class="sr-only">Next</span>
|
||||
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Menu with 8 Functional Buttons -->
|
||||
<div class="grid grid-cols-2 mt-4 gap-4 lg:grid-cols-4 md:grid-cols-4 sm:grid-cols-3">
|
||||
<a
|
||||
v-for="(menu, index) in responseMenus"
|
||||
:key="index"
|
||||
:href="menu.url"
|
||||
class="group relative flex flex-col transform animate-fade-in-up items-center justify-center border border-gray-100 rounded-xl bg-white p-4 text-gray-700 shadow-md transition-all duration-300 hover:border-blue-200 dark:text-light-700 group-hover:text-blue-800 hover:shadow-lg hover:-translate-y-1"
|
||||
:style="{ 'animation-delay': menu.animationDelay }"
|
||||
:class="{
|
||||
'dark:bg-gray-900 dark:border-gray-800 dark:hover:border-gray-500': menu.darkMode,
|
||||
}"
|
||||
:aria-label="menu.label"
|
||||
>
|
||||
<div class="mb-2 h-12 w-12 flex items-center justify-center">
|
||||
<svg class="h-6 w-6 text-blue-600 transition-colors group-hover:text-blue-800" :class="{ 'dark:text-light': menu.darkMode }" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" :d="menu.icon" />
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700 font-medium group-hover:text-blue-800" :class="{ 'dark:text-light-700': menu.darkMode }"> {{ menu.label }} </span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Form (30% width on lg) -->
|
||||
<div class="select-none lg:col-span-3">
|
||||
<div class="rounded-2xl bg-white p-6 text-gray-700 shadow-xl dark:border-gray-700 dark:bg-gray-800 dark:text-white">
|
||||
<!-- Show different content based on login status -->
|
||||
<div v-if="!userStore.user">
|
||||
<div class="mb-4 text-center">
|
||||
<p class="mb-4 text-gray-600">
|
||||
请先登录,开始志愿填报
|
||||
</p>
|
||||
<button
|
||||
class="w-full rounded-full bg-blue-600 px-6 py-3 text-lg text-white font-semibold shadow-lg transition-colors hover:bg-blue-700 hover:shadow-xl"
|
||||
@click="$router.push('/login')"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<score-form />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 招生章程、高考动态和热门院校推荐部分 -->
|
||||
<div class="content-section mt-12 select-none">
|
||||
<div class="dual-column-layout mb-10">
|
||||
<div class="left-column border-r-2 border-r-gray-800 border-dashed pr-4 dark:border-r-gray-400">
|
||||
<h3 class="mb-4 text-xl text-gray-800 font-bold dark:text-white">
|
||||
招生章程
|
||||
</h3>
|
||||
<ul class="article-list space-y-2">
|
||||
<li v-for="(item, index) in admissionRegulations" :key="`reg-${index}`">
|
||||
<a :href="item.link" :title="item.title" class="block rounded-lg px-3 py-2 text-gray-700 transition-colors duration-200 hover:bg-gray-100 dark:text-gray-300 hover:text-blue-600 dark:hover:bg-gray-700 dark:hover:text-blue-400">
|
||||
<div class="flex items-center">
|
||||
<svg class="mr-2 h-4 w-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ item.title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right-column">
|
||||
<h3 class="mb-4 text-xl text-gray-800 font-bold dark:text-white">
|
||||
高考动态
|
||||
</h3>
|
||||
<ul class="article-list space-y-2">
|
||||
<li v-for="(item, index) in examNews" :key="`news-${index}`">
|
||||
<a :href="item.link" :title="item.title" class="block rounded-lg px-3 py-2 text-gray-700 transition-colors duration-200 hover:bg-gray-100 dark:text-gray-300 hover:text-blue-600 dark:hover:bg-gray-700 dark:hover:text-blue-400">
|
||||
<div class="flex items-center">
|
||||
<svg class="mr-2 h-4 w-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span class="truncate">{{ item.title }}</span>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="popular-schools">
|
||||
<h3 class="mb-4 text-xl text-gray-800 font-bold dark:text-white">
|
||||
热门院校推荐
|
||||
</h3>
|
||||
|
||||
<!-- 区域过滤器 -->
|
||||
<div class="region-filter mb-6">
|
||||
<div class="flex flex-wrap gap-2" role="radiogroup" aria-label="选择院校区域">
|
||||
<button
|
||||
v-for="option in regionOptions"
|
||||
:key="option.value"
|
||||
class="region-filter-btn transform rounded-full px-4 py-2 text-sm font-medium transition-all duration-300 hover:scale-105 aria-checked:bg-blue-600 aria-checked:text-white aria-checked:font-semibold hover:shadow-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" :class="[
|
||||
regionFilter === option.value
|
||||
? 'bg-blue-600 text-white font-semibold shadow-md'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600',
|
||||
]"
|
||||
:aria-checked="regionFilter === option.value"
|
||||
role="radio"
|
||||
@click="regionFilter = option.value"
|
||||
>
|
||||
{{ option.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="school-grid grid grid-cols-1 gap-4 lg:grid-cols-3 md:grid-cols-2 xl:grid-cols-4">
|
||||
<div v-for="(school, index) in filteredSchools" :key="`school-${index}`" class="border border-gray-200 rounded-xl bg-white p-4 transition-all duration-300 dark:border-gray-700 hover:border-blue-300 dark:bg-gray-800 hover:shadow-lg dark:hover:border-blue-500 dark:hover:shadow-xl">
|
||||
<div class="flex">
|
||||
<div class="school-logo mr-4 h-16 w-16 flex-shrink-0">
|
||||
<img :src="school.logo" :alt="school.name" class="h-full w-full rounded-full object-contain">
|
||||
</div>
|
||||
<div class="school-info min-w-0 flex-1">
|
||||
<div class="school-name mb-1 truncate text-gray-800 font-bold dark:text-white" :title="school.name">
|
||||
{{ school.name }}
|
||||
</div>
|
||||
<div class="school-major mb-2 truncate text-sm text-gray-600 dark:text-gray-400" :title="school.major">
|
||||
热门专业:{{ school.major }}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="school-tag max-w-[80px] truncate rounded bg-gray-100 px-2 py-1 text-xs text-gray-600 dark:bg-gray-700 dark:text-gray-300" :title="school.tag">{{ school.tag }}</span>
|
||||
<button class="view-btn rounded-lg bg-blue-600 px-3 py-1 text-sm text-white transition-colors duration-200 hover:bg-blue-700">
|
||||
查看院校
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Staggered animation for menu items */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 招生章程、高考动态和热门院校推荐部分样式 */
|
||||
.dual-column-layout {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.left-column,
|
||||
.right-column {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.article-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.article-list li {
|
||||
margin-bottom: 15px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.popular-schools h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.school-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.school-logo {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.school-logo img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.school-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.school-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.school-major {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.school-tag {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.view-btn {
|
||||
padding: 4px 12px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dual-column-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.school-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* 区域过滤器样式 */
|
||||
.region-filter-btn {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 80px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.region-filter-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.region-filter-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.region-filter-btn:focus-visible {
|
||||
outline: 2px solid #2563eb;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 为活动状态添加更明显的视觉效果 */
|
||||
.region-filter-btn.active {
|
||||
background: linear-gradient(135deg, #3b82f6, #2563eb);
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: home
|
||||
</route>
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// --- Types ---
|
||||
interface SubCategory {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface Category {
|
||||
id: string
|
||||
name: string
|
||||
isOpen: boolean
|
||||
subCategories?: SubCategory[]
|
||||
}
|
||||
|
||||
interface Major {
|
||||
id: string
|
||||
name: string
|
||||
code: string
|
||||
subCategoryName: string
|
||||
duration: string
|
||||
genderRatio: string
|
||||
avgSalary: string
|
||||
}
|
||||
|
||||
// --- Mock Data ---
|
||||
const categories = ref<Category[]>([
|
||||
{ id: '1', name: '热门', isOpen: false },
|
||||
{
|
||||
id: '2',
|
||||
name: '医药卫生大类',
|
||||
isOpen: true,
|
||||
subCategories: [
|
||||
{ id: '2-1', name: '中医药类' },
|
||||
{ id: '2-2', name: '临床医学类' },
|
||||
{ id: '2-3', name: '健康管理与促进类' },
|
||||
{ id: '2-4', name: '医学技术类' },
|
||||
{ id: '2-5', name: '护理类' },
|
||||
{ id: '2-6', name: '康复治疗类' },
|
||||
{ id: '2-7', name: '公共卫生与卫生管理类' },
|
||||
{ id: '2-8', name: '眼视光类' },
|
||||
{ id: '2-9', name: '药学类' },
|
||||
],
|
||||
},
|
||||
{ id: '3', name: '文化艺术大类', isOpen: false, subCategories: [] },
|
||||
{ id: '4', name: '交通运输大类', isOpen: false, subCategories: [] },
|
||||
{ id: '5', name: '装备制造大类', isOpen: false, subCategories: [] },
|
||||
])
|
||||
|
||||
const majorsData: Major[] = [
|
||||
{ id: 'm1', name: '中医学', code: '520401K', subCategoryName: '中医药类', duration: '三年', genderRatio: '40:60', avgSalary: '¥-' },
|
||||
{ id: 'm2', name: '中医康复技术', code: '520416', subCategoryName: '中医药类', duration: '三年', genderRatio: '--', avgSalary: '¥8300' },
|
||||
{ id: 'm3', name: '朝医学', code: '520409K', subCategoryName: '中医药类', duration: '三年', genderRatio: '--', avgSalary: '¥-' },
|
||||
{ id: 'm4', name: '中药材生产与加工', code: '520414', subCategoryName: '中医药类', duration: '三年', genderRatio: '--', avgSalary: '¥6800' },
|
||||
{ id: 'm5', name: '口腔医学', code: '520102K', subCategoryName: '临床医学类', duration: '三年', genderRatio: '42:58', avgSalary: '¥-' },
|
||||
{ id: 'm6', name: '临床医学', code: '520101K', subCategoryName: '临床医学类', duration: '三年', genderRatio: '44:56', avgSalary: '¥-' },
|
||||
]
|
||||
|
||||
// --- State ---
|
||||
const activeTab = ref<'general' | 'vocational_bs' | 'vocational_hs'>('vocational_hs')
|
||||
const searchQuery = ref('')
|
||||
const activeCategory = ref('医药卫生大类')
|
||||
const activeSubCategory = ref('')
|
||||
|
||||
// --- Logic ---
|
||||
function toggleCategory(cat: Category) {
|
||||
cat.isOpen = !cat.isOpen
|
||||
}
|
||||
|
||||
const groupedMajors = computed(() => {
|
||||
const groups: Record<string, Major[]> = {}
|
||||
majorsData.forEach((major) => {
|
||||
if (!groups[major.subCategoryName]) {
|
||||
groups[major.subCategoryName] = []
|
||||
}
|
||||
groups[major.subCategoryName].push(major)
|
||||
})
|
||||
return groups
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Global Wrapper with Dark Mode Background -->
|
||||
<div class="min-h-screen bg-[#f9f9f9] text-gray-700 font-sans transition-colors duration-300 dark:bg-gray-900 dark:text-gray-200">
|
||||
<div class="mx-auto max-w-7xl px-4 lg:px-8 sm:px-6">
|
||||
<!-- Top Header / Tabs Area -->
|
||||
<div class="mb-6 mt-6 rounded-lg bg-white p-4 shadow transition-colors dark:bg-gray-800 sm:p-6">
|
||||
<div class="mx-auto max-w-7xl">
|
||||
<!-- Flex Container: Stacks on mobile, Row on md+ -->
|
||||
<div class="flex flex-col justify-between bg-white pt-2 md:flex-row md:items-center dark:bg-gray-800 sm:pt-4">
|
||||
<!-- Tabs (Scrollable on mobile) -->
|
||||
<div class="scrollbar-hide flex overflow-x-auto pb-2 space-x-1 md:pb-0">
|
||||
<button
|
||||
class="whitespace-nowrap rounded-t-lg px-6 py-3 text-base font-medium transition-colors sm:px-8 sm:text-lg" :class="[activeTab === 'general' ? 'bg-blue-500 text-white' : 'text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-600']"
|
||||
@click="activeTab = 'general'"
|
||||
>
|
||||
本科(普通)
|
||||
</button>
|
||||
<button
|
||||
class="whitespace-nowrap rounded-t-lg px-6 py-3 text-base font-medium transition-colors sm:px-8 sm:text-lg" :class="[activeTab === 'vocational_bs' ? 'bg-blue-500 text-white' : 'text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-600']"
|
||||
@click="activeTab = 'vocational_bs'"
|
||||
>
|
||||
本科(职业)
|
||||
</button>
|
||||
<button
|
||||
class="relative whitespace-nowrap rounded-t-lg px-6 py-3 text-base font-medium transition-colors sm:px-8 sm:text-lg" :class="[activeTab === 'vocational_hs' ? 'bg-blue-500 text-white' : 'text-gray-600 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-600']"
|
||||
@click="activeTab = 'vocational_hs'"
|
||||
>
|
||||
专科(高职)
|
||||
<!-- blue bottom line fix for active state -->
|
||||
<span v-if="activeTab === 'vocational_hs'" class="absolute bottom-[-1px] left-0 h-1 w-full bg-blue-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search & Sort -->
|
||||
<div class="mt-4 w-full flex flex-col items-start pb-2 md:mt-0 md:w-auto sm:flex-row sm:items-center space-y-3 sm:space-x-4 sm:space-y-0">
|
||||
<div class="hidden cursor-pointer text-sm text-gray-500 sm:block dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-600">
|
||||
默认排序 ▼
|
||||
</div>
|
||||
<div class="w-full flex sm:w-auto">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="输入专业名称"
|
||||
class="w-full flex-grow border rounded-l px-3 py-2 text-sm transition-colors sm:w-48 dark:border-gray-600 focus:border-blue-500 dark:bg-gray-700 light:bg-white dark:text-white focus:outline-none focus:ring-1 focus:ring-blue-500 dark:placeholder-gray-400"
|
||||
>
|
||||
<button class="whitespace-nowrap rounded-r bg-blue-500 px-5 py-2 text-sm text-white transition-colors hover:bg-blue-600">
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- blue Bar Line -->
|
||||
<div class="h-1 w-full bg-blue-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<!-- Flex-col on mobile (sidebar stacks on top), lg:flex-row on desktop -->
|
||||
<div class="mx-auto flex flex-col gap-6 rounded-lg py-2 lg:flex-row sm:py-6">
|
||||
<!-- Left Sidebar -->
|
||||
<div class="w-full flex-shrink-0 self-start overflow-hidden rounded-lg bg-white pb-4 shadow-sm transition-colors lg:w-64 dark:bg-gray-800">
|
||||
<ul>
|
||||
<li v-for="cat in categories" :key="cat.id" class="border-b border-gray-100 border-dashed last:border-0 dark:border-gray-700">
|
||||
<!-- Main Category Header -->
|
||||
<div
|
||||
class="flex cursor-pointer items-center justify-between px-4 py-4 transition-colors hover:text-blue-600" :class="[
|
||||
activeCategory === cat.name
|
||||
? 'text-blue-500 bg-blue-50 dark:bg-blue-900/20 border-l-4 border-blue-500'
|
||||
: 'text-gray-700 dark:text-gray-300 border-l-4 border-transparent',
|
||||
]"
|
||||
@click="toggleCategory(cat)"
|
||||
>
|
||||
<span class="font-medium">{{ cat.name }}</span>
|
||||
<span v-if="cat.subCategories && cat.subCategories.length > 0" class="text-xs">
|
||||
<!-- Icons -->
|
||||
<svg v-if="cat.isOpen" xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" /></svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /></svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Sub Categories -->
|
||||
<ul v-show="cat.isOpen && cat.subCategories" class="bg-white dark:bg-gray-800">
|
||||
<li
|
||||
v-for="sub in cat.subCategories"
|
||||
:key="sub.id"
|
||||
class="cursor-pointer py-3 pl-8 pr-4 text-sm text-gray-600 transition-colors hover:bg-gray-50 dark:text-gray-400 hover:text-blue-600 dark:hover:bg-gray-700 dark:hover:text-blue-600"
|
||||
@click.stop="activeSubCategory = sub.name"
|
||||
>
|
||||
{{ sub.name }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Right Content List -->
|
||||
<div class="flex-grow space-y-6">
|
||||
<!-- Loop through Groups -->
|
||||
<div v-for="(majors, groupName) in groupedMajors" :key="groupName" class="overflow-hidden rounded-sm bg-white shadow-sm transition-colors dark:bg-gray-800">
|
||||
<!-- Group Header (Banner) -->
|
||||
<div class="border-b border-blue-100 bg-blue-50 px-6 py-3 dark:border-gray-600 dark:bg-gray-700">
|
||||
<h2 class="text-center text-lg text-blue-500 font-bold">
|
||||
{{ groupName }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<!-- List Items -->
|
||||
<div class="p-4 space-y-8 sm:p-6">
|
||||
<div v-for="major in majors" :key="major.id" class="flex flex-col gap-3">
|
||||
<!-- Top Row: Title + Code + Action -->
|
||||
<div class="flex flex-col justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h3 class="text-lg text-gray-800 font-normal transition-colors sm:text-xl dark:text-gray-100">
|
||||
{{ major.name }}
|
||||
</h3>
|
||||
<span class="flex items-center gap-1 border border-gray-300 rounded bg-gray-50 px-1 py-0.5 text-xs text-gray-500 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-400">
|
||||
<svg class="h-3 w-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg>
|
||||
专业代码:{{ major.code }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Mobile: Button moves to its own row if needed, but flex-col handles it.
|
||||
Here we make it stretch on very small screens or auto on sm -->
|
||||
<button class="mt-2 w-full border border-blue-500 rounded px-4 py-1.5 text-center text-sm text-blue-500 transition-colors sm:mt-0 sm:w-auto hover:bg-blue-50 dark:hover:bg-blue-900/30">
|
||||
开设院校
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Row: Stats -->
|
||||
<div class="flex flex-wrap items-center gap-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div class="rounded-sm bg-gray-100 px-3 py-1 transition-colors dark:bg-gray-700">
|
||||
修业年限:{{ major.duration }}
|
||||
</div>
|
||||
<div class="rounded-sm bg-gray-100 px-3 py-1 transition-colors dark:bg-gray-700">
|
||||
男女比例:{{ major.genderRatio }}
|
||||
</div>
|
||||
<div class="rounded-sm bg-gray-100 px-3 py-1 transition-colors dark:bg-gray-700">
|
||||
平均薪酬:{{ major.avgSalary }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="flex items-center justify-center py-8 space-x-2">
|
||||
<span class="cursor-pointer text-sm text-gray-500 dark:text-gray-400 hover:text-blue-600">首页</span>
|
||||
<span class="flex cursor-pointer items-center text-sm text-gray-500 dark:text-gray-400 hover:text-blue-600"><svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg> 上一页</span>
|
||||
|
||||
<button class="h-8 w-8 flex items-center justify-center rounded bg-blue-500 text-sm text-white shadow-sm">
|
||||
1
|
||||
</button>
|
||||
<button class="h-8 w-8 flex items-center justify-center border border-gray-200 rounded bg-white text-sm text-gray-600 transition-colors dark:border-gray-600 hover:border-blue-500 dark:bg-gray-800 dark:text-gray-300 hover:text-blue-600">
|
||||
2
|
||||
</button>
|
||||
<button class="h-8 w-8 flex items-center justify-center border border-gray-200 rounded bg-white text-sm text-gray-600 transition-colors dark:border-gray-600 hover:border-blue-500 dark:bg-gray-800 dark:text-gray-300 hover:text-blue-600">
|
||||
3
|
||||
</button>
|
||||
|
||||
<span class="flex cursor-pointer items-center text-sm text-gray-500 dark:text-gray-400 hover:text-blue-600">下一页 <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg></span>
|
||||
<span class="cursor-pointer text-sm text-gray-500 dark:text-gray-400 hover:text-blue-600">尾页</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fixed Buttons Area -->
|
||||
<div class="fixed bottom-10 right-6 z-20 flex flex-col gap-4 sm:right-10">
|
||||
<!-- Back to Top -->
|
||||
<!-- <button @click="scrollToTop" class="bg-white dark:bg-gray-800 border border-blue-200 dark:border-gray-600 shadow-lg text-blue-500 w-12 h-12 rounded-full flex items-center justify-center hover:bg-blue-50 dark:hover:bg-gray-700 transition-all">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18"></path></svg>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Custom scrollbar for webkit */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #d1d5db;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #4b5563;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
/* Hide scrollbar for tabs on mobile but keep functionality */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
<script setup lang="ts">
|
||||
import { useHead } from '@unhead/vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'PrivacyPolicyPage',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: '隐私政策 - 艺体志愿宝',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: '艺体志愿宝平台隐私政策,详细说明我们如何收集、使用和保护您的个人信息',
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-6xl px-4 py-8 container">
|
||||
<!-- Hero Section -->
|
||||
<section class="mb-16 text-center">
|
||||
<h1 class="mb-4 text-4xl text-gray-900 font-bold sm:text-5xl dark:text-white">
|
||||
隐私政策
|
||||
</h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300">
|
||||
生效日期:2025年1月1日
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Privacy Policy Content -->
|
||||
<section class="mb-20 rounded-xl bg-white p-8 shadow-lg dark:bg-gray-800">
|
||||
<div class="text-gray-600 leading-relaxed space-y-8 dark:text-gray-300">
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
一、引言
|
||||
</h2>
|
||||
<p>
|
||||
艺体志愿宝(以下简称"我们")非常重视用户的隐私保护。本隐私政策将详细说明我们在使用艺体志愿宝平台(以下简称"平台")时收集、使用、存储和保护用户个人信息的政策和措施。
|
||||
</p>
|
||||
<p class="mt-4">
|
||||
请您在使用平台前仔细阅读本隐私政策,特别是加粗或下划线的内容。如果您不同意本隐私政策的任何内容,请立即停止使用平台。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
二、我们收集的信息
|
||||
</h2>
|
||||
|
||||
<h3 class="mb-3 text-xl text-gray-900 font-semibold dark:text-white">
|
||||
2.1 您主动提供的信息
|
||||
</h3>
|
||||
<p>
|
||||
当您使用平台的某些功能时,您可能需要向我们提供个人信息,包括但不限于:姓名、邮箱地址、手机号码、身份证号码、学校信息、考试成绩等。
|
||||
</p>
|
||||
|
||||
<h3 class="mb-3 mt-6 text-xl text-gray-900 font-semibold dark:text-white">
|
||||
2.2 我们自动收集的信息
|
||||
</h3>
|
||||
<p>
|
||||
当您访问或使用平台时,我们可能会自动收集某些信息,包括但不限于:IP地址、浏览器类型、操作系统、访问时间、访问页面、设备信息等。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
三、我们如何使用您的信息
|
||||
</h2>
|
||||
<p>
|
||||
我们收集您的信息主要用于以下目的:
|
||||
</p>
|
||||
<ul class="mt-4 list-disc pl-8 space-y-2">
|
||||
<li>为您提供平台的核心功能和服务</li>
|
||||
<li>处理您的请求和提供客户支持</li>
|
||||
<li>改进平台的功能和用户体验</li>
|
||||
<li>向您发送重要通知和更新</li>
|
||||
<li>开展市场调研和分析</li>
|
||||
<li>防止和打击欺诈、滥用等违法行为</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
四、信息分享与披露
|
||||
</h2>
|
||||
<p>
|
||||
我们不会将您的个人信息出售或出租给第三方。在以下情况下,我们可能会分享您的信息:
|
||||
</p>
|
||||
<ul class="mt-4 list-disc pl-8 space-y-2">
|
||||
<li>获得您的明确同意</li>
|
||||
<li>根据法律法规或政府要求</li>
|
||||
<li>保护我们或他人的合法权益</li>
|
||||
<li>与我们的合作伙伴共享(但需遵守严格的保密协议)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
五、数据安全
|
||||
</h2>
|
||||
<p>
|
||||
我们采取了合理的技术和管理措施来保护您的个人信息,防止其被未经授权的访问、使用、披露或篡改。这些措施包括但不限于:
|
||||
</p>
|
||||
<ul class="mt-4 list-disc pl-8 space-y-2">
|
||||
<li>使用加密技术保护数据传输和存储</li>
|
||||
<li>限制访问个人信息的人员范围</li>
|
||||
<li>定期进行安全审计和评估</li>
|
||||
<li>制定严格的安全管理制度</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
六、您的权利
|
||||
</h2>
|
||||
<p>
|
||||
根据相关法律法规,您有权:
|
||||
</p>
|
||||
<ul class="mt-4 list-disc pl-8 space-y-2">
|
||||
<li>访问和获取您的个人信息</li>
|
||||
<li>更正或补充您的个人信息</li>
|
||||
<li>删除您的个人信息</li>
|
||||
<li>限制或拒绝我们使用您的信息</li>
|
||||
<li>撤回您的同意</li>
|
||||
<li>提出投诉和请求协助</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
七、儿童隐私
|
||||
</h2>
|
||||
<p>
|
||||
平台不面向14周岁以下的儿童提供服务。如果我们发现自己在未事先获得可验证的家长同意的情况下收集了儿童的个人信息,我们将立即删除相关信息。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
八、隐私政策的变更
|
||||
</h2>
|
||||
<p>
|
||||
我们可能会不时更新本隐私政策。当政策发生变更时,我们会在平台上发布更新通知,并修改"生效日期"。建议您定期查看本隐私政策以了解最新情况。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="mb-4 text-2xl text-gray-900 font-bold dark:text-white">
|
||||
九、联系我们
|
||||
</h2>
|
||||
<p>
|
||||
如果您对本隐私政策有任何疑问或建议,请通过以下方式联系我们:
|
||||
</p>
|
||||
<div class="mt-4 space-y-2">
|
||||
<p><strong>电子邮箱:</strong>privacy@yitisheng.com</p>
|
||||
<p><strong>联系电话:</strong>400-123-4567</p>
|
||||
<p><strong>联系地址:</strong>河南省郑州市金水区文化路100号创新大厦A座12层</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-300 pt-8 dark:border-gray-700">
|
||||
<p class="text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
© 2025 艺体志愿宝 保留所有权利
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,504 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
// --- 类型定义 ---
|
||||
// interface StatItem {
|
||||
// label: string;
|
||||
// value: string | number;
|
||||
// icon?: string;
|
||||
// }
|
||||
|
||||
interface Ranking {
|
||||
label: string
|
||||
value: number
|
||||
trend?: 'up' | 'down' | 'flat'
|
||||
}
|
||||
|
||||
interface Major {
|
||||
college: string
|
||||
majors: string[]
|
||||
}
|
||||
|
||||
// --- 状态数据 ---
|
||||
const route = useRoute('/school/[schoolCode]')
|
||||
const schoolCode = ref('')
|
||||
|
||||
const activeLocationTab = ref('唐岛湾校区')
|
||||
|
||||
// 基础信息数据
|
||||
// const stats: StatItem[] = [
|
||||
// { label: '建校时间', value: '1953年', icon: '📅' },
|
||||
// { label: '占地面积', value: '5000亩', icon: '⛳' },
|
||||
// { label: '主管部门', value: '教育部', icon: '🏛️' },
|
||||
// { label: '博士点', value: '16个', icon: '🎓' },
|
||||
// { label: '硕士点', value: '33个', icon: '📜' },
|
||||
// { label: '国家重点学科', value: '5个', icon: '⭐' },
|
||||
// ];
|
||||
|
||||
const rankings: Ranking[] = [
|
||||
{ label: '软科综合', value: 67 },
|
||||
{ label: '校友会综合', value: 80 },
|
||||
{ label: 'US世界', value: 500 },
|
||||
{ label: '人气值排名', value: 70 },
|
||||
]
|
||||
|
||||
// 院系设置数据 (模拟部分)
|
||||
const departments: Major[] = [
|
||||
{ college: '地球科学与技术学院', majors: ['勘查技术与工程', '地质学', '资源勘查工程', '地球物理学'] },
|
||||
{ college: '石油工程学院', majors: ['海洋油气工程', '石油工程', '碳储科学与工程'] },
|
||||
{ college: '化学化工学院', majors: ['化学', '化学工程与工艺', '应用化学', '环境工程', '能源化学工程'] },
|
||||
{ college: '机电工程学院', majors: ['安全工程', '工业设计', '智能制造工程', '机械设计制造及其自动化'] },
|
||||
{ college: '储运与建筑工程学院', majors: ['土木工程', '工程力学', '建筑环境与能源应用工程', '油气储运工程'] },
|
||||
]
|
||||
|
||||
// 校园配置数据
|
||||
const facilities = [
|
||||
{ name: '4人/间', icon: '🛏️' },
|
||||
{ name: '5个食堂', icon: '🍚' },
|
||||
{ name: '上床下桌', icon: '🪑' },
|
||||
{ name: '独立卫浴', icon: '🚿' },
|
||||
{ name: '有空调', icon: '❄️' },
|
||||
{ name: '有游泳馆', icon: '🏊' },
|
||||
]
|
||||
|
||||
// 模拟图片 (使用占位符)
|
||||
const bgHeader = 'https://via.placeholder.com/1200x300/e6f2ff/0056b3?text=University+Header'
|
||||
const sceneryImages = [
|
||||
'https://via.placeholder.com/300x200/ffedcc/e67e22?text=Scenery+1',
|
||||
'https://via.placeholder.com/300x200/d4edda/155724?text=Scenery+2',
|
||||
'https://via.placeholder.com/300x200/f8d7da/721c24?text=Scenery+3',
|
||||
'https://via.placeholder.com/300x200/cce5ff/004085?text=Scenery+4',
|
||||
]
|
||||
|
||||
// 校区地址数据
|
||||
const locationInfo = computed(() => {
|
||||
return activeLocationTab.value === '唐岛湾校区'
|
||||
? { address: '山东省青岛市黄岛区长江西路66号', details: '附近3km内分布着 58个餐饮场所...' }
|
||||
: { address: '山东省东营市...', details: '老校区历史悠久...' }
|
||||
})
|
||||
|
||||
watchEffect(() => {
|
||||
schoolCode.value = route.params.schoolCode
|
||||
})
|
||||
useHead({
|
||||
title: () => { return `${schoolCode.value}|艺体志愿宝` },
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-7xl min-h-screen bg-gray-50 px-4 transition-colors duration-300 dark:bg-slate-900 lg:px-8 sm:px-6">
|
||||
<!-- 顶部导航 & Header sticky -->
|
||||
<header class="top-0 z-50 shadow-sm">
|
||||
<div class="mb-8 mt-8 rounded-lg bg-white dark:bg-slate-800">
|
||||
<div class="h-16 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-12 w-12 flex items-center justify-center rounded-full bg-blue-900 text-xs text-white font-bold">
|
||||
Logo
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="text-xl text-gray-900 font-bold dark:text-white">
|
||||
中国石油大学(华东)
|
||||
</h1>
|
||||
<div class="flex gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>公办</span><span>|</span><span>理工类</span><span>|</span><span>教育部直属</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签 -->
|
||||
<div class="ml-4 hidden gap-1 md:flex">
|
||||
<span class="border border-orange-500 rounded px-1.5 py-0.5 text-xs text-orange-500">211</span>
|
||||
<span class="border border-orange-500 rounded px-1.5 py-0.5 text-xs text-orange-500">双一流</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航菜单 -->
|
||||
<nav class="scrollbar-hide flex overflow-x-auto pb-2 text-sm text-gray-500 font-medium space-x-8 sm:pb-0 dark:text-gray-400">
|
||||
<a href="#" class="whitespace-nowrap border-b-2 border-orange-500 pb-3 text-orange-600 dark:text-orange-400">学校概况</a>
|
||||
<a href="#" class="whitespace-nowrap pb-3 hover:text-gray-700 dark:hover:text-gray-200">历年分数</a>
|
||||
<a href="#" class="whitespace-nowrap pb-3 hover:text-gray-700 dark:hover:text-gray-200">招生计划</a>
|
||||
<a href="#" class="whitespace-nowrap pb-3 hover:text-gray-700 dark:hover:text-gray-200">开设专业</a>
|
||||
<a href="#" class="whitespace-nowrap pb-3 hover:text-gray-700 dark:hover:text-gray-200">录取预测</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<main class="mx-auto max-w-7xl px-4 py-6 lg:px-8 sm:px-6">
|
||||
<!-- 顶部 Banner 信息卡片 -->
|
||||
<div class="mb-6 overflow-hidden rounded-lg bg-white shadow dark:bg-slate-800">
|
||||
<!-- Banner 图片 -->
|
||||
<div class="relative h-48 bg-cover bg-center md:h-64" :style="{ backgroundImage: `url(${bgHeader})` }">
|
||||
<div class="absolute bottom-4 right-4 flex gap-2">
|
||||
<button class="rounded bg-black/50 px-3 py-1 text-sm text-white backdrop-blur-sm hover:bg-black/70">
|
||||
📺 视频
|
||||
</button>
|
||||
<button class="rounded bg-white/90 px-3 py-1 text-sm text-gray-800 backdrop-blur-sm hover:bg-white">
|
||||
📷 校园风光
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 快速统计信息栏 -->
|
||||
<div class="grid grid-cols-2 gap-4 border-b border-gray-100 p-3 md:grid-cols-5 sm:grid-cols-4 dark:border-slate-700">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xl text-orange-500">
|
||||
🕒
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
建校时间
|
||||
</p>
|
||||
<p class="text-gray-900 font-medium dark:text-white">
|
||||
1953年
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xl text-purple-500">
|
||||
⛳
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
占地面积
|
||||
</p>
|
||||
<p class="text-gray-900 font-medium dark:text-white">
|
||||
5000亩
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xl text-blue-500">
|
||||
🎓
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
主管部门
|
||||
</p>
|
||||
<p class="text-gray-900 font-medium dark:text-white">
|
||||
教育部
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xl text-blue-500">
|
||||
📞
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
联系电话
|
||||
</p>
|
||||
<p class="text-gray-900 font-medium dark:text-white">
|
||||
0532-86983086
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="text-xl text-blue-500">
|
||||
📧
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
邮箱
|
||||
</p>
|
||||
<p class="text-gray-900 font-medium dark:text-white">
|
||||
zhaosheng@upc.edu.cn
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 网格布局:左侧主内容,右侧简单侧边栏(可选) -->
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-4">
|
||||
<!-- 左侧/主体内容 (占3份宽度) -->
|
||||
<div class="lg:col-span-3 space-y-6">
|
||||
<!-- 1. 基本信息模块 -->
|
||||
<section class="rounded-lg bg-white p-6 shadow-sm dark:bg-slate-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="border-l-4 border-orange-500 pl-3 text-lg text-gray-900 font-bold dark:text-white">
|
||||
基本信息
|
||||
</h2>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-orange-500">[详情]</a>
|
||||
</div>
|
||||
|
||||
<p class="mb-6 text-sm text-gray-600 leading-relaxed dark:text-gray-300">
|
||||
中国石油大学(华东)是教育部直属全国重点大学,是国家“211工程”重点建设和开展“985工程优势学科创新平台”建设并建有研究生院的高校之一。学校是教育部和五大能源企业集团公司、教育部和山东省人民政府共建的高校...
|
||||
</p>
|
||||
|
||||
<!-- 排名与数据 -->
|
||||
<div class="mb-6 rounded-lg bg-orange-50 p-4 dark:bg-slate-700/50">
|
||||
<div class="grid grid-cols-2 gap-4 text-center md:grid-cols-4">
|
||||
<div v-for="(rank, idx) in rankings" :key="idx" class="border-r border-orange-200 last:border-0 dark:border-slate-600">
|
||||
<div class="text-xl text-orange-600 font-bold dark:text-orange-400">
|
||||
{{ rank.value }}
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ rank.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 地理位置 & 地图 -->
|
||||
<div class="overflow-hidden border border-gray-200 rounded-lg dark:border-slate-700">
|
||||
<div class="flex border-b border-gray-200 bg-gray-50 dark:border-slate-700 dark:bg-slate-700">
|
||||
<button
|
||||
v-for="tab in ['唐岛湾校区', '东营科教园区', '古镇口校区']" :key="tab"
|
||||
class="px-4 py-2 text-sm transition-colors"
|
||||
:class="activeLocationTab === tab ? 'bg-white dark:bg-slate-800 text-orange-500 border-t-2 border-orange-500' : 'text-gray-600 dark:text-gray-400'"
|
||||
@click="activeLocationTab = tab"
|
||||
>
|
||||
{{ tab }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="h-64 flex flex-col md:flex-row">
|
||||
<!-- 模拟地图 -->
|
||||
<div class="group relative w-full flex items-center justify-center bg-blue-50 md:w-1/2 dark:bg-slate-900">
|
||||
<span class="text-4xl text-blue-300">MAP</span>
|
||||
<div class="absolute inset-0 bg-black/5 transition dark:bg-white/5 group-hover:bg-transparent" />
|
||||
<div class="absolute bottom-2 right-2 rounded bg-white px-2 py-1 text-xs shadow dark:bg-slate-700">
|
||||
地图详情 >
|
||||
</div>
|
||||
</div>
|
||||
<!-- 地址详情 -->
|
||||
<div class="w-full flex flex-col justify-center p-4 md:w-1/2">
|
||||
<h4 class="mb-2 flex items-center gap-2 text-gray-800 font-bold dark:text-white">
|
||||
📍 {{ activeLocationTab }}
|
||||
</h4>
|
||||
<p class="mb-3 text-sm text-orange-500">
|
||||
{{ locationInfo.address }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 leading-relaxed dark:text-gray-400">
|
||||
{{ locationInfo.details }}
|
||||
</p>
|
||||
<div class="mt-4 flex gap-4 text-center">
|
||||
<div>
|
||||
<div class="text-sm font-bold dark:text-white">
|
||||
58
|
||||
</div><div class="text-xs text-gray-400">
|
||||
餐饮场所
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-bold dark:text-white">
|
||||
27
|
||||
</div><div class="text-xs text-gray-400">
|
||||
酒店
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-sm font-bold dark:text-white">
|
||||
7
|
||||
</div><div class="text-xs text-gray-400">
|
||||
购物中心
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 2. 院系设置模块 -->
|
||||
<section class="rounded-lg bg-white p-6 shadow-sm dark:bg-slate-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="border-l-4 border-orange-500 pl-3 text-lg text-gray-900 font-bold dark:text-white">
|
||||
院系设置
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="bg-gray-50 text-xs text-gray-500 dark:bg-slate-700 dark:text-gray-400">
|
||||
<tr>
|
||||
<th class="w-1/4 px-4 py-3">
|
||||
学院
|
||||
</th>
|
||||
<th class="px-4 py-3">
|
||||
专业
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-slate-700">
|
||||
<tr v-for="(dept, idx) in departments" :key="idx" class="hover:bg-gray-50 dark:hover:bg-slate-700/50">
|
||||
<td class="px-4 py-3 text-gray-900 font-medium dark:text-white">
|
||||
{{ dept.college }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 dark:text-gray-300">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<span v-for="major in dept.majors" :key="major" class="rounded bg-gray-100 px-2 py-0.5 text-xs dark:bg-slate-600">
|
||||
{{ major }} <span class="ml-1 text-gray-300">本科</span>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页模拟 -->
|
||||
<div class="mt-4 flex justify-center gap-2 text-xs text-gray-500">
|
||||
<button class="px-2 py-1 hover:text-orange-500">
|
||||
首页
|
||||
</button>
|
||||
<button class="px-2 py-1 hover:text-orange-500">
|
||||
上一页
|
||||
</button>
|
||||
<button class="h-6 w-6 flex items-center justify-center rounded-full bg-orange-500 text-white">
|
||||
1
|
||||
</button>
|
||||
<button class="h-6 w-6 flex items-center justify-center rounded-full hover:bg-gray-100 dark:hover:bg-slate-700">
|
||||
2
|
||||
</button>
|
||||
<button class="px-2 py-1 hover:text-orange-500">
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 3. 校园风光 -->
|
||||
<section class="rounded-lg bg-white p-6 shadow-sm dark:bg-slate-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="border-l-4 border-orange-500 pl-3 text-lg text-gray-900 font-bold dark:text-white">
|
||||
校园风光
|
||||
</h2>
|
||||
<a href="#" class="text-xs text-orange-500">更多 ></a>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||||
<div v-for="(img, i) in sceneryImages" :key="i" class="group relative h-32 cursor-pointer overflow-hidden rounded-lg">
|
||||
<img :src="img" class="h-full w-full transform object-cover transition group-hover:scale-110" alt="scenery">
|
||||
<div class="absolute inset-0 bg-black/10 transition group-hover:bg-black/0" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 4. 校园配置 & 奖学金 (两列并排) -->
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
||||
<!-- 校园配置 -->
|
||||
<section class="rounded-lg bg-white p-6 shadow-sm dark:bg-slate-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="border-l-4 border-orange-500 pl-3 text-lg text-gray-900 font-bold dark:text-white">
|
||||
校园配置
|
||||
</h2>
|
||||
<!-- 简单下拉模拟 -->
|
||||
<span class="cursor-pointer rounded bg-gray-100 px-2 py-1 text-xs text-gray-500 dark:bg-slate-700">唐岛湾校区 ▾</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 mb-4 gap-4">
|
||||
<div v-for="item in facilities" :key="item.name" class="flex flex-col items-center rounded bg-gray-50 p-2 dark:bg-slate-700/30">
|
||||
<span class="mb-1 text-2xl">{{ item.icon }}</span>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-300">{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 overflow-x-auto pb-2">
|
||||
<div class="h-24 w-24 flex flex-shrink-0 items-center justify-center rounded bg-gray-200 text-xs text-gray-400 dark:bg-slate-700">
|
||||
食堂图片
|
||||
</div>
|
||||
<div class="h-24 w-24 flex flex-shrink-0 items-center justify-center rounded bg-gray-200 text-xs text-gray-400 dark:bg-slate-700">
|
||||
宿舍图片
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 奖学金设置 -->
|
||||
<section class="rounded-lg bg-white p-6 shadow-sm dark:bg-slate-800">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="border-l-4 border-orange-500 pl-3 text-lg text-gray-900 font-bold dark:text-white">
|
||||
奖学金设置
|
||||
</h2>
|
||||
<span class="text-xs text-gray-400">更新: 2024-06-18</span>
|
||||
</div>
|
||||
<div class="custom-scrollbar h-48 overflow-y-auto pr-2 text-sm text-gray-600 space-y-4 dark:text-gray-300">
|
||||
<div>
|
||||
<h4 class="mb-1 text-gray-800 font-bold dark:text-gray-200">
|
||||
奖学金设置
|
||||
</h4>
|
||||
<p class="text-xs leading-relaxed">
|
||||
目前学校已经建立起以各类奖学金、助学金、助学贷款、勤工助学、困难补助为主体的多元化资助体系。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="mb-1 text-gray-800 font-bold dark:text-gray-200">
|
||||
困难资助办法
|
||||
</h4>
|
||||
<p class="text-xs leading-relaxed">
|
||||
家庭经济特别困难的新生如暂时筹集不齐学费和住宿费,可在报到当天,通过学校开设的“绿色通道”办理入学手续。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧侧边栏 (保留男女比例,其他已移除) -->
|
||||
<div class="hidden lg:col-span-1 lg:block space-y-6">
|
||||
<!-- 男女比例 -->
|
||||
<div class="rounded-lg bg-white p-6 shadow-sm dark:bg-slate-800">
|
||||
<h3 class="mb-4 text-gray-900 font-bold dark:text-white">
|
||||
男女比例
|
||||
</h3>
|
||||
<div class="mb-2 flex items-end justify-between">
|
||||
<div class="text-xl text-blue-500 font-bold">
|
||||
♂ 70.12%
|
||||
</div>
|
||||
<div class="text-xl text-pink-500 font-bold">
|
||||
♀ 29.88%
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条 -->
|
||||
<div class="h-2 w-full flex overflow-hidden rounded-full bg-gray-200 dark:bg-slate-700">
|
||||
<div class="h-full bg-blue-500" style="width: 70%" />
|
||||
<div class="h-full bg-pink-500" style="width: 30%" />
|
||||
</div>
|
||||
<div class="mt-1 flex justify-between text-xs text-gray-400">
|
||||
<span>男生</span>
|
||||
<span>女生</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 此处原有:毕业去向、分数推荐、留言板、高考工具箱等,均已移除 -->
|
||||
|
||||
<!-- 占位:如果觉得右侧太空,可以放联系方式或者简单的文字链接 -->
|
||||
<div class="border border-blue-100 rounded-lg bg-blue-50 p-4 dark:border-slate-600 dark:bg-slate-700/50">
|
||||
<h4 class="mb-2 text-sm text-blue-800 font-bold dark:text-blue-300">
|
||||
招生办联系方式
|
||||
</h4>
|
||||
<p class="mb-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
电话:0532-86983086
|
||||
</p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
邮箱:zhaosheng@upc.edu.cn
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 隐藏滚动条但保留滚动功能 */
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
.custom-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.dark .custom-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: #475569;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,514 @@
|
|||
<script setup lang="ts">
|
||||
import type { FilterState } from '~/components/FilterBar.vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'UniversitiesPage',
|
||||
})
|
||||
|
||||
useHead({
|
||||
title: '找大学',
|
||||
})
|
||||
|
||||
const currentFilters = ref<FilterState | null>(null)
|
||||
const currentKeyword = ref('')
|
||||
|
||||
function handleDataChange(data: { keyword: string, filters: FilterState }) {
|
||||
console.warn('发起请求:', data.keyword, data.filters)
|
||||
currentKeyword.value = data.keyword
|
||||
currentFilters.value = data.filters
|
||||
}
|
||||
|
||||
// Define reactive data for filters
|
||||
const selectedRegions = ref<string[]>([])
|
||||
const selectedTags = ref<string[]>([])
|
||||
const selectedSchoolTypes = ref<string[]>([])
|
||||
const selectedCategories = ref<string[]>([])
|
||||
const universityName = ref('')
|
||||
|
||||
// Sample university data
|
||||
const universities = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '中国石油大学(华东)',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '山东青岛',
|
||||
tags: ['STEM', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['211', 'Double First-Class'],
|
||||
campus: 'East China',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '清华大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '北京',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '北京大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '北京',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '上海交通大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '上海',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '复旦大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '上海',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '浙江大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '浙江杭州',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '南京大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '江苏南京',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '中山大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '广东广州',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: '华中科技大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '湖北武汉',
|
||||
tags: ['STEM', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: '西安交通大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '陕西西安',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: '哈尔滨工业大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '黑龙江哈尔滨',
|
||||
tags: ['STEM', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
name: '北京理工大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '北京',
|
||||
tags: ['STEM', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
name: '电子科技大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '四川成都',
|
||||
tags: ['STEM', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 14,
|
||||
name: '南开大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '天津',
|
||||
tags: ['Comprehensive', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
{
|
||||
id: 15,
|
||||
name: '天津大学',
|
||||
logo: 'http://img1.youzy.cn/content/media/thumbs/p00026840.jpeg',
|
||||
location: '天津',
|
||||
tags: ['STEM', 'Public', 'Ministry-affiliated'],
|
||||
rankTags: ['985', '211', 'Double First-Class'],
|
||||
campus: '',
|
||||
},
|
||||
])
|
||||
|
||||
// New filter data for the updated UI
|
||||
const selectedProvinces = ref<string[]>([])
|
||||
|
||||
// Updated filter function to include province filtering
|
||||
const filteredUniversities = computed(() => {
|
||||
return universities.value.filter((university) => {
|
||||
// Filter by name
|
||||
if (
|
||||
universityName.value
|
||||
&& !university.name
|
||||
.toLowerCase()
|
||||
.includes(universityName.value.toLowerCase())
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter by province if not '不限'
|
||||
if (
|
||||
selectedProvinces.value.length > 0
|
||||
&& !selectedProvinces.value.includes('不限')
|
||||
) {
|
||||
// Extract province from location (first character or first two characters)
|
||||
const universityProvince = extractProvinceFromLocation(
|
||||
university.location,
|
||||
)
|
||||
if (!selectedProvinces.value.includes(universityProvince)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by region (simplified - in real app, would match location to regions)
|
||||
if (selectedRegions.value.length > 0) {
|
||||
// For demo, we'll skip region filtering
|
||||
}
|
||||
|
||||
// Filter by tags
|
||||
if (
|
||||
selectedTags.value.length > 0
|
||||
&& !selectedTags.value.some(tag => university.rankTags.includes(tag))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter by school type (simplified - in real app, would match school type)
|
||||
if (selectedSchoolTypes.value.length > 0) {
|
||||
// For demo, we'll skip school type filtering
|
||||
}
|
||||
|
||||
// Filter by category (simplified - in real app, would match category)
|
||||
if (selectedCategories.value.length > 0) {
|
||||
// For demo, we'll skip category filtering
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// Helper function to extract province from location
|
||||
function extractProvinceFromLocation(location: string): string {
|
||||
// Common provinces that are 2 characters
|
||||
const twoCharProvinces = [
|
||||
'北京',
|
||||
'上海',
|
||||
'天津',
|
||||
'重庆',
|
||||
'香港',
|
||||
'澳门',
|
||||
'台湾',
|
||||
]
|
||||
|
||||
for (const province of twoCharProvinces) {
|
||||
if (location.startsWith(province)) {
|
||||
return province
|
||||
}
|
||||
}
|
||||
|
||||
// For other provinces, take the first character
|
||||
if (location.length > 0) {
|
||||
return location.charAt(0)
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
// Pagination data
|
||||
const currentPage = ref(1)
|
||||
const itemsPerPage = 5
|
||||
const totalPages = computed(() =>
|
||||
Math.ceil(universities.value.length / itemsPerPage),
|
||||
)
|
||||
|
||||
// Add helper function for pagination
|
||||
function getPageNumber(index: number) {
|
||||
if (totalPages.value <= 5) {
|
||||
return index
|
||||
}
|
||||
|
||||
const currentPageValue = currentPage.value
|
||||
const totalPagesValue = totalPages.value
|
||||
|
||||
if (currentPageValue <= 3) {
|
||||
// First few pages: show 1, 2, 3, 4, 5
|
||||
return index
|
||||
}
|
||||
else if (currentPageValue >= totalPagesValue - 2) {
|
||||
// Last few pages: show last 5 pages
|
||||
return totalPagesValue - 5 + index
|
||||
}
|
||||
else {
|
||||
// Middle pages: show current page in center
|
||||
return currentPageValue - 3 + index
|
||||
}
|
||||
}
|
||||
|
||||
// Get universities for current page
|
||||
const paginatedUniversities = computed(() => {
|
||||
const startIndex = (currentPage.value - 1) * itemsPerPage
|
||||
return filteredUniversities.value.slice(
|
||||
startIndex,
|
||||
startIndex + itemsPerPage,
|
||||
)
|
||||
})
|
||||
|
||||
// Search function
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-7xl px-4 lg:px-8 sm:px-6">
|
||||
<!-- Top Section: Filters -->
|
||||
<div class="mb-8 mt-8 rounded-lg bg-white p-6 shadow">
|
||||
<div class="grid grid-cols-1 select-none gap-6 lg:grid-cols-6">
|
||||
<!-- Top Toolbar -->
|
||||
<div class="lg:col-span-4">
|
||||
<FilterBar @change="handleDataChange" />
|
||||
<div class="mb-4 mt-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold">
|
||||
大学: {{ filteredUniversities.length }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div
|
||||
v-for="university in paginatedUniversities"
|
||||
:key="university.id"
|
||||
class="flex justify-between border border-gray-200 rounded-lg p-4 transition-shadow hover:shadow-md"
|
||||
>
|
||||
<!-- Left: Logo and Name -->
|
||||
<div class="mr-4 flex items-center">
|
||||
<img
|
||||
:src="university.logo"
|
||||
:alt="university.name"
|
||||
class="mr-3 h-12 w-12 object-contain"
|
||||
>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<!-- name location -->
|
||||
<div class="w-full flex items-center">
|
||||
<h3 class="text-gray-900 font-bold">
|
||||
{{ university.name }}
|
||||
</h3>
|
||||
<div class="ml-5 flex items-center text-gray-600">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mr-1 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ university.location }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="tag in university.tags"
|
||||
:key="tag"
|
||||
class="rounded bg-gray-100 px-2 py-1 text-xs text-gray-700"
|
||||
>
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Middle: Location and Tags -->
|
||||
<div class="w-full flex flex-wrap gap-2">
|
||||
<span
|
||||
v-for="rankTag in university.rankTags"
|
||||
:key="rankTag"
|
||||
class="rounded bg-blue-100 px-2 py-1 text-xs text-blue-700"
|
||||
>
|
||||
{{ rankTag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right: Action Button -->
|
||||
<div class="flex-3">
|
||||
<button
|
||||
class="flex items-center rounded-md bg-orange-100 px-3 py-2 text-orange-700 transition-colors hover:bg-orange-200"
|
||||
>
|
||||
<span>录取概率</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="ml-1 h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-8 flex items-center justify-center space-x-2">
|
||||
<button
|
||||
:disabled="currentPage === 1"
|
||||
class="border border-gray-300 rounded-md bg-white px-4 py-2 text-sm text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50"
|
||||
@click="currentPage = 1"
|
||||
>
|
||||
首页
|
||||
</button>
|
||||
<button
|
||||
:disabled="currentPage === 1"
|
||||
class="border border-gray-300 rounded-md bg-white px-4 py-2 text-sm text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50"
|
||||
@click="currentPage = currentPage - 1"
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<!-- Page numbers -->
|
||||
<div class="flex space-x-1">
|
||||
<button
|
||||
v-for="page in Math.min(5, totalPages)"
|
||||
:key="page"
|
||||
:class="[
|
||||
currentPage === getPageNumber(page)
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white text-gray-700 hover:bg-gray-50',
|
||||
]"
|
||||
class="border border-gray-300 rounded-md px-3 px-3 py-2 py-2 text-sm text-sm font-medium"
|
||||
@click="currentPage = getPageNumber(page)"
|
||||
>
|
||||
{{ getPageNumber(page) }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
:disabled="currentPage === totalPages"
|
||||
class="border border-gray-300 rounded-md bg-white px-4 py-2 text-sm text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50"
|
||||
@click="currentPage = currentPage + 1"
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
<button
|
||||
:disabled="currentPage === totalPages"
|
||||
class="border border-gray-300 rounded-md bg-white px-4 py-2 text-sm text-gray-700 font-medium hover:bg-gray-50 disabled:opacity-50"
|
||||
@click="currentPage = totalPages"
|
||||
>
|
||||
末页
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="lg:col-span-2">
|
||||
<div class="h-full flex items-center justify-center border border-gray-200 rounded-lg bg-gray-50 p-6">
|
||||
<p class="text-gray-500">
|
||||
预留空间
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="flex space-x-4">
|
||||
<div class="mt-8 p-4 border border-dashed border-gray-300 rounded">
|
||||
<p>当前筛选条件: {{ JSON.stringify(currentFilters) }}</p>
|
||||
<p>当前搜索词: {{ currentKeyword }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<!-- Bottom Section: Reserved Space -->
|
||||
<div class="mb-8 border border-gray-200 rounded-lg bg-gray-50 p-6">
|
||||
<p class="text-center text-gray-500">
|
||||
预留空间
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Additional styling for the university cards */
|
||||
.university-card {
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.university-card:hover {
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Pagination styling */
|
||||
.pagination-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 0.375rem;
|
||||
border: 1px solid #d1d5db;
|
||||
background-color: #ffffff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
padding: 0.5rem 0.75rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.pagination-btn:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.pagination-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.current-page {
|
||||
background-color: #f97316;
|
||||
color: white;
|
||||
border-color: #f97316;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
declare interface Window {
|
||||
// extend the window
|
||||
}
|
||||
|
||||
// with unplugin-vue-markdown, markdown files can be treated as Vue components
|
||||
declare module '*.md' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<object, object, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
|
||||
const component: DefineComponent<object, object, any>
|
||||
export default component
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import { acceptHMRUpdate, defineStore } from 'pinia'
|
||||
|
||||
interface User {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
/**
|
||||
* Current logged-in user.
|
||||
*/
|
||||
const user = ref<User | null>(null)
|
||||
|
||||
// Initialize user from localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
const savedUser = localStorage.getItem('user')
|
||||
if (savedUser) {
|
||||
user.value = JSON.parse(savedUser)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Login the user.
|
||||
*
|
||||
* @param username - username to login with
|
||||
* @param password - password to login with
|
||||
*/
|
||||
function login(username: string, password: string) {
|
||||
const newUser = { username, password }
|
||||
user.value = newUser
|
||||
localStorage.setItem('user', JSON.stringify(newUser))
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout the user.
|
||||
*/
|
||||
function logout() {
|
||||
user.value = null
|
||||
localStorage.removeItem('user')
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
}
|
||||
})
|
||||
|
||||
if (import.meta.hot)
|
||||
import.meta.hot.accept(acceptHMRUpdate(useUserStore as any, import.meta.hot))
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
@import './markdown.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background: #121212;
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
#nprogress {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
background: rgb(13, 148, 136);
|
||||
opacity: 0.75;
|
||||
position: fixed;
|
||||
z-index: 1031;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
.prose pre:not(.shiki) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.prose .shiki {
|
||||
font-family: 'DM Mono', monospace;
|
||||
font-size: 1.2em;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.prose img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shiki,
|
||||
.shiki span {
|
||||
color: var(--shiki-light);
|
||||
background: var(--shiki-light-bg);
|
||||
}
|
||||
|
||||
html.dark .shiki,
|
||||
html.dark .shiki span {
|
||||
color: var(--shiki-dark);
|
||||
background: var(--shiki-dark-bg);
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
|
||||
// It's recommended to commit this file.
|
||||
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
|
||||
|
||||
declare module 'vue-router/auto-routes' {
|
||||
import type {
|
||||
RouteRecordInfo,
|
||||
ParamValue,
|
||||
ParamValueOneOrMore,
|
||||
ParamValueZeroOrMore,
|
||||
ParamValueZeroOrOne,
|
||||
} from 'vue-router'
|
||||
|
||||
/**
|
||||
* Route name map generated by unplugin-vue-router
|
||||
*/
|
||||
export interface RouteNamedMap {
|
||||
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
|
||||
'/[...all]': RouteRecordInfo<'/[...all]', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
|
||||
'/about': RouteRecordInfo<'/about', '/about', Record<never, never>, Record<never, never>>,
|
||||
'/contact-us': RouteRecordInfo<'/contact-us', '/contact-us', Record<never, never>, Record<never, never>>,
|
||||
'/demo/pop-confirm': RouteRecordInfo<'/demo/pop-confirm', '/demo/pop-confirm', Record<never, never>, Record<never, never>>,
|
||||
'/hi/[name]': RouteRecordInfo<'/hi/[name]', '/hi/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
|
||||
'/majors': RouteRecordInfo<'/majors', '/majors', Record<never, never>, Record<never, never>>,
|
||||
'/privacy-policy': RouteRecordInfo<'/privacy-policy', '/privacy-policy', Record<never, never>, Record<never, never>>,
|
||||
'/README': RouteRecordInfo<'/README', '/README', Record<never, never>, Record<never, never>>,
|
||||
'/school/[schoolCode]': RouteRecordInfo<'/school/[schoolCode]', '/school/:schoolCode', { schoolCode: ParamValue<true> }, { schoolCode: ParamValue<false> }>,
|
||||
'/simulate': RouteRecordInfo<'/simulate', '/simulate', Record<never, never>, Record<never, never>>,
|
||||
'/universities': RouteRecordInfo<'/universities', '/universities', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import type { ViteSSGContext } from 'vite-ssg'
|
||||
|
||||
export type UserModule = (ctx: ViteSSGContext) => void
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
export default {
|
||||
content: [
|
||||
'./index.html',
|
||||
'./src/**/*.{vue,js,ts,jsx,tsx,md}',
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
screens: {
|
||||
'xs': '375px',
|
||||
'sm': '640px',
|
||||
'md': '768px',
|
||||
'lg': '1024px',
|
||||
'xl': '1280px',
|
||||
'2xl': '1536px',
|
||||
'3xl': '1800px', // 自定义断点
|
||||
'tablet': '768px', // 自定义命名
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue