配置页:订阅的增删改和立刻更新
写配置的纯逻辑在 shared/providers.ts。删除前 subscriptionReferences 同时查 直接 use 和经订阅副本(共用缓存文件 path)的间接引用,有被引用的就拦下并列出 策略组。订阅不允许改名:名字被 use: 和副本引用,同步所有引用不如删了重建。 页面还没挂进 App.vue 的导航,由集成者统一加。
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 新增或编辑一个订阅。
|
||||
*
|
||||
* 原生 <dialog>,同 InboundForm。缓存路径不写进表单:它由名字自动生成,
|
||||
* 让用户填只会填出和链式代理对不上的路径。订阅不能改名——名字被策略组的 `use:`
|
||||
* 和生成的副本引用,改名要同步所有引用,所以编辑时名字只是静态展示,想换名就删了重建。
|
||||
*/
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { DEFAULT_INTERVAL } from '@shared/providers.ts'
|
||||
import type { Subscription } from '@shared/providers.ts'
|
||||
import { GENERATED_PREFIX } from '@shared/inbound-groups.ts'
|
||||
import IconClose from '~icons/ri/close-line'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
/** 传了就是编辑,没传就是新增 */
|
||||
editing: Subscription | null
|
||||
/** proxy-providers 里的所有名字(含生成的副本),用来挡重名 */
|
||||
takenNames: string[]
|
||||
saving: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
/** 编辑时 name 是被编辑那个订阅的原名 */
|
||||
submit: [draft: { name: string; url: string; interval: number }]
|
||||
}>()
|
||||
|
||||
const dialog = ref<HTMLDialogElement | null>(null)
|
||||
const name = ref('')
|
||||
const url = ref('')
|
||||
const interval = ref(String(DEFAULT_INTERVAL))
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(open) => {
|
||||
if (open) {
|
||||
name.value = props.editing?.name ?? ''
|
||||
url.value = props.editing?.url ?? ''
|
||||
interval.value = String(props.editing?.interval ?? DEFAULT_INTERVAL)
|
||||
dialog.value?.showModal()
|
||||
} else {
|
||||
dialog.value?.close()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const nameError = computed(() => {
|
||||
const n = name.value.trim()
|
||||
if (n === '') return ''
|
||||
// 带这个前缀的会被当成 Patchbay 生成物:列表里不显示,pruneGenerated 还会把它清掉
|
||||
if (n.startsWith(GENERATED_PREFIX)) return `名称不能以 ${GENERATED_PREFIX} 开头`
|
||||
if (props.takenNames.includes(n)) return '名称已存在'
|
||||
return ''
|
||||
})
|
||||
|
||||
const intervalInvalid = computed(() => {
|
||||
const raw = interval.value.trim()
|
||||
return raw === '' || !/^\d+$/.test(raw) || Number(raw) < 1
|
||||
})
|
||||
|
||||
const canSubmit = computed(
|
||||
() =>
|
||||
url.value.trim() !== '' &&
|
||||
!intervalInvalid.value &&
|
||||
(props.editing !== null || (name.value.trim() !== '' && nameError.value === '')) &&
|
||||
!props.saving,
|
||||
)
|
||||
|
||||
function submit() {
|
||||
if (!canSubmit.value) return
|
||||
emit('submit', {
|
||||
name: props.editing?.name ?? name.value.trim(),
|
||||
url: url.value.trim(),
|
||||
interval: Number(interval.value.trim()),
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog ref="dialog" class="dlg" @close="emit('close')" @cancel.prevent="emit('close')">
|
||||
<form method="dialog" @submit.prevent="submit">
|
||||
<header class="dlg-head">
|
||||
<h2>{{ editing ? `编辑订阅 ${editing.name}` : '新增订阅' }}</h2>
|
||||
<button type="button" class="x" @click="emit('close')"><IconClose /></button>
|
||||
</header>
|
||||
|
||||
<div class="body">
|
||||
<label v-if="!editing" class="row">
|
||||
<span class="k">名称</span>
|
||||
<span class="v">
|
||||
<input v-model="name" type="text" placeholder="subs" autocomplete="off" />
|
||||
<span v-if="nameError" class="bad">{{ nameError }}</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span class="k">地址</span>
|
||||
<span class="v">
|
||||
<input v-model="url" class="mono" type="text" placeholder="https://…" autocomplete="off" />
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label class="row">
|
||||
<span class="k">更新间隔</span>
|
||||
<span class="v">
|
||||
<input v-model="interval" class="mono" type="text" inputmode="numeric" autocomplete="off" />
|
||||
<span v-if="intervalInvalid" class="bad">正整数,单位秒</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<footer class="dlg-foot">
|
||||
<button type="button" class="btn" @click="emit('close')">取消</button>
|
||||
<button type="submit" class="btn btn-go" :disabled="!canSubmit">
|
||||
{{ saving ? '保存中' : '保存' }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
</dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dlg {
|
||||
width: min(520px, calc(100vw - 2 * var(--s-5)));
|
||||
padding: 0;
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--r-lg);
|
||||
background: var(--c-surface);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.dlg::backdrop {
|
||||
background: rgb(0 0 0 / 0.45);
|
||||
}
|
||||
|
||||
.dlg-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--s-4) var(--s-5);
|
||||
border-bottom: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.dlg-head h2 {
|
||||
font-size: var(--t-lg);
|
||||
font-weight: 600;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.x {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--r-sm);
|
||||
background: none;
|
||||
color: var(--c-text-faint);
|
||||
cursor: pointer;
|
||||
}
|
||||
.x:hover {
|
||||
background: var(--c-surface-sunken);
|
||||
color: var(--c-text);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-4);
|
||||
padding: var(--s-5);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 5rem 1fr;
|
||||
gap: var(--s-4);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.k {
|
||||
color: var(--c-text-soft);
|
||||
font-size: var(--t-sm);
|
||||
}
|
||||
|
||||
.v {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--s-1);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: var(--s-2) var(--s-3);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--c-bg);
|
||||
}
|
||||
|
||||
.bad {
|
||||
font-size: var(--t-xs);
|
||||
color: var(--c-danger);
|
||||
}
|
||||
|
||||
.dlg-foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--s-2);
|
||||
padding: var(--s-4) var(--s-5);
|
||||
border-top: 1px solid var(--c-border);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: var(--s-2) var(--s-4);
|
||||
border: 1px solid var(--c-border-strong);
|
||||
border-radius: var(--r-sm);
|
||||
background: var(--c-surface);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-go {
|
||||
background: var(--c-accent);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-go:disabled {
|
||||
background: var(--c-surface-sunken);
|
||||
border-color: var(--c-border);
|
||||
color: var(--c-text-faint);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user