Add settings page with synced appearance and password change
- Store app settings as one JSON document on the server, merged per key and pushed to other devices; cache locally to avoid a flash on load - Settings page groups appearance, account, shortcuts, shares, API tokens and trash; full screen with a section list on phones - Font size and content width settings - Change the login password, signing out other sessions - Settings writes and password changes only accept a login session
This commit is contained in:
1 parent
5b326a70bf
commit
eb63b14c8e
25 files changed
+915
-290
No files matched your search
@@ -0,0 +1,96 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { copyText } from '../editor/widgets'
|
||||
import { api, ApiError, type ApiToken } from './api'
|
||||
import Icon from './Icon.vue'
|
||||
import { toast } from './workspace'
|
||||
|
||||
const tokens = ref<ApiToken[] | null>(null)
|
||||
const failed = ref(false)
|
||||
const name = ref('')
|
||||
const scope = ref<ApiToken['scope']>('read')
|
||||
const busy = ref(false)
|
||||
// 刚创建的令牌明文,只在本次对话框中显示
|
||||
const created = ref<string | null>(null)
|
||||
const fmt = new Intl.DateTimeFormat('zh-CN', { dateStyle: 'short', timeStyle: 'short' })
|
||||
|
||||
function errorText(err: unknown) {
|
||||
return err instanceof ApiError ? err.message : '网络错误,请检查连接'
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
tokens.value = await api.tokens()
|
||||
} catch {
|
||||
failed.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function create() {
|
||||
busy.value = true
|
||||
try {
|
||||
const res = await api.createToken(name.value.trim(), scope.value)
|
||||
tokens.value = [res.info, ...(tokens.value ?? [])]
|
||||
created.value = res.token
|
||||
name.value = ''
|
||||
} catch (err) {
|
||||
toast('创建失败:' + errorText(err))
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function copy() {
|
||||
toast((await copyText(created.value!)) ? '令牌已复制' : '复制失败,请手动选中复制')
|
||||
}
|
||||
|
||||
async function revoke(t: ApiToken) {
|
||||
if (!confirm(`吊销“${t.name}”?使用它的脚本将立即无法访问。`)) return
|
||||
try {
|
||||
await api.deleteToken(t.id)
|
||||
tokens.value = tokens.value!.filter((x) => x.id !== t.id)
|
||||
} catch (err) {
|
||||
toast('吊销失败:' + errorText(err))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<form class="rm-share-form" @submit.prevent="create">
|
||||
<input v-model="name" type="text" autocomplete="off" placeholder="名称,如:备份脚本" aria-label="名称" required maxlength="100" />
|
||||
<select v-model="scope" aria-label="权限">
|
||||
<option value="read">只读</option>
|
||||
<option value="write">读写</option>
|
||||
</select>
|
||||
<button class="rm-btn rm-btn-primary" type="submit" :disabled="busy">创建</button>
|
||||
</form>
|
||||
<div v-if="created" class="rm-token-created">
|
||||
<p>令牌只显示这一次,请立即保存:</p>
|
||||
<div class="rm-token-value">
|
||||
<code>{{ created }}</code>
|
||||
<button class="rm-btn" @click="copy">复制</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="rm-share-note">
|
||||
调用时带上请求头 <code>Authorization: Bearer 令牌</code>;只读令牌只能读取。接口说明见
|
||||
<a href="/api/v1/openapi.json" target="_blank" rel="noopener">openapi.json</a>。
|
||||
</p>
|
||||
<p v-if="failed" class="rm-tree-empty">读取 API 令牌失败</p>
|
||||
<p v-else-if="tokens && !tokens.length" class="rm-tree-empty">还没有 API 令牌</p>
|
||||
<ul v-else class="rm-trash-list">
|
||||
<li v-for="t in tokens ?? []" :key="t.id">
|
||||
<Icon name="key" />
|
||||
<div class="rm-trash-info">
|
||||
<span class="rm-trash-path">{{ t.name }} · {{ t.scope === 'write' ? '读写' : '只读' }}</span>
|
||||
<span class="rm-trash-time">
|
||||
{{ fmt.format(t.createdAt) }} 创建 · {{ t.lastUsedAt ? fmt.format(t.lastUsedAt) + ' 最近使用' : '从未使用' }}
|
||||
</span>
|
||||
</div>
|
||||
<button class="rm-btn rm-btn-danger" @click="revoke(t)">吊销</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user