更新若干功能
This commit is contained in:
@@ -6,6 +6,8 @@ import { UserRound, ImageUp, KeyRound, CloudUpload, LogIn, LogOut, RefreshCw, Wi
|
||||
import { call, isNative } from '../api'
|
||||
import { useAppStore } from '../store'
|
||||
import { teams, teamsErr, teamsLoading, loadTeams as loadTeamsShared, switchTeam as switchTeamShared } from '../team'
|
||||
import RemoteImg from '../components/RemoteImg.vue'
|
||||
import ImagePreview from '../components/ImagePreview.vue'
|
||||
|
||||
const store = useAppStore(), { t } = useI18n(), native = isNative()
|
||||
const router = useRouter()
|
||||
@@ -14,19 +16,24 @@ const pwdForm = reactive({ old: '', next: '', confirm: '' })
|
||||
const busy = ref(''), msg = ref('')
|
||||
const loaded = ref(false)
|
||||
const avatarOpen = ref(false)
|
||||
const AVATAR_HIST_KEY = 'cc-avatar-history'
|
||||
const avatarHistory = ref([]) // [{mode,value,preview?}]
|
||||
const serverAvatars = ref([])
|
||||
const previewOpen = ref(false)
|
||||
const previewSrc = ref('')
|
||||
const TABS = ['info', 'teams', 'assets', 'security', 'sync']
|
||||
const tab = ref(TABS.includes(localStorage.getItem('cc-profile-tab')) ? localStorage.getItem('cc-profile-tab') : 'info')
|
||||
const doneTodos = ref(0)
|
||||
const resolvedTickets = ref(0)
|
||||
const sync = computed(() => store.syncStatus)
|
||||
// ---- 全局文件存储(管理员在设置页配置;此处只读,决定素材库可用性与提示文案) ----
|
||||
const fsCfg = reactive({ mode: 'local', baseUrl: '', apiKey: '' })
|
||||
const fsCfg = reactive({ mode: 'local', baseUrl: '' })
|
||||
const serverStorage = computed(() => fsCfg.mode === 'server')
|
||||
|
||||
async function loadFileStorage() {
|
||||
try {
|
||||
const c = await call('GetFileStorageConfig')
|
||||
Object.assign(fsCfg, { mode: c.mode || 'local', baseUrl: c.baseUrl || '', apiKey: c.apiKey || '' })
|
||||
Object.assign(fsCfg, { mode: c.mode || 'local', baseUrl: c.baseUrl || '' })
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -76,6 +83,10 @@ async function copyAsset(f) {
|
||||
store.showToast({ type: 'success', key: 'assetsCopiedToast' })
|
||||
} catch { store.showToast({ type: 'error', key: 'assetsCopyFailed' }) }
|
||||
}
|
||||
function openAssetPreview(f) {
|
||||
previewSrc.value = f.url
|
||||
previewOpen.value = true
|
||||
}
|
||||
function fmtSize(n) {
|
||||
if (n >= 1 << 20) return (n / (1 << 20)).toFixed(1) + ' MB'
|
||||
if (n >= 1024) return Math.round(n / 1024) + ' KB'
|
||||
@@ -203,19 +214,115 @@ async function persist() {
|
||||
await store.saveSettings({ avatarMode: form.avatarMode, avatarValue: form.avatarValue, imageMode: form.imageMode })
|
||||
}
|
||||
watch(() => [form.avatarMode, form.avatarValue, form.imageMode], persist)
|
||||
|
||||
function loadLocalAvatarHistory() {
|
||||
try {
|
||||
const raw = JSON.parse(localStorage.getItem(AVATAR_HIST_KEY) || '[]')
|
||||
return Array.isArray(raw) ? raw.filter(x => x && x.value).slice(0, 12) : []
|
||||
} catch { return [] }
|
||||
}
|
||||
function saveLocalAvatarHistory(list) {
|
||||
avatarHistory.value = list.slice(0, 12)
|
||||
localStorage.setItem(AVATAR_HIST_KEY, JSON.stringify(avatarHistory.value.map(({ mode, value }) => ({ mode, value }))))
|
||||
}
|
||||
async function loadAvatarHistory() {
|
||||
if (sync.value.loggedIn) {
|
||||
try {
|
||||
const items = await call('ListAvatarHistory')
|
||||
avatarHistory.value = (items || []).map(x => ({ id: x.id, mode: x.mode, value: x.value, createdAt: x.createdAt }))
|
||||
// 首次:把本地残留历史迁移到线上
|
||||
const local = loadLocalAvatarHistory()
|
||||
if (local.length && !avatarHistory.value.length) {
|
||||
for (const it of [...local].reverse()) {
|
||||
try { await call('PushAvatarHistory', it.mode || 'base64', it.value) } catch { /* ignore */ }
|
||||
}
|
||||
localStorage.removeItem(AVATAR_HIST_KEY)
|
||||
const again = await call('ListAvatarHistory')
|
||||
avatarHistory.value = (again || []).map(x => ({ id: x.id, mode: x.mode, value: x.value, createdAt: x.createdAt }))
|
||||
}
|
||||
return
|
||||
} catch { /* 回退本地 */ }
|
||||
}
|
||||
avatarHistory.value = loadLocalAvatarHistory()
|
||||
}
|
||||
async function pushAvatarHistory(mode, value) {
|
||||
if (!value) return
|
||||
if (sync.value.loggedIn) {
|
||||
try {
|
||||
const items = await call('PushAvatarHistory', mode || 'base64', value)
|
||||
avatarHistory.value = (items || []).map(x => ({ id: x.id, mode: x.mode, value: x.value, createdAt: x.createdAt }))
|
||||
await refreshHistPreviews()
|
||||
return
|
||||
} catch { /* 回退本地 */ }
|
||||
}
|
||||
const next = [{ mode: mode || 'base64', value }, ...avatarHistory.value.filter(x => x.value !== value)]
|
||||
saveLocalAvatarHistory(next)
|
||||
await refreshHistPreviews()
|
||||
}
|
||||
async function resolveHistPreview(item) {
|
||||
if (item._src) return item._src
|
||||
if (item.mode === 'url' || /^https?:\/\//i.test(item.value)) {
|
||||
try {
|
||||
const { resolveImageSrc } = await import('../api')
|
||||
item._src = await resolveImageSrc(item.value)
|
||||
} catch { item._src = item.value }
|
||||
} else if (item.mode === 'path') {
|
||||
try { item._src = await call('ReadImageAsDataURL', item.value) } catch { item._src = '' }
|
||||
} else {
|
||||
item._src = item.value
|
||||
}
|
||||
return item._src
|
||||
}
|
||||
async function loadServerAvatars() {
|
||||
serverAvatars.value = []
|
||||
if (!serverStorage.value || !sync.value.loggedIn) return
|
||||
try {
|
||||
const r = await call('ListServerFiles', 'mine', 0, 1)
|
||||
serverAvatars.value = (r.items || []).filter(f => f.kind === 'avatar')
|
||||
} catch {}
|
||||
}
|
||||
const histPreviews = ref({})
|
||||
async function refreshHistPreviews() {
|
||||
const map = {}
|
||||
for (const it of avatarHistory.value) {
|
||||
map[it.value] = await resolveHistPreview(it)
|
||||
}
|
||||
histPreviews.value = map
|
||||
}
|
||||
// 存储走向由后端按管理员全局配置实时决定(auto):server 时上传返回 url,否则 base64。
|
||||
async function pickAvatar() {
|
||||
try {
|
||||
const r = await call('PickAvatarImage', 'auto')
|
||||
if (r && r.value) {
|
||||
if (form.avatarValue) await pushAvatarHistory(form.avatarMode, form.avatarValue)
|
||||
form.avatarMode = r.mode || 'base64'
|
||||
form.avatarValue = r.value
|
||||
await pushAvatarHistory(form.avatarMode, form.avatarValue)
|
||||
}
|
||||
} catch (e) { store.showToast({ type: 'error', text: errText(e) }) }
|
||||
}
|
||||
function clearAvatar() { form.avatarMode = ''; form.avatarValue = '' }
|
||||
async function clearAvatar() {
|
||||
if (form.avatarValue) await pushAvatarHistory(form.avatarMode, form.avatarValue)
|
||||
form.avatarMode = ''
|
||||
form.avatarValue = ''
|
||||
}
|
||||
function selectHistory(item) {
|
||||
form.avatarMode = item.mode || 'base64'
|
||||
form.avatarValue = item.value
|
||||
}
|
||||
async function selectServerAvatar(f) {
|
||||
form.avatarMode = 'url'
|
||||
form.avatarValue = f.url
|
||||
await pushAvatarHistory('url', f.url)
|
||||
}
|
||||
// 打开弹窗时刷新全局配置,保证提示文案与实际走向一致
|
||||
watch(avatarOpen, v => { if (v) loadFileStorage() })
|
||||
watch(avatarOpen, async v => {
|
||||
if (!v) return
|
||||
await loadFileStorage()
|
||||
await loadAvatarHistory()
|
||||
await refreshHistPreviews()
|
||||
await loadServerAvatars()
|
||||
})
|
||||
async function syncNow() {
|
||||
if (busy.value) return
|
||||
busy.value = 'sync'; msg.value = ''
|
||||
@@ -386,7 +493,7 @@ onUnmounted(() => removeEventListener('keydown', onKey))
|
||||
<section v-else-if="tab === 'assets'" class="panel profile-card">
|
||||
<header class="pc-head">
|
||||
<span class="pc-badge img"><Images /></span>
|
||||
<div><b>{{ t('assetsTab') }}</b><small>{{ t('assetsHint') }}</small></div>
|
||||
<div><b>{{ t('assetsTab') }}</b><small>{{ sync.userId === 1 ? t('assetsHintAdmin') : t('assetsHint') }}</small></div>
|
||||
</header>
|
||||
<div v-if="!sync.loggedIn" class="pc-empty">
|
||||
<span class="pc-empty-ico"><Images /></span>
|
||||
@@ -413,7 +520,7 @@ onUnmounted(() => removeEventListener('keydown', onKey))
|
||||
<p v-if="assetsErr" class="db-message">{{ assetsErr }}</p>
|
||||
<div v-if="assets.length" class="assets-grid">
|
||||
<figure v-for="f in assets" :key="f.id" class="asset-card">
|
||||
<a :href="f.url" target="_blank" rel="noreferrer"><img :src="f.url" loading="lazy" alt="" /></a>
|
||||
<button type="button" class="asset-thumb" @click="openAssetPreview(f)"><RemoteImg :src="f.url" /></button>
|
||||
<figcaption>
|
||||
<b :title="f.original || f.name">{{ f.kind === 'avatar' ? t('assetsKindAvatar') : t('assetsKindContent') }} · {{ fmtSize(f.size) }}</b>
|
||||
<small v-if="assetsScope !== 'mine'">{{ f.username || ('#' + f.userId) }}</small>
|
||||
@@ -506,10 +613,27 @@ onUnmounted(() => removeEventListener('keydown', onKey))
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 存储方式由管理员全局配置强制决定,用户不再自行选择 -->
|
||||
<p class="auto-update-hint">{{ t('storageFollowHint', { mode: serverStorage ? t('fsModeServer') : t('fsModeLocal') }) }}</p>
|
||||
<div v-if="avatarHistory.length" class="avatar-hist">
|
||||
<b>{{ t('avatarHistory') }}</b>
|
||||
<div class="avatar-hist-grid">
|
||||
<button v-for="it in avatarHistory" :key="it.value" type="button" class="avatar-hist-item" :class="{ on: form.avatarValue === it.value }" :title="t('avatarReselect')" @click="selectHistory(it)">
|
||||
<img v-if="histPreviews[it.value]" :src="histPreviews[it.value]" alt="" />
|
||||
<UserRound v-else />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="serverAvatars.length" class="avatar-hist">
|
||||
<b>{{ t('avatarServerHistory') }}</b>
|
||||
<div class="avatar-hist-grid">
|
||||
<button v-for="f in serverAvatars" :key="f.id" type="button" class="avatar-hist-item" :class="{ on: form.avatarValue === f.url }" :title="t('avatarReselect')" @click="selectServerAvatar(f)">
|
||||
<RemoteImg :src="f.url" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Teleport>
|
||||
<ImagePreview :open="previewOpen" :src="previewSrc" @close="previewOpen = false" />
|
||||
</div></template>
|
||||
|
||||
Reference in New Issue
Block a user