feat: 初始化TvTa UI桌面应用项目
完成基础项目搭建,包含: 1. Tauri 2.0 + Vue3 + Naive UI 基础框架配置 2. 多语言国际化支持 3. Pinia状态管理与主题切换 4. 系统工具方法封装(电源、网络、蓝牙等) 5. 侧边栏/顶部两种布局系统 6. 完整示例页面与基础路由配置 7. 项目构建与打包配置
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=TvTa UI
|
||||||
|
|
||||||
|
# API 请求基础路径
|
||||||
|
VITE_API_BASE_URL=http://localhost:8080
|
||||||
|
|
||||||
|
# API 请求超时时间 (毫秒)
|
||||||
|
VITE_API_TIMEOUT=5000
|
||||||
|
|
||||||
|
# 默认语言 (zh-CN / en)
|
||||||
|
VITE_DEFAULT_LOCALE=zh-CN
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# 生产环境配置
|
||||||
|
# 使用方式: pnpm build (默认使用 production 模式)
|
||||||
|
|
||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=TvTa UI
|
||||||
|
|
||||||
|
# API 请求基础路径 (生产环境通常是线上域名)
|
||||||
|
VITE_API_BASE_URL=https://api.tvta-ui.com
|
||||||
|
|
||||||
|
# API 请求超时时间
|
||||||
|
VITE_API_TIMEOUT=10000
|
||||||
|
|
||||||
|
# 默认语言
|
||||||
|
VITE_DEFAULT_LOCALE=zh-CN
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
# 测试环境配置
|
||||||
|
# 使用方式: pnpm build --mode test
|
||||||
|
# pnpm build:test
|
||||||
|
# 注意:Tauri 打包时如果需要指定 mode,需要结合前端构建命令配置,
|
||||||
|
# 但通常 Tauri 直接调用前端 build。
|
||||||
|
# 如果要打测试包的 Tauri 应用,可以在 tauri.conf.json 的 beforeBuildCommand 中临时修改为 "pnpm build:test"
|
||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=TvTa UI (Test)
|
||||||
|
|
||||||
|
# API 请求基础路径 (测试环境)
|
||||||
|
VITE_API_BASE_URL=https://test-api.tvta-ui.com
|
||||||
|
|
||||||
|
# API 请求超时时间
|
||||||
|
VITE_API_TIMEOUT=10000
|
||||||
|
|
||||||
|
# 默认语言
|
||||||
|
VITE_DEFAULT_LOCALE=zh-CN
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"Vue.volar",
|
||||||
|
"tauri-apps.tauri-vscode",
|
||||||
|
"rust-lang.rust-analyzer",
|
||||||
|
"ms-vscode.prettier-vscode",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
# TvTa UI - 现代化跨平台桌面应用模板
|
||||||
|
|
||||||
|
👋 欢迎使用 **TvTa UI**!这是一个专为小白和全栈开发者设计的现代化桌面应用模板。无论你是使用 macOS 还是 Windows,都可以通过本项目快速构建出一个美观、功能强大的桌面软件。
|
||||||
|
|
||||||
|
本项目基于最新的 **Tauri 2.0**(高性能、小体积)、**Vue 3**(易上手的前端框架)和 **Naive UI**(高颜值组件库)构建。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 核心亮点
|
||||||
|
|
||||||
|
* 🖥️ **跨平台支持**: 完美支持 macOS (M1/Intel) 和 Windows 10/11。
|
||||||
|
* 🎨 **高颜值 UI**: 内置 Naive UI + UnoCSS,提供深色/浅色模式一键切换。
|
||||||
|
* 🧩 **多布局系统**: 支持“侧边栏导航”和“顶部导航”两种布局,可随时切换。
|
||||||
|
* 🌍 **国际化支持**: 内置中英文切换功能,支持自动跟随系统或手动设置。
|
||||||
|
* 🛠️ **系统级能力**: 已封装关机、重启、WiFi 控制、蓝牙设置、开机自启等系统底层功能。
|
||||||
|
* 🔌 **后端对接**: 内置 Axios 网络请求封装,开箱即用,轻松对接 Spring Boot 等后端。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 环境准备 (小白必读)
|
||||||
|
|
||||||
|
在开始之前,请确保你的电脑上安装了以下基础软件。
|
||||||
|
|
||||||
|
### 1. 安装 Node.js (前端运行环境)
|
||||||
|
* **下载地址**: [Node.js 官网](https://nodejs.org/)
|
||||||
|
* **推荐版本**: 下载 **LTS (长期支持版)**,例如 v18 或 v20。
|
||||||
|
* **验证**: 打开终端(Mac)或CMD(Win),输入 `node -v`,出现版本号即成功。
|
||||||
|
|
||||||
|
### 2. 安装 Rust (Tauri 核心依赖)
|
||||||
|
Tauri 依赖 Rust 语言环境进行打包构建。
|
||||||
|
|
||||||
|
#### 🍎 macOS 用户
|
||||||
|
打开终端 (Terminal),复制以下命令并回车:
|
||||||
|
```bash
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
```
|
||||||
|
安装完成后,**重启终端**,输入 `rustc --version` 验证。
|
||||||
|
|
||||||
|
#### 🪟 Windows 用户
|
||||||
|
1. 访问 [Rust 官网安装页面](https://www.rust-lang.org/tools/install)。
|
||||||
|
2. 下载 `RUSTUP-INIT.EXE` (64位) 并运行。
|
||||||
|
3. 按照提示一路回车安装(需要安装 Visual Studio C++ 生成工具,安装程序会自动提示)。
|
||||||
|
4. 安装完成后,打开 CMD 或 PowerShell,输入 `rustc --version` 验证。
|
||||||
|
|
||||||
|
### 3. 安装包管理器 pnpm (推荐)
|
||||||
|
本项目推荐使用 `pnpm`,因为它比 npm 更快更省空间。
|
||||||
|
```bash
|
||||||
|
npm install -g pnpm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 快速启动
|
||||||
|
|
||||||
|
### 第一步:下载项目并进入目录
|
||||||
|
如果你已经下载了解压包,请直接在终端进入该文件夹:
|
||||||
|
```bash
|
||||||
|
cd tvta-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### 第二步:安装依赖
|
||||||
|
这一步会下载所有需要的第三方库(Vue, Naive UI 等)。
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
*(如果网络不好,可能会慢一些,请耐心等待)*
|
||||||
|
|
||||||
|
### 第三步:启动开发模式
|
||||||
|
这一步会启动一个本地窗口,你可以实时看到修改的效果。
|
||||||
|
```bash
|
||||||
|
pnpm tauri dev
|
||||||
|
```
|
||||||
|
🎉 **成功!** 你应该能看到一个名为 "TvTa UI" 的应用窗口弹出来了。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 打包发布
|
||||||
|
|
||||||
|
想把做好的软件发给朋友用?只需要一个命令!
|
||||||
|
|
||||||
|
### macOS 打包 (生成 .dmg / .app)
|
||||||
|
```bash
|
||||||
|
pnpm tauri build
|
||||||
|
```
|
||||||
|
生成的安装包在:`src-tauri/target/release/bundle/dmg/`
|
||||||
|
|
||||||
|
### Windows 打包 (生成 .exe / .msi)
|
||||||
|
```bash
|
||||||
|
pnpm tauri build
|
||||||
|
```
|
||||||
|
生成的安装包在:`src-tauri/target/release/bundle/nsis/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔌 功能使用指南
|
||||||
|
|
||||||
|
### 1. 切换布局
|
||||||
|
点击顶部导航栏的 **"布局切换"** 图标(类似网页模板的图标),可以在“侧边栏模式”和“顶部菜单模式”之间无缝切换。
|
||||||
|
|
||||||
|
### 2. 多语言切换
|
||||||
|
点击顶部导航栏的 **"地球仪"** 图标,可以切换 **简体中文** 和 **English**。
|
||||||
|
|
||||||
|
### 3. 系统功能测试
|
||||||
|
进入左侧菜单的 **"系统方法测试"** 页面,你可以尝试:
|
||||||
|
* 🔒 **锁定屏幕**
|
||||||
|
* 📡 **开关 WiFi** (需管理员权限)
|
||||||
|
* 🔊 **调节音量** (目前主要支持 macOS)
|
||||||
|
* 🚀 **设置开机自启**
|
||||||
|
|
||||||
|
### 4. 对接后端接口
|
||||||
|
打开 `src/utils/request.ts` 文件,修改 `baseURL` 为你的后端地址:
|
||||||
|
```typescript
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: 'http://localhost:8080', // 修改这里,例如 'https://api.myapp.com'
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ 常见问题 (Q&A)
|
||||||
|
|
||||||
|
**Q: 启动时报错 `failed to get serde` 或网络连接错误?**
|
||||||
|
A: 这通常是国内网络连接 Rust 官方源 (crates.io) 慢导致的。
|
||||||
|
**解决方法**: 我们已经为你配置了中科大 (USTC) 镜像源。如果仍有问题,请检查你的网络代理设置,或者尝试在终端运行 `unset https_proxy` 关闭代理。
|
||||||
|
|
||||||
|
**Q: Windows 上提示缺少 "WebView2"?**
|
||||||
|
A: Windows 10/11 通常自带 WebView2。如果是旧版系统,Tauri 安装程序会自动提示用户下载安装,无需担心。
|
||||||
|
|
||||||
|
**Q: 为什么有些系统功能(如关机)点击没反应?**
|
||||||
|
A: 在开发模式 (`pnpm tauri dev`) 下,系统权限可能受限。请尝试打包后安装运行,或者以管理员身份运行终端。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 项目结构一览
|
||||||
|
|
||||||
|
```
|
||||||
|
tvta-ui/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/ # 公共组件 (布局切换器、语言切换器等)
|
||||||
|
│ ├── layouts/ # 布局文件 (MainLayout, TopMenuLayout)
|
||||||
|
│ ├── locales/ # 多语言文件 (zh-CN.ts, en.ts)
|
||||||
|
│ ├── stores/ # 状态管理 (Pinia)
|
||||||
|
│ ├── sys-methods/ # 系统底层方法封装 (电源、网络等)
|
||||||
|
│ ├── views/ # 页面文件
|
||||||
|
│ ├── App.vue # 根组件
|
||||||
|
│ └── main.ts # 入口文件
|
||||||
|
├── src-tauri/ # Rust 后端代码 (配置、权限、图标)
|
||||||
|
└── package.json # 项目配置
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy Coding! 祝你开发愉快!** 🚀
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue').EffectScope
|
||||||
|
const computed: typeof import('vue').computed
|
||||||
|
const createApp: typeof import('vue').createApp
|
||||||
|
const customRef: typeof import('vue').customRef
|
||||||
|
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
|
||||||
|
const defineComponent: typeof import('vue').defineComponent
|
||||||
|
const effectScope: typeof import('vue').effectScope
|
||||||
|
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||||
|
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||||
|
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||||
|
const h: typeof import('vue').h
|
||||||
|
const inject: typeof import('vue').inject
|
||||||
|
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 isShallow: typeof import('vue').isShallow
|
||||||
|
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 onBeforeUnmount: typeof import('vue').onBeforeUnmount
|
||||||
|
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
|
||||||
|
const onDeactivated: typeof import('vue').onDeactivated
|
||||||
|
const onErrorCaptured: typeof import('vue').onErrorCaptured
|
||||||
|
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 onUnmounted: typeof import('vue').onUnmounted
|
||||||
|
const onUpdated: typeof import('vue').onUpdated
|
||||||
|
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
|
||||||
|
const provide: typeof import('vue').provide
|
||||||
|
const reactive: typeof import('vue').reactive
|
||||||
|
const readonly: typeof import('vue').readonly
|
||||||
|
const ref: typeof import('vue').ref
|
||||||
|
const resolveComponent: typeof import('vue').resolveComponent
|
||||||
|
const shallowReactive: typeof import('vue').shallowReactive
|
||||||
|
const shallowReadonly: typeof import('vue').shallowReadonly
|
||||||
|
const shallowRef: typeof import('vue').shallowRef
|
||||||
|
const toRaw: typeof import('vue').toRaw
|
||||||
|
const toRef: typeof import('vue').toRef
|
||||||
|
const toRefs: typeof import('vue').toRefs
|
||||||
|
const toValue: typeof import('vue').toValue
|
||||||
|
const triggerRef: typeof import('vue').triggerRef
|
||||||
|
const unref: typeof import('vue').unref
|
||||||
|
const useAttrs: typeof import('vue').useAttrs
|
||||||
|
const useCssModule: typeof import('vue').useCssModule
|
||||||
|
const useCssVars: typeof import('vue').useCssVars
|
||||||
|
const useDialog: typeof import('naive-ui').useDialog
|
||||||
|
const useId: typeof import('vue').useId
|
||||||
|
const useLoadingBar: typeof import('naive-ui').useLoadingBar
|
||||||
|
const useMessage: typeof import('naive-ui').useMessage
|
||||||
|
const useModel: typeof import('vue').useModel
|
||||||
|
const useNotification: typeof import('naive-ui').useNotification
|
||||||
|
const useSlots: typeof import('vue').useSlots
|
||||||
|
const useTemplateRef: typeof import('vue').useTemplateRef
|
||||||
|
const watch: typeof import('vue').watch
|
||||||
|
const watchEffect: typeof import('vue').watchEffect
|
||||||
|
const watchPostEffect: typeof import('vue').watchPostEffect
|
||||||
|
const watchSyncEffect: typeof import('vue').watchSyncEffect
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// oxlint-disable
|
||||||
|
// ------
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
LanguageSwitcher: typeof import('./src/components/LanguageSwitcher.vue')['default']
|
||||||
|
LayoutSwitcher: typeof import('./src/components/LayoutSwitcher.vue')['default']
|
||||||
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
|
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||||
|
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||||
|
NDivider: typeof import('naive-ui')['NDivider']
|
||||||
|
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||||
|
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
|
||||||
|
NGrid: typeof import('naive-ui')['NGrid']
|
||||||
|
NGridItem: typeof import('naive-ui')['NGridItem']
|
||||||
|
NInput: typeof import('naive-ui')['NInput']
|
||||||
|
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||||
|
NLayout: typeof import('naive-ui')['NLayout']
|
||||||
|
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||||
|
NLayoutFooter: typeof import('naive-ui')['NLayoutFooter']
|
||||||
|
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||||
|
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||||
|
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||||
|
NMenu: typeof import('naive-ui')['NMenu']
|
||||||
|
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||||
|
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||||
|
NPopover: typeof import('naive-ui')['NPopover']
|
||||||
|
NSelect: typeof import('naive-ui')['NSelect']
|
||||||
|
NSlider: typeof import('naive-ui')['NSlider']
|
||||||
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
|
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||||
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
UserProfile: typeof import('./src/components/UserProfile.vue')['default']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Tauri + Vue 3 App</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"name": "tvta-ui",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:test": "vite build --mode test",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"tauri": "tauri"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2",
|
||||||
|
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
|
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
|
"@tauri-apps/plugin-shell": "^2.3.3",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"naive-ui": "^2.43.2",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-i18n": "^9.14.5",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify/json": "^2.2.417",
|
||||||
|
"@tauri-apps/cli": "^2",
|
||||||
|
"@types/node": "^25.0.1",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"unocss": "^66.5.10",
|
||||||
|
"unplugin-auto-import": "^20.3.0",
|
||||||
|
"unplugin-vue-components": "^30.0.0",
|
||||||
|
"vite": "^6.0.3",
|
||||||
|
"vue-tsc": "^3.1.8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||||
|
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,5 @@
|
||||||
|
[source.crates-io]
|
||||||
|
replace-with = "ustc"
|
||||||
|
|
||||||
|
[source.ustc]
|
||||||
|
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
/target/
|
||||||
|
|
||||||
|
# Generated by Tauri
|
||||||
|
# will have schema files for capabilities auto-completion
|
||||||
|
/gen/schemas
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
[package]
|
||||||
|
name = "tvta-ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A Tauri App"
|
||||||
|
authors = ["you"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
# The `_lib` suffix may seem redundant but it is necessary
|
||||||
|
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||||
|
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||||
|
name = "tvta_ui_lib"
|
||||||
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tauri-build = { version = "2", features = [] }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tauri = { version = "2", features = [] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-shell = "2"
|
||||||
|
tauri-plugin-notification = "2"
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-autostart = "2"
|
||||||
|
tauri-plugin-os = "2"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "default",
|
||||||
|
"description": "Capability for the main window",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
"core:default",
|
||||||
|
"opener:default",
|
||||||
|
"shell:allow-open",
|
||||||
|
{
|
||||||
|
"identifier": "shell:allow-execute",
|
||||||
|
"allow": [
|
||||||
|
{ "name": "shutdown", "cmd": "shutdown", "args": true, "sidecar": false },
|
||||||
|
{ "name": "reboot", "cmd": "reboot", "args": true, "sidecar": false },
|
||||||
|
{ "name": "pmset", "cmd": "pmset", "args": true, "sidecar": false },
|
||||||
|
{ "name": "rundll32", "cmd": "rundll32", "args": true, "sidecar": false },
|
||||||
|
{ "name": "networksetup", "cmd": "networksetup", "args": true, "sidecar": false },
|
||||||
|
{ "name": "netsh", "cmd": "netsh", "args": true, "sidecar": false },
|
||||||
|
{ "name": "osascript", "cmd": "osascript", "args": true, "sidecar": false },
|
||||||
|
{ "name": "powershell", "cmd": "powershell", "args": true, "sidecar": false },
|
||||||
|
{ "name": "open", "cmd": "open", "args": true, "sidecar": false },
|
||||||
|
{ "name": "getmac", "cmd": "getmac", "args": true, "sidecar": false }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification:default",
|
||||||
|
"dialog:default",
|
||||||
|
"autostart:allow-enable",
|
||||||
|
"autostart:allow-disable",
|
||||||
|
"autostart:allow-is-enabled",
|
||||||
|
"os:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 974 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
|
#[tauri::command]
|
||||||
|
fn greet(name: &str) -> String {
|
||||||
|
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
|
pub fn run() {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.plugin(tauri_plugin_shell::init())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_autostart::init(tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec![])))
|
||||||
|
.plugin(tauri_plugin_os::init())
|
||||||
|
.invoke_handler(tauri::generate_handler![greet])
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
tvta_ui_lib::run()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "tvta-ui",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"identifier": "com.besty.tvta-ui",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
|
"beforeBuildCommand": "pnpm build",
|
||||||
|
"frontendDist": "../dist"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "TvTa UI",
|
||||||
|
"width": 1440,
|
||||||
|
"height": 810,
|
||||||
|
"minHeight": 600,
|
||||||
|
"minWidth": 800,
|
||||||
|
"center": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": "all",
|
||||||
|
"icon": [
|
||||||
|
"icons/32x32.png",
|
||||||
|
"icons/128x128.png",
|
||||||
|
"icons/128x128@2x.png",
|
||||||
|
"icons/icon.icns",
|
||||||
|
"icons/icon.ico"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { zhCN, dateZhCN, enUS, dateEnUS, darkTheme } from 'naive-ui'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import { useLayoutStore } from '@/stores/layout'
|
||||||
|
import MainLayout from '@/layouts/MainLayout.vue'
|
||||||
|
import TopMenuLayout from '@/layouts/TopMenuLayout.vue'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const layoutStore = useLayoutStore()
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
const theme = computed(() => themeStore.isDark ? darkTheme : null)
|
||||||
|
const naiveLocale = computed(() => locale.value === 'zh-CN' ? zhCN : enUS)
|
||||||
|
const naiveDateLocale = computed(() => locale.value === 'zh-CN' ? dateZhCN : dateEnUS)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-config-provider :locale="naiveLocale" :date-locale="naiveDateLocale" :theme="theme">
|
||||||
|
<n-global-style />
|
||||||
|
<n-loading-bar-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<n-notification-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<component :is="layoutStore.currentLayout === 'sider' ? MainLayout : TopMenuLayout" />
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-notification-provider>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { setLanguage, supportLanguages } from '@/locales'
|
||||||
|
|
||||||
|
const handleSelect = (key: string) => {
|
||||||
|
setLanguage(key)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-dropdown trigger="hover" :options="supportLanguages" @select="handleSelect" style="border-radius: 16px;">
|
||||||
|
<button class="icon-btn i-carbon-language text-xl" title="切换语言" />
|
||||||
|
</n-dropdown>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useLayoutStore, type LayoutType } from '@/stores/layout'
|
||||||
|
|
||||||
|
const layoutStore = useLayoutStore()
|
||||||
|
|
||||||
|
const layouts = [
|
||||||
|
{
|
||||||
|
type: 'sider' as LayoutType,
|
||||||
|
name: '侧边导航',
|
||||||
|
icon: 'i-carbon-page-scroll'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'top' as LayoutType,
|
||||||
|
name: '顶部导航',
|
||||||
|
icon: 'i-carbon-page-first'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-popover trigger="click" placement="bottom" style="padding: 0; border-radius: 16px;">
|
||||||
|
<template #trigger>
|
||||||
|
<button class="icon-btn i-carbon-template text-xl" title="切换布局" />
|
||||||
|
</template>
|
||||||
|
<div class="p-2 grid grid-cols-2 gap-2 w-64">
|
||||||
|
<div
|
||||||
|
v-for="layout in layouts"
|
||||||
|
:key="layout.type"
|
||||||
|
class="cursor-pointer border-2 rounded p-2 hover:border-teal-500 transition-colors"
|
||||||
|
:class="layoutStore.currentLayout === layout.type ? 'border-teal-600 bg-teal-50 dark:bg-teal-900/20' : 'border-transparent hover:bg-gray-100 dark:hover:bg-gray-800'"
|
||||||
|
@click="layoutStore.setLayout(layout.type)"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center gap-2">
|
||||||
|
<!-- 简易缩略图模拟 -->
|
||||||
|
<div class="w-full h-16 bg-gray-200 dark:bg-gray-700 rounded flex overflow-hidden border border-gray-300 dark:border-gray-600">
|
||||||
|
<template v-if="layout.type === 'sider'">
|
||||||
|
<div class="w-1/4 h-full bg-gray-400 dark:bg-gray-500"></div>
|
||||||
|
<div class="w-3/4 h-full flex flex-col">
|
||||||
|
<div class="h-1/4 w-full bg-gray-300 dark:bg-gray-600 border-b border-gray-400/20"></div>
|
||||||
|
<div class="flex-1 bg-white dark:bg-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="w-full h-full flex flex-col">
|
||||||
|
<div class="h-1/4 w-full bg-gray-400 dark:bg-gray-500"></div>
|
||||||
|
<div class="flex-1 bg-white dark:bg-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<span class="text-xs">{{ layout.name }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
// 模拟用户数据
|
||||||
|
const userInfo = ref({
|
||||||
|
username: 'Admin User',
|
||||||
|
email: 'admin@trao-ui.com',
|
||||||
|
role: '超级管理员',
|
||||||
|
avatar: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
|
||||||
|
department: '技术研发部',
|
||||||
|
joinDate: '2024-01-01'
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
message.success('已退出登录')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleProfile = () => {
|
||||||
|
message.info('跳转至个人中心')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-popover trigger="click" placement="bottom-end" style="padding: 0; border-radius: 16px;">
|
||||||
|
<template #trigger>
|
||||||
|
<n-avatar
|
||||||
|
round
|
||||||
|
size="medium"
|
||||||
|
:src="userInfo.avatar"
|
||||||
|
class="cursor-pointer hover:opacity-80 transition-opacity"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="w-72">
|
||||||
|
<!-- 头部背景 -->
|
||||||
|
<div class="h-16 bg-gradient-to-r from-[#000000] to-[#007bff] rounded-t-16px"></div>
|
||||||
|
|
||||||
|
<!-- 个人信息 -->
|
||||||
|
<div class="px-4 pb-4 -mt-8 relative">
|
||||||
|
<n-avatar
|
||||||
|
round
|
||||||
|
:size="64"
|
||||||
|
:src="userInfo.avatar"
|
||||||
|
class="border-4 border-white dark:border-gray-800"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="text-lg font-bold">{{ userInfo.username }}</div>
|
||||||
|
<div class="text-gray-500 text-sm">{{ userInfo.email }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 space-y-2">
|
||||||
|
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div class="i-carbon-badge mr-2"></div>
|
||||||
|
<span>{{ userInfo.role }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div class="i-carbon-enterprise mr-2"></div>
|
||||||
|
<span>{{ userInfo.department }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
<div class="i-carbon-calendar mr-2"></div>
|
||||||
|
<span>入职时间: {{ userInfo.joinDate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<n-divider class="my-3" />
|
||||||
|
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<n-button secondary class="flex-1" @click="handleProfile">
|
||||||
|
<template #icon>
|
||||||
|
<div class="i-carbon-user"></div>
|
||||||
|
</template>
|
||||||
|
个人中心
|
||||||
|
</n-button>
|
||||||
|
<n-button secondary type="error" class="flex-1" @click="handleLogout">
|
||||||
|
<template #icon>
|
||||||
|
<div class="i-carbon-logout"></div>
|
||||||
|
</template>
|
||||||
|
退出登录
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-popover>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, h, computed } from 'vue'
|
||||||
|
import { RouterView, useRouter } from 'vue-router'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import LayoutSwitcher from '@/components/LayoutSwitcher.vue'
|
||||||
|
import UserProfile from '@/components/UserProfile.vue'
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
const renderIcon = (icon: string) => {
|
||||||
|
return () => h('div', { class: icon })
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const router = useRouter()
|
||||||
|
const collapsed = ref(false)
|
||||||
|
|
||||||
|
const menuOptions = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('menu.home'),
|
||||||
|
key: 'home',
|
||||||
|
icon: renderIcon('i-carbon-home'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testAxios'),
|
||||||
|
key: 'test-axios',
|
||||||
|
icon: renderIcon('i-carbon-api'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testNotification'),
|
||||||
|
key: 'test-notification',
|
||||||
|
icon: renderIcon('i-carbon-notification'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testPinia'),
|
||||||
|
key: 'test-pinia',
|
||||||
|
icon: renderIcon('i-carbon-data-base'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testSysMethods'),
|
||||||
|
key: 'test-sys-methods',
|
||||||
|
icon: renderIcon('i-carbon-settings'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleUpdateValue = (key: string) => {
|
||||||
|
if (key === 'home') router.push('/')
|
||||||
|
else router.push('/' + key.replace('-', '/'))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-layout class="h-100vh" has-sider>
|
||||||
|
<n-layout-sider
|
||||||
|
bordered
|
||||||
|
collapse-mode="width"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:width="240"
|
||||||
|
:collapsed="collapsed"
|
||||||
|
show-trigger
|
||||||
|
@collapse="collapsed = true"
|
||||||
|
@expand="collapsed = false"
|
||||||
|
>
|
||||||
|
<div class="h-16 flex items-center justify-center font-bold text-xl">
|
||||||
|
<div class="i-carbon-cloud-service-management text-2xl mr-2 text-teal-600" />
|
||||||
|
<span v-if="!collapsed">TvTa UI</span>
|
||||||
|
</div>
|
||||||
|
<n-menu
|
||||||
|
:collapsed="collapsed"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:collapsed-icon-size="22"
|
||||||
|
:options="menuOptions"
|
||||||
|
@update:value="handleUpdateValue"
|
||||||
|
/>
|
||||||
|
</n-layout-sider>
|
||||||
|
<n-layout content-style="display: flex; flex-direction: column; min-height: 100%;">
|
||||||
|
<n-layout-header bordered class="h-16 flex items-center justify-between px-6">
|
||||||
|
<div class="text-lg font-medium">{{ $t('app.title') }} - {{ $t('app.footer') }}</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<LayoutSwitcher />
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<button class="icon-btn i-carbon-sun dark:i-carbon-moon text-xl" @click="themeStore.toggleTheme" />
|
||||||
|
<UserProfile />
|
||||||
|
</div>
|
||||||
|
</n-layout-header>
|
||||||
|
<n-layout-content class="flex-1" content-style="padding: 24px;">
|
||||||
|
<RouterView />
|
||||||
|
</n-layout-content>
|
||||||
|
<n-layout-footer bordered class="p-4 text-center text-gray-500 text-sm">
|
||||||
|
<div>{{ $t('app.footer') }}</div>
|
||||||
|
<div>{{ $t('app.icp') }}</div>
|
||||||
|
</n-layout-footer>
|
||||||
|
</n-layout>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h, computed } from 'vue'
|
||||||
|
import { RouterView, useRouter } from 'vue-router'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import LayoutSwitcher from '@/components/LayoutSwitcher.vue'
|
||||||
|
import UserProfile from '@/components/UserProfile.vue'
|
||||||
|
import LanguageSwitcher from '@/components/LanguageSwitcher.vue'
|
||||||
|
import { t } from '@/locales'
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
const renderIcon = (icon: string) => {
|
||||||
|
return () => h('div', { class: icon })
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const menuOptions = computed(() => [
|
||||||
|
{
|
||||||
|
label: t('menu.home'),
|
||||||
|
key: 'home',
|
||||||
|
icon: renderIcon('i-carbon-home'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testAxios'),
|
||||||
|
key: 'test-axios',
|
||||||
|
icon: renderIcon('i-carbon-api'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testNotification'),
|
||||||
|
key: 'test-notification',
|
||||||
|
icon: renderIcon('i-carbon-notification'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testPinia'),
|
||||||
|
key: 'test-pinia',
|
||||||
|
icon: renderIcon('i-carbon-data-base'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('menu.testSysMethods'),
|
||||||
|
key: 'test-sys-methods',
|
||||||
|
icon: renderIcon('i-carbon-settings'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleUpdateValue = (key: string) => {
|
||||||
|
if (key === 'home') router.push('/')
|
||||||
|
else router.push('/' + key.replace('-', '/'))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-layout class="h-100vh" content-style="display: flex; flex-direction: column;">
|
||||||
|
<n-layout-header bordered class="h-16 flex items-center justify-between px-6">
|
||||||
|
<div class="flex items-center gap-8">
|
||||||
|
<div class="flex items-center font-bold text-xl">
|
||||||
|
<div class="i-carbon-cloud-service-management text-2xl mr-2 text-teal-600" />
|
||||||
|
<span>TvTa UI</span>
|
||||||
|
</div>
|
||||||
|
<n-menu
|
||||||
|
mode="horizontal"
|
||||||
|
:options="menuOptions"
|
||||||
|
@update:value="handleUpdateValue"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<slot name="header-extra"></slot>
|
||||||
|
<LayoutSwitcher />
|
||||||
|
<LanguageSwitcher />
|
||||||
|
<button class="icon-btn i-carbon-sun dark:i-carbon-moon text-xl" @click="themeStore.toggleTheme" />
|
||||||
|
<UserProfile />
|
||||||
|
</div>
|
||||||
|
</n-layout-header>
|
||||||
|
<n-layout-content class="flex-1" content-style="padding: 24px;">
|
||||||
|
<RouterView />
|
||||||
|
</n-layout-content>
|
||||||
|
<n-layout-footer bordered class="p-4 text-center text-gray-500 text-sm">
|
||||||
|
<div>{{ $t('app.footer') }}</div>
|
||||||
|
<div>{{ $t('app.icp') }}</div>
|
||||||
|
</n-layout-footer>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
export default {
|
||||||
|
app: {
|
||||||
|
title: 'TvTa UI',
|
||||||
|
footer: '© 2025 TvTa UI Team',
|
||||||
|
icp: 'ICP License 00000000-1'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
home: 'Home',
|
||||||
|
testAxios: 'Axios Test',
|
||||||
|
testNotification: 'Notification Test',
|
||||||
|
testPinia: 'State Test',
|
||||||
|
testSysMethods: 'System Methods'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
theme: 'Theme',
|
||||||
|
language: 'Language',
|
||||||
|
light: 'Light',
|
||||||
|
dark: 'Dark'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { createI18n, useI18n } from 'vue-i18n'
|
||||||
|
import zhCN from './zh-CN'
|
||||||
|
import en from './en'
|
||||||
|
|
||||||
|
const i18n = createI18n({
|
||||||
|
legacy: false, // 使用 Composition API
|
||||||
|
locale: import.meta.env.VITE_DEFAULT_LOCALE || 'zh-CN', // 默认语言
|
||||||
|
fallbackLocale: 'en',
|
||||||
|
messages: {
|
||||||
|
'zh-CN': zhCN,
|
||||||
|
en
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default i18n
|
||||||
|
|
||||||
|
export const supportLanguages = [
|
||||||
|
{ label: '简体中文', key: 'zh-CN' },
|
||||||
|
{ label: 'English', key: 'en' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export function setLanguage(locale: string) {
|
||||||
|
if (i18n.mode === 'legacy') {
|
||||||
|
(i18n.global.locale as any).value = locale
|
||||||
|
} else {
|
||||||
|
(i18n.global.locale as any).value = locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLanguage() {
|
||||||
|
return i18n.global.locale
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封装的 t 方法,用于 setup 中
|
||||||
|
*/
|
||||||
|
export const t = (key: string) => {
|
||||||
|
return i18n.global.t(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook 形式,如果需要响应式能力推荐使用这个
|
||||||
|
*/
|
||||||
|
export const useAppI18n = () => {
|
||||||
|
return useI18n()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
export default {
|
||||||
|
app: {
|
||||||
|
title: 'TvTa UI',
|
||||||
|
footer: '© 2025 TvTa UI 团队',
|
||||||
|
icp: '京ICP备00000000号-1'
|
||||||
|
},
|
||||||
|
menu: {
|
||||||
|
home: '首页',
|
||||||
|
testAxios: 'Axios 测试',
|
||||||
|
testNotification: '通知测试',
|
||||||
|
testPinia: '状态测试',
|
||||||
|
testSysMethods: '系统方法测试'
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
theme: '主题',
|
||||||
|
language: '语言',
|
||||||
|
light: '浅色',
|
||||||
|
dark: '深色'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from '@/router'
|
||||||
|
import i18n, { t } from '@/locales'
|
||||||
|
import 'virtual:uno.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
app.use(i18n)
|
||||||
|
|
||||||
|
// 挂载全局 $t 方法,虽然 vue-i18n 插件已经自动挂载了 $t,但为了符合用户要求显式二次封装
|
||||||
|
app.config.globalProperties.$t = t
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test/axios',
|
||||||
|
name: 'test-axios',
|
||||||
|
component: () => import('../views/TestAxios.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test/notification',
|
||||||
|
name: 'test-notification',
|
||||||
|
component: () => import('../views/TestNotification.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test/pinia',
|
||||||
|
name: 'test-pinia',
|
||||||
|
component: () => import('../views/TestPinia.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test/sys-methods',
|
||||||
|
name: 'test-sys-methods',
|
||||||
|
component: () => import('../views/TestSysMethods.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export type LayoutType = 'sider' | 'top'
|
||||||
|
|
||||||
|
export const useLayoutStore = defineStore('layout', () => {
|
||||||
|
const currentLayout = ref<LayoutType>('sider')
|
||||||
|
|
||||||
|
function setLayout(layout: LayoutType) {
|
||||||
|
currentLayout.value = layout
|
||||||
|
}
|
||||||
|
|
||||||
|
return { currentLayout, setLayout }
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
export const useThemeStore = defineStore('theme', () => {
|
||||||
|
const isDark = ref(false)
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
isDark.value = !isDark.value
|
||||||
|
}
|
||||||
|
|
||||||
|
return { isDark, toggleTheme }
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Command } from '@tauri-apps/plugin-shell';
|
||||||
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置系统音量
|
||||||
|
* @param value 0-100
|
||||||
|
*/
|
||||||
|
export const setSystemVolume = async (value: number) => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'macos') {
|
||||||
|
await Command.create('osascript', ['-e', `set volume output volume ${value}`]).execute();
|
||||||
|
}
|
||||||
|
// Windows 需要 nircmd 或复杂的 PowerShell 脚本
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set volume failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置静音状态
|
||||||
|
* @param mute true 静音, false 取消静音
|
||||||
|
*/
|
||||||
|
export const setMute = async (mute: boolean) => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'macos') {
|
||||||
|
const state = mute ? 'true' : 'false';
|
||||||
|
await Command.create('osascript', ['-e', `set volume output muted ${state}`]).execute();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set mute failed:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { enable, disable, isEnabled } from '@tauri-apps/plugin-autostart';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否开机自启
|
||||||
|
* @param enable true 开启, false 关闭
|
||||||
|
*/
|
||||||
|
export const setAutoStart = async (enableAutoStart: boolean) => {
|
||||||
|
try {
|
||||||
|
if (enableAutoStart) {
|
||||||
|
await enable();
|
||||||
|
} else {
|
||||||
|
await disable();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set auto start failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否已开启开机自启
|
||||||
|
*/
|
||||||
|
export const isAutoStartEnabled = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
return await isEnabled();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Check auto start failed:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Command } from '@tauri-apps/plugin-shell';
|
||||||
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开蓝牙设置面板 (推荐)
|
||||||
|
* @description
|
||||||
|
* macOS: 打开系统设置蓝牙面板
|
||||||
|
* Windows: 打开 ms-settings:bluetooth
|
||||||
|
*/
|
||||||
|
export const openBluetoothSettings = async () => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'windows') {
|
||||||
|
await Command.create('powershell', ['start', 'ms-settings:bluetooth']).execute();
|
||||||
|
} else if (osPlatform === 'macos') {
|
||||||
|
await Command.create('open', ['/System/Library/PreferencePanes/Bluetooth.prefPane']).execute();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Open Bluetooth settings failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置蓝牙状态 (实验性)
|
||||||
|
* @description
|
||||||
|
* macOS: 尝试使用 blueutil (需用户自行安装 brew install blueutil)
|
||||||
|
* Windows: 尝试使用 PowerShell (通常需要特殊权限或模块)
|
||||||
|
* @deprecated 建议使用 openBluetoothSettings
|
||||||
|
*/
|
||||||
|
export const setBluetoothState = async (enable: boolean) => {
|
||||||
|
// 仅作为占位,实际很难通过原生命令通用控制
|
||||||
|
console.warn('setBluetoothState is experimental and requires external tools (like blueutil on macOS).');
|
||||||
|
const osPlatform = await platform();
|
||||||
|
if (osPlatform === 'macos') {
|
||||||
|
// 示例:如果安装了 blueutil
|
||||||
|
// await Command.create('blueutil', ['-p', enable ? '1' : '0']).execute();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { save } from '@tauri-apps/plugin-dialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择文件保存位置
|
||||||
|
* @param options 配置项 (默认文件名、过滤器等)
|
||||||
|
* @returns 返回选择的路径,取消则返回 null
|
||||||
|
*/
|
||||||
|
export const selectSaveLocation = async (options?: {
|
||||||
|
defaultPath?: string;
|
||||||
|
title?: string;
|
||||||
|
filters?: { name: string; extensions: string[] }[];
|
||||||
|
}): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const filePath = await save({
|
||||||
|
defaultPath: options?.defaultPath,
|
||||||
|
title: options?.title || '选择保存位置',
|
||||||
|
filters: options?.filters
|
||||||
|
});
|
||||||
|
return filePath;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Select save location failed:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
export * from './power';
|
||||||
|
export * from './network';
|
||||||
|
export * from './bluetooth';
|
||||||
|
export * from './audio';
|
||||||
|
export * from './system';
|
||||||
|
export * from './notification';
|
||||||
|
export * from './autostart';
|
||||||
|
export * from './file';
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { Command } from '@tauri-apps/plugin-shell';
|
||||||
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 WiFi 开关状态
|
||||||
|
* @param enable true 开启, false 关闭
|
||||||
|
* @description
|
||||||
|
* macOS: networksetup -setairportpower en0 on/off (默认尝试 en0)
|
||||||
|
* Windows: netsh interface set interface "Wi-Fi" admin=enabled/disabled
|
||||||
|
*/
|
||||||
|
export const setWifiState = async (enable: boolean) => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
const state = enable ? 'on' : 'off'; // macOS
|
||||||
|
const winState = enable ? 'enabled' : 'disabled'; // Windows
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'windows') {
|
||||||
|
// 注意:接口名称 "Wi-Fi" 可能因系统语言或硬件不同而不同
|
||||||
|
await Command.create('netsh', ['interface', 'set', 'interface', 'Wi-Fi', `admin=${winState}`]).execute();
|
||||||
|
} else if (osPlatform === 'macos') {
|
||||||
|
// 默认尝试 en0 (通常是 WiFi)
|
||||||
|
await Command.create('networksetup', ['-setairportpower', 'en0', state]).execute();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Set WiFi state failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 WiFi 开关状态 (仅作为示例,实际解析输出较复杂)
|
||||||
|
* @returns Promise<boolean>
|
||||||
|
*/
|
||||||
|
export const getWifiState = async (): Promise<boolean> => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'macos') {
|
||||||
|
const output = await Command.create('networksetup', ['-getairportpower', 'en0']).execute();
|
||||||
|
return output.stdout.includes(': On');
|
||||||
|
}
|
||||||
|
// Windows获取状态比较复杂,此处省略
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { sendNotification as tauriSendNotification, isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送系统级通知
|
||||||
|
* @param title 标题
|
||||||
|
* @param body 内容
|
||||||
|
*/
|
||||||
|
export const sendSystemNotification = async (title: string, body: string) => {
|
||||||
|
let permissionGranted = await isPermissionGranted();
|
||||||
|
|
||||||
|
if (!permissionGranted) {
|
||||||
|
const permission = await requestPermission();
|
||||||
|
permissionGranted = permission === 'granted';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionGranted) {
|
||||||
|
tauriSendNotification({
|
||||||
|
title,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { Command } from '@tauri-apps/plugin-shell';
|
||||||
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 立即关机
|
||||||
|
* @description 调用系统 shutdown 命令
|
||||||
|
* macOS: shutdown -h now
|
||||||
|
* Windows: shutdown /s /t 0
|
||||||
|
*/
|
||||||
|
export const shutdown = async () => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'windows') {
|
||||||
|
await Command.create('shutdown', ['/s', '/t', '0']).execute();
|
||||||
|
} else {
|
||||||
|
// macOS / Linux
|
||||||
|
await Command.create('shutdown', ['-h', 'now']).execute();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Shutdown failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重启系统
|
||||||
|
* @description 调用系统 reboot/shutdown 命令
|
||||||
|
*/
|
||||||
|
export const reboot = async () => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'windows') {
|
||||||
|
await Command.create('shutdown', ['/r', '/t', '0']).execute();
|
||||||
|
} else {
|
||||||
|
await Command.create('reboot').execute();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Reboot failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 系统睡眠
|
||||||
|
* @description
|
||||||
|
* macOS: pmset sleepnow
|
||||||
|
* Windows: rundll32.exe powrprof.dll,SetSuspendState 0,1,0
|
||||||
|
*/
|
||||||
|
export const sleep = async () => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'windows') {
|
||||||
|
await Command.create('rundll32', ['powrprof.dll,SetSuspendState', '0,1,0']).execute();
|
||||||
|
} else if (osPlatform === 'macos') {
|
||||||
|
await Command.create('pmset', ['sleepnow']).execute();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Sleep failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 锁定屏幕
|
||||||
|
* @description
|
||||||
|
* macOS: pmset displaysleepnow (模拟锁屏,或调用 loginwindow)
|
||||||
|
* Windows: rundll32.exe user32.dll,LockWorkStation
|
||||||
|
*/
|
||||||
|
export const lockScreen = async () => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'windows') {
|
||||||
|
await Command.create('rundll32', ['user32.dll,LockWorkStation']).execute();
|
||||||
|
} else if (osPlatform === 'macos') {
|
||||||
|
// macOS锁屏通常使用 pmset displaysleepnow 或者 AppleScript
|
||||||
|
// 这里使用 pmset displaysleepnow 作为近似实现
|
||||||
|
await Command.create('pmset', ['displaysleepnow']).execute();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Lock screen failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { Command } from '@tauri-apps/plugin-shell';
|
||||||
|
import { platform } from '@tauri-apps/plugin-os';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MAC 地址 (简单实现)
|
||||||
|
*/
|
||||||
|
export const getMacAddress = async (): Promise<string> => {
|
||||||
|
const osPlatform = await platform();
|
||||||
|
try {
|
||||||
|
if (osPlatform === 'macos') {
|
||||||
|
const output = await Command.create('networksetup', ['-getmacaddress', 'en0']).execute();
|
||||||
|
// Output format: "Ethernet Address: xx:xx:xx:xx:xx:xx (Device: en0)"
|
||||||
|
const match = output.stdout.match(/([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})/);
|
||||||
|
return match ? match[0] : '';
|
||||||
|
} else if (osPlatform === 'windows') {
|
||||||
|
const output = await Command.create('getmac', []).execute();
|
||||||
|
// 解析 getmac 输出
|
||||||
|
return output.stdout;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
} catch (error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import axios from 'axios'
|
||||||
|
import { createDiscreteApi } from 'naive-ui'
|
||||||
|
|
||||||
|
const { message } = createDiscreteApi(['message'])
|
||||||
|
|
||||||
|
const service = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080',
|
||||||
|
timeout: Number(import.meta.env.VITE_API_TIMEOUT) || 5000
|
||||||
|
})
|
||||||
|
|
||||||
|
service.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
// Add token here if needed
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
service.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
message.error(error.message || 'Request Error')
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default service
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center justify-center h-full">
|
||||||
|
<div class="i-carbon-rocket text-6xl text-teal-600 mb-4" />
|
||||||
|
<h1 class="text-3xl font-bold mb-2">欢迎使用 TvTa UI</h1>
|
||||||
|
<p class="text-gray-500">基于 Tauri 2.0 + Vue 3 的跨平台应用模板</p>
|
||||||
|
<div class="mt-8 flex gap-4">
|
||||||
|
<n-button type="primary" @click="$router.push('/test/axios')">开始测试</n-button>
|
||||||
|
<n-button @click="$router.push('/test/notification')">查看演示</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import request from '@/utils/request'
|
||||||
|
import { useMessage } from 'naive-ui'
|
||||||
|
|
||||||
|
const url = ref('/api/test')
|
||||||
|
const method = ref('GET')
|
||||||
|
const response = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: 'GET', value: 'GET' },
|
||||||
|
{ label: 'POST', value: 'POST' },
|
||||||
|
{ label: 'PUT', value: 'PUT' },
|
||||||
|
{ label: 'DELETE', value: 'DELETE' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const sendRequest = async () => {
|
||||||
|
loading.value = true
|
||||||
|
response.value = ''
|
||||||
|
try {
|
||||||
|
const res = await request({
|
||||||
|
url: url.value,
|
||||||
|
method: method.value
|
||||||
|
})
|
||||||
|
response.value = JSON.stringify(res, null, 2)
|
||||||
|
message.success('请求成功')
|
||||||
|
} catch (error: any) {
|
||||||
|
response.value = error.message
|
||||||
|
// message.error('请求失败') // Interceptor handles this
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card title="Axios 请求测试">
|
||||||
|
<n-space vertical>
|
||||||
|
<n-input-group>
|
||||||
|
<n-select v-model:value="method" :options="options" :style="{ width: '120px' }" />
|
||||||
|
<n-input v-model:value="url" placeholder="输入接口地址" />
|
||||||
|
<n-button type="primary" :loading="loading" @click="sendRequest">发送请求</n-button>
|
||||||
|
</n-input-group>
|
||||||
|
<n-input
|
||||||
|
v-model:value="response"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="响应结果将显示在这里"
|
||||||
|
:autosize="{ minRows: 10 }"
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useMessage, useNotification, useDialog } from 'naive-ui'
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const notification = useNotification()
|
||||||
|
const dialog = useDialog()
|
||||||
|
|
||||||
|
const showMessage = (type: 'info' | 'success' | 'warning' | 'error') => {
|
||||||
|
message[type](`这是一个 ${type} 消息`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNotification = (type: 'info' | 'success' | 'warning' | 'error') => {
|
||||||
|
notification[type]({
|
||||||
|
content: `这是一个 ${type} 通知`,
|
||||||
|
meta: '通知详情',
|
||||||
|
duration: 2500,
|
||||||
|
keepAliveOnHover: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const showDialog = () => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '警告',
|
||||||
|
content: '你确定要执行此操作吗?',
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: () => {
|
||||||
|
message.success('你点击了确定')
|
||||||
|
},
|
||||||
|
onNegativeClick: () => {
|
||||||
|
message.info('你点击了取消')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-space vertical>
|
||||||
|
<n-card title="全局消息 (Message)">
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="showMessage('info')">Info</n-button>
|
||||||
|
<n-button type="success" @click="showMessage('success')">Success</n-button>
|
||||||
|
<n-button type="warning" @click="showMessage('warning')">Warning</n-button>
|
||||||
|
<n-button type="error" @click="showMessage('error')">Error</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-card title="通知 (Notification)">
|
||||||
|
<n-space>
|
||||||
|
<n-button @click="showNotification('info')">Info</n-button>
|
||||||
|
<n-button type="success" @click="showNotification('success')">Success</n-button>
|
||||||
|
<n-button type="warning" @click="showNotification('warning')">Warning</n-button>
|
||||||
|
<n-button type="error" @click="showNotification('error')">Error</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-card title="对话框 (Dialog)">
|
||||||
|
<n-button type="warning" @click="showDialog">打开对话框</n-button>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
const { isDark } = storeToRefs(themeStore)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n-card title="Pinia 状态管理测试">
|
||||||
|
<div class="mb-4">
|
||||||
|
当前主题模式:
|
||||||
|
<n-tag :type="isDark ? 'info' : 'warning'">
|
||||||
|
{{ isDark ? '深色模式' : '浅色模式' }}
|
||||||
|
</n-tag>
|
||||||
|
</div>
|
||||||
|
<n-button @click="themeStore.toggleTheme">切换主题</n-button>
|
||||||
|
<p class="mt-4 text-gray-500">
|
||||||
|
点击切换按钮,观察顶部栏和整个应用的主题变化。这证明了 Pinia 状态在组件间是共享和响应式的。
|
||||||
|
</p>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useMessage, useDialog } from 'naive-ui';
|
||||||
|
import {
|
||||||
|
shutdown,
|
||||||
|
reboot,
|
||||||
|
sleep,
|
||||||
|
lockScreen,
|
||||||
|
setWifiState,
|
||||||
|
getWifiState,
|
||||||
|
openBluetoothSettings,
|
||||||
|
setSystemVolume,
|
||||||
|
setMute,
|
||||||
|
getMacAddress,
|
||||||
|
sendSystemNotification,
|
||||||
|
setAutoStart,
|
||||||
|
isAutoStartEnabled,
|
||||||
|
selectSaveLocation,
|
||||||
|
} from '@/sys-methods';
|
||||||
|
|
||||||
|
const message = useMessage();
|
||||||
|
const dialog = useDialog();
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const wifiEnabled = ref(false);
|
||||||
|
const volume = ref(50);
|
||||||
|
const isMuted = ref(false);
|
||||||
|
const macAddress = ref('');
|
||||||
|
const autoStartEnabled = ref(false);
|
||||||
|
const savePath = ref('');
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
wifiEnabled.value = await getWifiState();
|
||||||
|
macAddress.value = await getMacAddress();
|
||||||
|
autoStartEnabled.value = await isAutoStartEnabled();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Init failed', e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 电源管理操作确认
|
||||||
|
const confirmAction = (action: string, handler: () => Promise<void>) => {
|
||||||
|
dialog.warning({
|
||||||
|
title: '危险操作确认',
|
||||||
|
content: `确定要执行 ${action} 吗?`,
|
||||||
|
positiveText: '确定',
|
||||||
|
negativeText: '取消',
|
||||||
|
onPositiveClick: async () => {
|
||||||
|
try {
|
||||||
|
await handler();
|
||||||
|
message.success(`${action} 指令已发送`);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`执行失败: ${error.message || error}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 事件处理
|
||||||
|
const handleShutdown = () => confirmAction('关机', shutdown);
|
||||||
|
const handleReboot = () => confirmAction('重启', reboot);
|
||||||
|
const handleSleep = () => confirmAction('睡眠', sleep);
|
||||||
|
const handleLockScreen = async () => {
|
||||||
|
try {
|
||||||
|
await lockScreen();
|
||||||
|
message.success('已锁定屏幕');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`锁定失败: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWifiChange = async (value: boolean) => {
|
||||||
|
try {
|
||||||
|
await setWifiState(value);
|
||||||
|
wifiEnabled.value = value;
|
||||||
|
message.success(`WiFi 已${value ? '开启' : '关闭'}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`设置 WiFi 失败: ${error}`);
|
||||||
|
// 恢复状态
|
||||||
|
wifiEnabled.value = !value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBluetooth = async () => {
|
||||||
|
try {
|
||||||
|
await openBluetoothSettings();
|
||||||
|
message.success('已打开蓝牙设置');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`打开蓝牙设置失败: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVolumeChange = async (value: number) => {
|
||||||
|
try {
|
||||||
|
await setSystemVolume(value);
|
||||||
|
volume.value = value;
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`设置音量失败: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMuteChange = async (value: boolean) => {
|
||||||
|
try {
|
||||||
|
await setMute(value);
|
||||||
|
isMuted.value = value;
|
||||||
|
message.success(value ? '已静音' : '已取消静音');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`设置静音失败: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNotification = async () => {
|
||||||
|
try {
|
||||||
|
await sendSystemNotification('系统测试', '这是一条来自 Trao UI 的系统通知测试');
|
||||||
|
message.success('通知已发送');
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`发送通知失败: ${error}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoStartChange = async (value: boolean) => {
|
||||||
|
try {
|
||||||
|
await setAutoStart(value);
|
||||||
|
autoStartEnabled.value = value;
|
||||||
|
message.success(`开机自启已${value ? '开启' : '关闭'}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
message.error(`设置开机自启失败: ${error}`);
|
||||||
|
autoStartEnabled.value = !value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectSavePath = async () => {
|
||||||
|
const path = await selectSaveLocation();
|
||||||
|
if (path) {
|
||||||
|
savePath.value = path;
|
||||||
|
message.success(`已选择路径: ${path}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="space-y-6">
|
||||||
|
<n-card title="电源与会话管理">
|
||||||
|
<div class="flex gap-4 flex-wrap">
|
||||||
|
<n-button type="error" @click="handleShutdown">关机</n-button>
|
||||||
|
<n-button type="warning" @click="handleReboot">重启</n-button>
|
||||||
|
<n-button type="info" @click="handleSleep">睡眠</n-button>
|
||||||
|
<n-button type="primary" secondary @click="handleLockScreen">锁定屏幕</n-button>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
|
||||||
|
<n-grid :cols="2" :x-gap="12" :y-gap="12">
|
||||||
|
<n-grid-item>
|
||||||
|
<n-card title="网络与连接">
|
||||||
|
<n-space vertical>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>WiFi 开关 (macOS/Win)</span>
|
||||||
|
<n-switch :value="wifiEnabled" @update:value="handleWifiChange" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>蓝牙设置</span>
|
||||||
|
<n-button size="small" @click="handleBluetooth">打开面板</n-button>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>MAC 地址</span>
|
||||||
|
<n-tag type="success">{{ macAddress || '获取中...' }}</n-tag>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item>
|
||||||
|
<n-card title="音量控制 (macOS)">
|
||||||
|
<n-space vertical>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<span>音量</span>
|
||||||
|
<n-slider v-model:value="volume" :step="10" @update:value="handleVolumeChange" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between mt-2">
|
||||||
|
<span>静音</span>
|
||||||
|
<n-switch :value="isMuted" @update:value="handleMuteChange" />
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
|
||||||
|
<n-card title="系统功能">
|
||||||
|
<n-space vertical>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>系统通知</span>
|
||||||
|
<n-button type="primary" @click="handleNotification">发送测试通知</n-button>
|
||||||
|
</div>
|
||||||
|
<n-divider />
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>开机自启</span>
|
||||||
|
<n-switch :value="autoStartEnabled" @update:value="handleAutoStartChange" />
|
||||||
|
</div>
|
||||||
|
<n-divider />
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>文件保存路径</span>
|
||||||
|
<div class="flex gap-2 items-center">
|
||||||
|
<span class="text-xs text-gray-400 truncate max-w-[200px]">{{ savePath || '未选择' }}</span>
|
||||||
|
<n-button size="small" @click="handleSelectSavePath">选择...</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
readonly VITE_APP_TITLE: string
|
||||||
|
readonly VITE_API_BASE_URL: string
|
||||||
|
readonly VITE_API_TIMEOUT: string
|
||||||
|
readonly VITE_DEFAULT_LOCALE: string
|
||||||
|
// 更多环境变量...
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type { DefineComponent } from 'vue'
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { defineConfig, presetUno, presetAttributify, presetIcons } from 'unocss'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
presets: [
|
||||||
|
presetUno(),
|
||||||
|
presetAttributify(),
|
||||||
|
presetIcons({
|
||||||
|
scale: 1.2,
|
||||||
|
warn: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
shortcuts: [
|
||||||
|
['btn', 'px-4 py-1 rounded inline-block bg-teal-600 text-white cursor-pointer hover:bg-teal-700 disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50'],
|
||||||
|
['icon-btn', 'text-[0.9em] inline-block cursor-pointer select-none opacity-75 transition duration-200 ease-in-out hover:opacity-100 hover:text-teal-600']
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import UnoCSS from 'unocss/vite'
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(async () => ({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
UnoCSS(),
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
'vue',
|
||||||
|
{
|
||||||
|
'naive-ui': [
|
||||||
|
'useDialog',
|
||||||
|
'useMessage',
|
||||||
|
'useNotification',
|
||||||
|
'useLoadingBar'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, 'src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clearScreen: false,
|
||||||
|
server: {
|
||||||
|
port: 1420,
|
||||||
|
strictPort: true,
|
||||||
|
host: host || false,
|
||||||
|
hmr: host
|
||||||
|
? {
|
||||||
|
protocol: "ws",
|
||||||
|
host,
|
||||||
|
port: 1421,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
watch: {
|
||||||
|
ignored: ["**/src-tauri/**"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "part7"
|
||||||
|
version = "0.1.0"
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
[package]
|
||||||
|
name = "part7"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
fn main() {
|
||||||
|
println!("===== 题目 4-1:成绩统计分析器 =====\n");
|
||||||
|
ex4_1_scores();
|
||||||
|
println!("\n===== 题目 4-2:斐波那契数列生成器 =====\n");
|
||||||
|
ex4_2_fibonacci();
|
||||||
|
println!("\n===== 题目 4-3:购物车模拟 =====\n");
|
||||||
|
ex4_3_cart();
|
||||||
|
println!("\n===== 题目 4-4:单词统计器 =====\n");
|
||||||
|
ex4_4_word_count();
|
||||||
|
println!("\n===== 题目 4-5:矩阵转置 =====\n");
|
||||||
|
ex4_5_transpose();
|
||||||
|
println!("\n===== 题目 4-6:简易栈实现与括号匹配 =====\n");
|
||||||
|
ex4_6_stack();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 4-1 成绩统计分析器 =====================
|
||||||
|
fn ex4_1_scores() {
|
||||||
|
let scores = vec![85, 42, 96, 78, 55, 88, 91, 63, 100, 74];
|
||||||
|
println!("成绩列表: {:?}", scores);
|
||||||
|
|
||||||
|
let max = scores.iter().max().unwrap();
|
||||||
|
let min = scores.iter().min().unwrap();
|
||||||
|
let avg = scores.iter().sum::<i32>() as f64 / scores.len() as f64;
|
||||||
|
|
||||||
|
println!("最高分: {}", max);
|
||||||
|
println!("最低分: {}", min);
|
||||||
|
println!("平均分: {:.1}", avg);
|
||||||
|
|
||||||
|
let failing: Vec<i32> = scores.iter().filter(|&&s| s < 60).copied().collect();
|
||||||
|
println!("不及格成绩: {:?}", failing);
|
||||||
|
|
||||||
|
let has_perfect = scores.contains(&100);
|
||||||
|
println!("有满分: {}", if has_perfect { "是" } else { "否" });
|
||||||
|
|
||||||
|
let mut sorted = scores.clone();
|
||||||
|
sorted.sort_by(|a, b| b.cmp(a));
|
||||||
|
println!("排序后(高到低): {:?}", sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 4-2 斐波那契数列生成器 =====================
|
||||||
|
fn fibonacci(n: usize) -> Vec<u64> {
|
||||||
|
match n {
|
||||||
|
0 => vec![],
|
||||||
|
1 => vec![1],
|
||||||
|
_ => {
|
||||||
|
let mut fib = vec![1, 1];
|
||||||
|
for _ in 2..n {
|
||||||
|
let next = fib[fib.len() - 1] + fib[fib.len() - 2];
|
||||||
|
fib.push(next);
|
||||||
|
}
|
||||||
|
fib
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ex4_2_fibonacci() {
|
||||||
|
let fib = fibonacci(20);
|
||||||
|
println!("{:?}", fib);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 4-3 购物车模拟 =====================
|
||||||
|
struct Item {
|
||||||
|
name: String,
|
||||||
|
price: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_item(cart: &mut Vec<Item>, name: &str, price: f64) {
|
||||||
|
cart.push(Item {
|
||||||
|
name: name.to_string(),
|
||||||
|
price,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_item(cart: &mut Vec<Item>, name: &str) {
|
||||||
|
if let Some(pos) = cart.iter().position(|item| item.name == name) {
|
||||||
|
cart.remove(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn total_price(cart: &Vec<Item>) -> f64 {
|
||||||
|
cart.iter().map(|item| item.price).sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_items(cart: &Vec<Item>) {
|
||||||
|
for item in cart {
|
||||||
|
println!(" - {}: ¥{:.2}", item.name, item.price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ex4_3_cart() {
|
||||||
|
let mut cart = Vec::new();
|
||||||
|
|
||||||
|
add_item(&mut cart, "苹果", 3.50);
|
||||||
|
add_item(&mut cart, "香蕉", 2.00);
|
||||||
|
add_item(&mut cart, "牛奶", 6.80);
|
||||||
|
add_item(&mut cart, "面包", 5.00);
|
||||||
|
add_item(&mut cart, "鸡蛋", 12.00);
|
||||||
|
|
||||||
|
println!("购物车清单:");
|
||||||
|
list_items(&cart);
|
||||||
|
println!("当前总价: ¥{:.2}", total_price(&cart));
|
||||||
|
|
||||||
|
remove_item(&mut cart, "香蕉");
|
||||||
|
|
||||||
|
println!("\n移除\"香蕉\"后:");
|
||||||
|
println!("购物车清单:");
|
||||||
|
list_items(&cart);
|
||||||
|
println!("当前总价: ¥{:.2}", total_price(&cart));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 4-4 单词统计器 =====================
|
||||||
|
fn ex4_4_word_count() {
|
||||||
|
let text = "the quick brown fox jumps over the lazy dog the fox was quick";
|
||||||
|
let words: Vec<&str> = text.split_whitespace().collect();
|
||||||
|
|
||||||
|
let mut stats: Vec<(String, i32)> = Vec::new();
|
||||||
|
|
||||||
|
for word in &words {
|
||||||
|
if let Some(pos) = stats.iter().position(|(w, _)| w == word) {
|
||||||
|
stats[pos].1 += 1;
|
||||||
|
} else {
|
||||||
|
stats.push((word.to_string(), 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stats.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
println!("单词统计结果:");
|
||||||
|
for (word, count) in &stats {
|
||||||
|
println!(" {}: {}", word, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 4-5 矩阵转置 =====================
|
||||||
|
fn transpose(matrix: &Vec<Vec<i32>>) -> Vec<Vec<i32>> {
|
||||||
|
if matrix.is_empty() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = matrix.len();
|
||||||
|
let cols = matrix[0].len();
|
||||||
|
|
||||||
|
(0..cols)
|
||||||
|
.map(|col| (0..rows).map(|row| matrix[row][col]).collect())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ex4_5_transpose() {
|
||||||
|
let matrix = vec![vec![1, 2, 3], vec![4, 5, 6], vec![7, 8, 9]];
|
||||||
|
|
||||||
|
println!("原始矩阵:");
|
||||||
|
for row in &matrix {
|
||||||
|
println!("{:?}", row);
|
||||||
|
}
|
||||||
|
|
||||||
|
let transposed = transpose(&matrix);
|
||||||
|
|
||||||
|
println!("\n转置后:");
|
||||||
|
for row in &transposed {
|
||||||
|
println!("{:?}", row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 4-6 简易栈实现与括号匹配 =====================
|
||||||
|
struct Stack<T> {
|
||||||
|
data: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Stack<T> {
|
||||||
|
fn new() -> Self {
|
||||||
|
Stack { data: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, item: T) {
|
||||||
|
self.data.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop(&mut self) -> Option<T> {
|
||||||
|
self.data.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peek(&self) -> Option<&T> {
|
||||||
|
self.data.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.data.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.data.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_balanced(s: &str) -> bool {
|
||||||
|
let mut stack = Stack::new();
|
||||||
|
|
||||||
|
for ch in s.chars() {
|
||||||
|
match ch {
|
||||||
|
'(' | '[' | '{' => stack.push(ch),
|
||||||
|
')' => {
|
||||||
|
if stack.pop() != Some('(') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
']' => {
|
||||||
|
if stack.pop() != Some('[') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'}' => {
|
||||||
|
if stack.pop() != Some('{') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ex4_6_stack() {
|
||||||
|
let tests = [
|
||||||
|
("()", true),
|
||||||
|
("()[]{}", true),
|
||||||
|
("(]", false),
|
||||||
|
("([)]", false),
|
||||||
|
("{[]}", true),
|
||||||
|
("(((", false),
|
||||||
|
("", true),
|
||||||
|
];
|
||||||
|
|
||||||
|
println!("括号匹配测试:");
|
||||||
|
for (input, expected) in tests {
|
||||||
|
let result = is_balanced(input);
|
||||||
|
let status = if result == expected { "✓" } else { "✗" };
|
||||||
|
println!(
|
||||||
|
" {} \"{}\" -> {} (期望: {})",
|
||||||
|
status, input, result, expected
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||