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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user