mirror of
https://github.com/ZeroCatDev/Classworks.git
synced 2026-06-16 21:05:09 +00:00
Compare commits
2 Commits
8479ab4fff
...
5d0b0bb175
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d0b0bb175 | ||
|
|
e4116d7ec4 |
72
CLAUDE.md
Normal file
72
CLAUDE.md
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Classworks (作业板) is a homework board widget for classroom large screens. It's a Vue 3 + Vuetify 3 PWA with real-time sync via Socket.IO. The UI is in Chinese.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install # Install dependencies
|
||||||
|
pnpm run dev # Dev server at localhost:3031 (network-accessible)
|
||||||
|
pnpm run build # Production build (auto-runs prebuild to regenerate sound list)
|
||||||
|
pnpm run preview # Preview production build
|
||||||
|
pnpm run lint # ESLint with auto-fix
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Framework**: Vue 3 (Composition API + Options API mixed), JavaScript (no TypeScript)
|
||||||
|
- **UI**: Vuetify 3 (Material Design 3), `@mdi/font` icons, SCSS
|
||||||
|
- **State**: Pinia 3
|
||||||
|
- **Routing**: Vue Router 4 with file-based routes (`unplugin-vue-router` + `vite-plugin-vue-layouts`)
|
||||||
|
- **Build**: Vite 5, pnpm
|
||||||
|
- **Real-time**: Socket.IO client (singleton in `src/utils/socketClient.js`)
|
||||||
|
- **Data**: Pluggable KV provider abstraction (`src/utils/dataProvider.js`) with IndexedDB local and HTTP server backends
|
||||||
|
- **PWA**: `vite-plugin-pwa` with Workbox service worker
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Data Layer
|
||||||
|
|
||||||
|
`src/utils/dataProvider.js` abstracts data operations. It routes to either:
|
||||||
|
- `src/utils/providers/kvLocalProvider.js` — IndexedDB via `idb`
|
||||||
|
- `src/utils/providers/kvServerProvider.js` — HTTP API via axios
|
||||||
|
|
||||||
|
Server failover is handled by `src/utils/serverRotation.js`.
|
||||||
|
|
||||||
|
### Real-time Layer
|
||||||
|
|
||||||
|
`src/utils/socketClient.js` — Socket.IO singleton with room-based token join/leave for live updates.
|
||||||
|
|
||||||
|
### Settings Layer
|
||||||
|
|
||||||
|
`src/utils/settings.js` — Comprehensive localStorage-based settings with typed definitions, defaults, and legacy migration. ~600 lines.
|
||||||
|
|
||||||
|
### UI Layer
|
||||||
|
|
||||||
|
File-based routing: each `.vue` in `src/pages/` becomes a route. Layouts in `src/layouts/`. The main dashboard is `src/pages/index.vue` (78KB — the core view composing homework grid, time card, noise monitor, random picker, exam schedule, etc.).
|
||||||
|
|
||||||
|
Components are organized by feature:
|
||||||
|
- `src/components/home/` — Home page components
|
||||||
|
- `src/components/settings/` — Settings cards
|
||||||
|
- `src/components/auth/` — Authentication flow
|
||||||
|
- `src/components/attendance/` — Attendance management
|
||||||
|
- `src/components/common/` — Shared components
|
||||||
|
|
||||||
|
### Key Utilities
|
||||||
|
|
||||||
|
- `src/axios/axios.js` — Axios instance with auth interceptors and rate limit handling
|
||||||
|
- `src/utils/api.js` — API helpers, namespace info, server rotation
|
||||||
|
- `src/utils/visitorId.js` — FingerprintJS device identification
|
||||||
|
- `src/utils/soundList.js` — Auto-generated from `public/sounds/` by `scripts/generate-sound-list.js` (runs as `prebuild`)
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- 2-space indent, trim trailing whitespace (`.editorconfig`)
|
||||||
|
- Path alias: `@/` maps to `src/` (`jsconfig.json`)
|
||||||
|
- ESLint flat config (ESLint 9) with Vue recommended rules (`eslint.config.js`)
|
||||||
|
- Mixed Composition API and Options API usage
|
||||||
|
- No TypeScript
|
||||||
@ -499,11 +499,11 @@ export default {
|
|||||||
|
|
||||||
const openFeedback = () => {
|
const openFeedback = () => {
|
||||||
// 打开反馈对话框
|
// 打开反馈对话框
|
||||||
if (typeof window.openSentryFeedback === 'function') {
|
// if (typeof window.openSentryFeedback === 'function') {
|
||||||
window.openSentryFeedback();
|
// window.openSentryFeedback();
|
||||||
} else {
|
// } else {
|
||||||
console.warn('Sentry Feedback 功能不可用');
|
console.warn('Sentry Feedback 功能不可用');
|
||||||
}
|
//}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDonationLink = () => {
|
const openDonationLink = () => {
|
||||||
|
|||||||
16
src/main.js
16
src/main.js
@ -29,14 +29,14 @@ app.mount('#app')
|
|||||||
// ====== 以下全部异步,不阻塞首屏渲染 ======
|
// ====== 以下全部异步,不阻塞首屏渲染 ======
|
||||||
|
|
||||||
// 异步初始化 Sentry(延迟到首帧渲染完成后,防止 errorHandler 与渲染周期冲突)
|
// 异步初始化 Sentry(延迟到首帧渲染完成后,防止 errorHandler 与渲染周期冲突)
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
import('./utils/sentry').then(({ initSentry }) => {
|
// import('./utils/sentry').then(({ initSentry }) => {
|
||||||
const router = app.config.globalProperties.$router
|
// const router = app.config.globalProperties.$router
|
||||||
initSentry(app, router)
|
// initSentry(app, router)
|
||||||
}).catch((err) => {
|
// }).catch((err) => {
|
||||||
console.warn('Sentry 初始化失败:', err)
|
// console.warn('Sentry 初始化失败:', err)
|
||||||
})
|
// })
|
||||||
}, 1000)
|
//}, 1000)
|
||||||
|
|
||||||
// 异步加载 Clarity(在页面完全加载后)
|
// 异步加载 Clarity(在页面完全加载后)
|
||||||
const loadClarity = async () => {
|
const loadClarity = async () => {
|
||||||
|
|||||||
@ -619,7 +619,7 @@ const ExamConfigEditor = defineAsyncComponent({
|
|||||||
loader: () => import("@/components/ExamConfigEditor.vue"),
|
loader: () => import("@/components/ExamConfigEditor.vue"),
|
||||||
delay: 0,
|
delay: 0,
|
||||||
});
|
});
|
||||||
import dataProvider from "@/utils/dataProvider";
|
import dataProvider, {syncManager} from "@/utils/dataProvider";
|
||||||
import { useExamStore } from "@/stores/examStore";
|
import { useExamStore } from "@/stores/examStore";
|
||||||
import {
|
import {
|
||||||
getSetting,
|
getSetting,
|
||||||
@ -1207,6 +1207,9 @@ export default {
|
|||||||
// 实时频道:加入设备房间并监听键变化
|
// 实时频道:加入设备房间并监听键变化
|
||||||
this.setupRealtimeChannel();
|
this.setupRealtimeChannel();
|
||||||
|
|
||||||
|
// 初始化离线同步管理器
|
||||||
|
syncManager.init();
|
||||||
|
|
||||||
// 初始化 Token 显示信息
|
// 初始化 Token 显示信息
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.updateTokenDisplayInfo();
|
this.updateTokenDisplayInfo();
|
||||||
@ -1224,6 +1227,7 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
|
syncManager.destroy();
|
||||||
if (this.unwatchSettings) {
|
if (this.unwatchSettings) {
|
||||||
this.unwatchSettings();
|
this.unwatchSettings();
|
||||||
}
|
}
|
||||||
@ -1570,8 +1574,12 @@ export default {
|
|||||||
};
|
};
|
||||||
this.state.synced = true;
|
this.state.synced = true;
|
||||||
this.state.showNoDataMessage = false;
|
this.state.showNoDataMessage = false;
|
||||||
|
if (response.fromCache) {
|
||||||
|
this.$message.warning("离线模式", "显示缓存数据,联网后将自动同步");
|
||||||
|
} else {
|
||||||
this.$message.success("下载成功", "数据已更新");
|
this.$message.success("下载成功", "数据已更新");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 数据加载失败时的处理
|
// 数据加载失败时的处理
|
||||||
console.error("数据加载失败:", error);
|
console.error("数据加载失败:", error);
|
||||||
@ -1672,7 +1680,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.state.synced = true;
|
this.state.synced = true;
|
||||||
|
if (response.queuedForSync) {
|
||||||
|
this.$message.warning("已保存到本地", "网络恢复后将自动同步到服务器");
|
||||||
|
} else {
|
||||||
this.$message.success(response.message || "保存成功");
|
this.$message.success(response.message || "保存成功");
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.loading.upload = false;
|
this.loading.upload = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -230,6 +230,7 @@
|
|||||||
<background-settings-card border />
|
<background-settings-card border />
|
||||||
</v-tabs-window-item>
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
|
||||||
<v-tabs-window-item value="developer">
|
<v-tabs-window-item value="developer">
|
||||||
<settings-card
|
<settings-card
|
||||||
border
|
border
|
||||||
|
|||||||
@ -10,163 +10,133 @@ export const formatError = (message, code = "UNKNOWN_ERROR") => ({
|
|||||||
error: {code, message},
|
error: {code, message},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Cache key prefix to avoid collision with kv-local mode data
|
||||||
|
const CACHE_PREFIX = "_cache:";
|
||||||
|
|
||||||
|
function isServerError(result) {
|
||||||
|
return result && result.success === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isNetworkError(result) {
|
||||||
|
return isServerError(result) && result.error?.code === "NETWORK_ERROR";
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sync manager: flushes queued writes when back online ---
|
||||||
|
|
||||||
|
let _onlineHandler = null;
|
||||||
|
let _flushing = false;
|
||||||
|
|
||||||
|
async function flushSyncQueue() {
|
||||||
|
if (_flushing) return;
|
||||||
|
_flushing = true;
|
||||||
|
try {
|
||||||
|
const queueResult = await kvLocalProvider.getSyncQueue();
|
||||||
|
if (queueResult.success === false || !Array.isArray(queueResult) || queueResult.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const entry of queueResult) {
|
||||||
|
try {
|
||||||
|
const result = await kvServerProvider.saveData(entry.key, entry.data);
|
||||||
|
if (result.success !== false) {
|
||||||
|
await kvLocalProvider.removeFromSyncQueue(entry.key);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If a single item fails, stop — will retry on next online event
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_flushing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const syncManager = {
|
||||||
|
init() {
|
||||||
|
if (_onlineHandler) return;
|
||||||
|
_onlineHandler = () => flushSyncQueue();
|
||||||
|
window.addEventListener("online", _onlineHandler);
|
||||||
|
// Attempt flush on startup in case items were queued before last exit
|
||||||
|
if (navigator.onLine) flushSyncQueue();
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
if (_onlineHandler) {
|
||||||
|
window.removeEventListener("online", _onlineHandler);
|
||||||
|
_onlineHandler = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
flushNow: flushSyncQueue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper: check if we should use the server provider
|
||||||
|
function useServerProvider() {
|
||||||
|
const provider = getSetting("server.provider");
|
||||||
|
return provider === "kv-server" || provider === "classworkscloud";
|
||||||
|
}
|
||||||
|
|
||||||
// Main data provider with simplified API
|
// Main data provider with simplified API
|
||||||
export default {
|
export default {
|
||||||
// Provider API methods
|
// Provider API methods
|
||||||
loadData: async (key) => {
|
loadData: async (key) => {
|
||||||
const provider = getSetting("server.provider");
|
if (!useServerProvider()) {
|
||||||
const useServer =
|
|
||||||
provider === "kv-server" || provider === "classworkscloud";
|
|
||||||
|
|
||||||
if (useServer) {
|
|
||||||
return kvServerProvider.loadData(key);
|
|
||||||
} else {
|
|
||||||
return kvLocalProvider.loadData(key);
|
return kvLocalProvider.loadData(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server mode: network-first with cache fallback
|
||||||
|
const result = await kvServerProvider.loadData(key);
|
||||||
|
|
||||||
|
if (!isNetworkError(result)) {
|
||||||
|
// Success or non-network error (e.g. NOT_FOUND) — cache on success
|
||||||
|
if (result.success !== false) {
|
||||||
|
kvLocalProvider.saveData(CACHE_PREFIX + key, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network error — try local cache
|
||||||
|
const cached = await kvLocalProvider.loadData(CACHE_PREFIX + key);
|
||||||
|
if (cached.success !== false) {
|
||||||
|
return {...cached, fromCache: true};
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
saveData: async (key, data) => {
|
saveData: async (key, data) => {
|
||||||
const provider = getSetting("server.provider");
|
if (!useServerProvider()) {
|
||||||
const useServer =
|
|
||||||
provider === "kv-server" || provider === "classworkscloud";
|
|
||||||
|
|
||||||
if (useServer) {
|
|
||||||
return kvServerProvider.saveData(key, data);
|
|
||||||
} else {
|
|
||||||
return kvLocalProvider.saveData(key, data);
|
return kvLocalProvider.saveData(key, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server mode: write-through — persist locally first
|
||||||
|
await kvLocalProvider.saveData(CACHE_PREFIX + key, data);
|
||||||
|
|
||||||
|
const result = await kvServerProvider.saveData(key, data);
|
||||||
|
|
||||||
|
if (result.success !== false) {
|
||||||
|
// Server save succeeded — remove from sync queue if present
|
||||||
|
await kvLocalProvider.removeFromSyncQueue(key);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server save failed — queue for later sync
|
||||||
|
await kvLocalProvider.addToSyncQueue({key, data, timestamp: Date.now()});
|
||||||
|
return {success: true, queuedForSync: true};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取键名列表
|
|
||||||
* @param {Object} options - 查询选项
|
|
||||||
* @param {string} options.sortBy - 排序字段,默认为 "key"
|
|
||||||
* @param {string} options.sortDir - 排序方向,"asc" 或 "desc",默认为 "asc"
|
|
||||||
* @param {number} options.limit - 每页返回的记录数,默认为 100
|
|
||||||
* @param {number} options.skip - 跳过的记录数,默认为 0
|
|
||||||
* @returns {Promise<Object>} 包含键名列表和分页信息的响应对象
|
|
||||||
*
|
|
||||||
* 使用示例:
|
|
||||||
* ```javascript
|
|
||||||
* // 获取前10个键名
|
|
||||||
* const result = await dataProvider.loadKeys({ limit: 10 });
|
|
||||||
* if (result.success !== false) {
|
|
||||||
* console.log('键名列表:', result.keys);
|
|
||||||
* console.log('总数:', result.total_rows);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 获取第二页数据(跳过前10个)
|
|
||||||
* const page2 = await dataProvider.loadKeys({ limit: 10, skip: 10 });
|
|
||||||
*
|
|
||||||
* // 按键名降序排列
|
|
||||||
* const sorted = await dataProvider.loadKeys({ sortDir: 'desc' });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* 返回值格式:
|
|
||||||
* ```javascript
|
|
||||||
* {
|
|
||||||
* keys: ["key1", "key2", "key3"],
|
|
||||||
* total_rows: 150,
|
|
||||||
* current_page: {
|
|
||||||
* limit: 10,
|
|
||||||
* skip: 0,
|
|
||||||
* count: 10
|
|
||||||
* },
|
|
||||||
* load_more: "/api/kv/namespace/_keys?..." // 仅服务器模式
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
loadKeys: async (options = {}) => {
|
loadKeys: async (options = {}) => {
|
||||||
const provider = getSetting("server.provider");
|
if (!useServerProvider()) {
|
||||||
const useServer =
|
|
||||||
provider === "kv-server" || provider === "classworkscloud";
|
|
||||||
|
|
||||||
if (useServer) {
|
|
||||||
return kvServerProvider.loadKeys(options);
|
|
||||||
} else {
|
|
||||||
return kvLocalProvider.loadKeys(options);
|
return kvLocalProvider.loadKeys(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = await kvServerProvider.loadKeys(options);
|
||||||
|
|
||||||
|
if (!isNetworkError(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Network error — fall back to local cache keys
|
||||||
|
return kvLocalProvider.loadKeys(options);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取键的云端访问地址,并处理本地到云端的数据迁移
|
|
||||||
*
|
|
||||||
* 功能说明:
|
|
||||||
* 1. 如果用户选择本地存储,则将本地键数据读取并存储到云端
|
|
||||||
* 2. 如果云端配置为空或错误则自动改成classworksCloudDefaults的配置
|
|
||||||
* 3. 根据网站验证情况(私有则添加token,公开或受保护则不需要)拼接键的get路径并返回
|
|
||||||
*
|
|
||||||
* @param {string} key - 要获取地址的键名
|
|
||||||
* @param {Object} options - 选项配置
|
|
||||||
* @param {boolean} options.migrateFromLocal - 是否从本地迁移数据到云端,默认为true
|
|
||||||
* @param {boolean} options.autoConfigureCloud - 是否自动配置云端默认设置,默认为true
|
|
||||||
* @returns {Promise<Object>} 包含键访问地址和操作结果的响应对象
|
|
||||||
*
|
|
||||||
* 使用示例:
|
|
||||||
* ```javascript
|
|
||||||
* import dataProvider from '@/utils/dataProvider';
|
|
||||||
*
|
|
||||||
* // 基本用法:获取键的云端地址并自动迁移本地数据
|
|
||||||
* const result = await dataProvider.getKeyCloudUrl('exam_configs');
|
|
||||||
* if (result.success) {
|
|
||||||
* console.log('云端访问地址:', result.url);
|
|
||||||
* console.log('是否已迁移数据:', result.migrated);
|
|
||||||
* console.log('是否自动配置:', result.configured);
|
|
||||||
* } else {
|
|
||||||
* console.error('获取失败:', result.error.message);
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 仅获取地址,不迁移数据
|
|
||||||
* const urlOnly = await dataProvider.getKeyCloudUrl('my_data', {
|
|
||||||
* migrateFromLocal: false
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // 不自动配置云端设置
|
|
||||||
* const noAutoConfig = await dataProvider.getKeyCloudUrl('my_data', {
|
|
||||||
* autoConfigureCloud: false
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* 传入参数示例:
|
|
||||||
* ```javascript
|
|
||||||
* // 参数1: key (必需)
|
|
||||||
* 'exam_configs' // 字符串类型的键名
|
|
||||||
*
|
|
||||||
* // 参数2: options (可选)
|
|
||||||
* {
|
|
||||||
* migrateFromLocal: true, // 是否迁移本地数据
|
|
||||||
* autoConfigureCloud: true // 是否自动配置云端
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* 返回值格式:
|
|
||||||
* ```javascript
|
|
||||||
* // 成功时返回:
|
|
||||||
* {
|
|
||||||
* success: true,
|
|
||||||
* url: "https://kv-service.houlang.cloud/device-uuid-123/exam_configs?token=abc123", // 私有访问时包含token
|
|
||||||
* migrated: true, // 是否成功迁移了本地数据
|
|
||||||
* configured: false // 是否自动配置了云端设置
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 公开访问时返回:
|
|
||||||
* {
|
|
||||||
* success: true,
|
|
||||||
* url: "https://kv-service.houlang.cloud/device-uuid-123/exam_configs", // 公开访问不包含token
|
|
||||||
* migrated: false,
|
|
||||||
* configured: true
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* // 失败时返回:
|
|
||||||
* {
|
|
||||||
* success: false,
|
|
||||||
* error: {
|
|
||||||
* code: "CLOUD_URL_ERROR",
|
|
||||||
* message: "获取键云端地址失败"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
async getKeyCloudUrl(key, options = {}) {
|
async getKeyCloudUrl(key, options = {}) {
|
||||||
const {
|
const {
|
||||||
migrateFromLocal = true,
|
migrateFromLocal = true,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import {formatResponse, formatError} from "../dataProvider";
|
|||||||
|
|
||||||
// Database initialization for local storage
|
// Database initialization for local storage
|
||||||
const DB_NAME = "ClassworksDB";
|
const DB_NAME = "ClassworksDB";
|
||||||
const DB_VERSION = 2;
|
const DB_VERSION = 3;
|
||||||
|
|
||||||
const initDB = async () => {
|
const initDB = async () => {
|
||||||
return openDB(DB_NAME, DB_VERSION, {
|
return openDB(DB_NAME, DB_VERSION, {
|
||||||
@ -17,6 +17,11 @@ const initDB = async () => {
|
|||||||
if (!db.objectStoreNames.contains("system")) {
|
if (!db.objectStoreNames.contains("system")) {
|
||||||
db.createObjectStore("system");
|
db.createObjectStore("system");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a sync queue store for offline write buffering
|
||||||
|
if (!db.objectStoreNames.contains("syncQueue")) {
|
||||||
|
db.createObjectStore("syncQueue");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -112,4 +117,42 @@ export const kvLocalProvider = {
|
|||||||
return formatError("获取本地键名列表失败:" + error.message);
|
return formatError("获取本地键名列表失败:" + error.message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- Sync queue operations for offline write buffering ---
|
||||||
|
|
||||||
|
async addToSyncQueue(entry) {
|
||||||
|
try {
|
||||||
|
const db = await initDB();
|
||||||
|
await db.put("syncQueue", JSON.stringify(entry), entry.key);
|
||||||
|
return formatResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return formatError("添加同步队列失败:" + error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async getSyncQueue() {
|
||||||
|
try {
|
||||||
|
const db = await initDB();
|
||||||
|
const keys = await db.getAllKeys("syncQueue");
|
||||||
|
const entries = [];
|
||||||
|
for (const key of keys) {
|
||||||
|
const raw = await db.get("syncQueue", key);
|
||||||
|
if (raw) entries.push(JSON.parse(raw));
|
||||||
|
}
|
||||||
|
entries.sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
return formatResponse(entries);
|
||||||
|
} catch (error) {
|
||||||
|
return formatError("获取同步队列失败:" + error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeFromSyncQueue(key) {
|
||||||
|
try {
|
||||||
|
const db = await initDB();
|
||||||
|
await db.delete("syncQueue", key);
|
||||||
|
return formatResponse(true);
|
||||||
|
} catch (error) {
|
||||||
|
return formatError("删除同步队列项失败:" + error);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -209,7 +209,7 @@ export default defineConfig({
|
|||||||
// UI 框架
|
// UI 框架
|
||||||
'vendor-vuetify': ['vuetify'],
|
'vendor-vuetify': ['vuetify'],
|
||||||
// 监控(异步加载,独立 chunk)
|
// 监控(异步加载,独立 chunk)
|
||||||
'vendor-sentry': ['@sentry/vue'],
|
// 'vendor-sentry': ['@sentry/vue'],
|
||||||
// 实时通信
|
// 实时通信
|
||||||
'vendor-socket': ['socket.io-client'],
|
'vendor-socket': ['socket.io-client'],
|
||||||
// 通用工具库
|
// 通用工具库
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user