1. 后台图表多样化 API 的 /admin/overview 新增了按 AI 提供商聚合的用量(providerSeries)和云端数据构成(dataDist)。Admin.vue 概览页改用 ECharts:日活折线图、Token 堆叠柱状图(叠加调用次数折线)、AI 提供商用量饼图、云端数据构成饼图。

2. 端口监控与刷新间隔 「存为应用」不再跳转启动台,而是在本页弹出复用的 LaunchAppFormModal 表单,保存成功后弹确认框询问「留在本页 / 前往启动台」。启动台和端口监控页头都加了 RefreshIntervalPicker(仅手动 / 5 / 10 / 30 / 60 秒,默认 30 秒),选择记忆在 localStorage。

3. 团队所有者与用户详情 团队列表的所有者、用户列表的用户名都改为「头像 + 昵称 + ID」的可点击单元格,点击弹出用户详情:头像、昵称、头衔、待办/工单/笔记/团队数量、注册时间、最近活跃、拥有团队数。API 新增 GET /admin/users/:id,团队列表关联查询出 ownerNickname/ownerAvatar。

4. 项目专属排除规则 SQLite exclusion_rules 加了 project_id 列(唯一约束改为 (project_id, pattern),旧库自动重建迁移)。项目详情页「代码」标签下新增专属规则面板,可增删;扫描时全局规则 + 项目规则叠加生效。规则随项目身份上云,其它机器认领项目后自动带回。

5. 一句话 AI 生成待办/工单 待办页和工单页工具栏各嵌入一个 AI 生成输入条:一句话回车后由后端 AIGenerateTasks 调用 AI 拆解成多条草稿,弹预览框逐条勾选、可改标题/类型/优先级/日期,选归属项目后批量保存。

6. 项目云同步与认领

全局挂载了 CloudClaimModal:登录同步后发现云端有本机未落地的项目时自动弹出(同一批只打扰一次),每个项目可「选目录认领」或「忽略」;工作台横幅和个人主页可随时再打开。
统计数据按机器码推送到 stats:<机器码> 文档,只推不拉,不同电脑的扫描历史互不覆盖。
个人主页 · 云同步新增「项目云同步」区块:自动/手动模式切换(手动模式下常规同步跳过项目文档,仅点「立即同步项目」时推拉)、待认领入口、已忽略项目的恢复列表。
途中补齐了约 60 个中英双语 i18n 键,并修掉了 PortMonitor 引用但缺失的 pmSavedToast 键。
This commit is contained in:
李琦
2026-08-17 09:04:53 +08:00
parent c65cedf9fa
commit c430a0f6da
25 changed files with 1666 additions and 240 deletions

View File

@@ -153,6 +153,7 @@ function setTab(v) {
if (v === 'teams') loadTeams()
// 素材库可用性跟随全局配置,进入时先刷新配置再拉列表(管理员可能刚在设置页改过)
if (v === 'assets') { loadTeams(); loadFileStorage().then(() => reloadAssets()) }
if (v === 'sync') loadProjSync()
}
// 按时段问候,让页面更像“我的空间”而不是后台表单
const greeting = computed(() => {
@@ -333,6 +334,44 @@ async function syncNow() {
} catch (e) { msg.value = errText(e); store.showToast({ type: 'error', key: 'syncFailToast' }) }
finally { busy.value = '' }
}
// ---- 项目云同步:自动/手动模式、手动拉取、认领入口与忽略恢复 ----
const projSyncMode = ref('auto')
const ignoredProjects = ref([])
const pendingCount = ref(0)
async function loadProjSync() {
projSyncMode.value = store.settings.projectSyncMode || 'auto'
if (!sync.value.loggedIn) { ignoredProjects.value = []; pendingCount.value = 0; return }
try { ignoredProjects.value = (await call('ListCloudIgnoredProjects')) || [] } catch { ignoredProjects.value = [] }
try { pendingCount.value = ((await call('ListCloudPendingProjects')) || []).length } catch { pendingCount.value = 0 }
}
async function changeProjSyncMode() {
await store.saveSettings({ projectSyncMode: projSyncMode.value })
store.showToast({ type: 'success', key: projSyncMode.value === 'auto' ? 'projSyncAutoToast' : 'projSyncManualToast' })
}
async function syncProjectsNow() {
if (busy.value) return
busy.value = 'projsync'
try {
const st = await call('SyncProjectsNow')
store.syncStatus = st
store.showToast({ type: 'success', key: 'syncDoneToast', params: { pushed: st.pushed, pulled: st.pulled } })
await loadProjSync()
} catch (e) { store.showToast({ type: 'error', text: errText(e) }) }
finally { busy.value = '' }
}
async function unignoreProject(name) {
try {
await call('UnignoreCloudProject', name)
store.showToast({ type: 'success', key: 'projUnignoredToast', params: { name } })
await loadProjSync()
} catch (e) { store.showToast({ type: 'error', text: errText(e) }) }
}
function openClaim() {
store.cloudClaimOpen = true
}
// 认领弹窗关闭后刷新待认领计数
watch(() => store.cloudClaimOpen, v => { if (!v && tab.value === 'sync') loadProjSync() })
async function syncLogout() {
await call('SyncLogout')
store.showToast({ type: 'success', key: 'logoutToast' })
@@ -359,6 +398,7 @@ onMounted(async () => {
await loadFileStorage()
if (tab.value === 'teams') loadTeams()
if (tab.value === 'assets') { loadTeams(); reloadAssets() }
if (tab.value === 'sync') loadProjSync()
addEventListener('keydown', onKey)
})
onUnmounted(() => removeEventListener('keydown', onKey))
@@ -591,6 +631,37 @@ onUnmounted(() => removeEventListener('keydown', onKey))
<span v-for="s in scopeTags" :key="s.key" class="pc-tag"><component :is="s.icon" />{{ t(s.key) }}</span>
</div>
</div>
<template v-if="sync.loggedIn">
<div class="proj-sync">
<header class="proj-sync-head">
<b><Folder />{{ t('projSyncTitle') }}</b>
<small>{{ t('projSyncHint') }}</small>
</header>
<div class="proj-sync-row">
<label class="proj-sync-mode">
<span>{{ t('projSyncModeLabel') }}</span>
<select v-model="projSyncMode" @change="changeProjSyncMode">
<option value="auto">{{ t('projSyncModeAuto') }}</option>
<option value="manual">{{ t('projSyncModeManual') }}</option>
</select>
</label>
<button class="btn secondary" :disabled="!!busy" @click="syncProjectsNow">
<RefreshCw :class="{ spin: busy === 'projsync' }" />{{ busy === 'projsync' ? t('syncingBtn') : t('projSyncNowBtn') }}
</button>
<button v-if="pendingCount" class="btn primary" @click="openClaim">
<CloudUpload />{{ t('projClaimBtn', { n: pendingCount }) }}
</button>
</div>
<div v-if="ignoredProjects.length" class="proj-ignored">
<small>{{ t('projIgnoredTitle') }}</small>
<div class="proj-ignored-list">
<span v-for="n in ignoredProjects" :key="n" class="proj-ignored-item">
{{ n }}<button type="button" :title="t('projUnignoreBtn')" @click="unignoreProject(n)"><X /></button>
</span>
</div>
</div>
</div>
</template>
</section>
</div>