Files
RawMark/web/src/app/TokensPanel.vue
T
eezd eb63b14c8e 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
2026-10-17 16:23:39 +08:00

97 lines
3.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>