Files
FileShare/web/src/components/ExpirePicker.vue
T
SKYMirror 9686fe887a FileCodeBox Go 重写版 v2.5.6(安全审计修复版)
Go 1.27.1 (Gin+GORM) + Vue 3 文件快传服务:

- 安全审计全部修复(docs/security-audit-2026-09-05.md):
  bcrypt 密码哈希与自动升级、presign 直传服务端大小/内容校验、
  全局请求体上限、依赖升级(govulncheck 0 命中)、janitor 后台清理、
  管理端审计动作落库、/admin CORS 收紧、通知内容白名单净化、
  会话默认 7 天、限流缓存故障降级、robots.txt 端点等
- 前端:取件链接复制修复(不再重复拼接提取码)、markdown 净化器加固
- Redis 支持库号(FCB_REDIS_DB / redis://…/db URL)
- 文档:docs/api/* 与 openapi.yaml 同步最新行为(robots.txt、
  提码 5 位起、chunk 32MiB 上限、admin 审计动作等)

验证:gofmt/go vet/go test 全绿;二进制端到端冒烟通过
2026-09-05 04:22:41 +08:00

123 lines
4.6 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">
/**
* 有效期选择器(需求⑧:按 config 的 expireStyle / max_save_seconds / max_save_count 动态渲染):
* - 选项来自 config.expireStyle 白名单
* - style=count 时数值上限 = max_save_count0=不限),并展示提示
* - style=day/hour/minute 时按 max_save_seconds 折算上限并展示提示
* - 策略最终由服务端强制(超限 403/400 中文错误),这里仅体验层提示
*/
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useConfigStore } from '@/stores/config'
import { EXPIRE_STYLES } from '@/utils/format'
const props = defineProps<{
value: number
style: string
compact?: boolean
}>()
const emit = defineEmits<{
'update:value': [v: number]
'update:style': [v: string]
}>()
const { t, te } = useI18n()
const config = useConfigStore()
const styles = computed(() => {
const list = config.expireStyle.length ? config.expireStyle : ['day', 'hour', 'minute', 'forever', 'count']
const known = EXPIRE_STYLES.filter((s) => list.includes(s.value))
const extra = list.filter((s) => !EXPIRE_STYLES.some((k) => k.value === s)).map((s) => ({ value: s, label: s }))
return [...known, ...extra]
})
const isForever = computed(() => props.style === 'forever')
const isCount = computed(() => props.style === 'count')
/** 需求⑧:count 上限 = max_save_count0=不限) */
const countMax = computed(() => (config.maxSaveCount > 0 ? config.maxSaveCount : 9999))
/** 需求⑧:时间类上限按 max_save_seconds 折算(0=仅默认 7 天兜底,不额外提示) */
const secondsMax = computed(() => (config.maxSaveSeconds > 0 ? config.maxSaveSeconds : 0))
const unitSeconds: Record<string, number> = { day: 86400, hour: 3600, minute: 60 }
/** 时间型选项的数值上限(按 max_save_seconds 折算;无限制时 9999 */
function maxFor(style: string): number {
const sec = secondsMax.value
const unit = unitSeconds[style]
if (!unit) return style === 'count' ? countMax.value : 9999
if (sec <= 0) return 9999
return Math.max(1, Math.floor(sec / unit))
}
/** 需求⑧:超出上限时给出体验层提示(不拦截提交,服务端强制) */
const overPolicy = computed(() => {
if (isCount.value && config.maxSaveCount > 0 && props.value > config.maxSaveCount) {
return t('expire.maxCountHint', { n: config.maxSaveCount })
}
if (!isForever.value && !isCount.value && secondsMax.value > 0) {
const max = maxFor(props.style)
if (props.value > max) {
const unitLabel = EXPIRE_STYLES.find((s) => s.value === props.style)?.label ?? props.style
return t('expire.maxSecondsHint', { value: `${max} ${unitLabel}` })
}
}
return ''
})
const unitLabel = computed(() => EXPIRE_STYLES.find((s) => s.value === props.style)?.label ?? props.style)
/** 需求⑩:已知样式走 i18n,未知样式原样展示 */
function styleLabel(value: string, raw: string): string {
const key = `expireStyle.${value}`
return te(key) ? t(key) : raw
}
function onStyleChange(e: Event): void {
const v = (e.target as HTMLSelectElement).value
emit('update:style', v)
if (v === 'count' && props.value > countMax.value) emit('update:value', 1)
if (v === 'minute' && props.value < 1) emit('update:value', 10)
}
</script>
<template>
<div class="field-row">
<label v-if="!isForever" class="expire-value" :class="{ compact }" :style="compact ? 'flex:0 0 110px' : ''">
<span class="field-sub">{{ t('expire.value') }}</span>
<input
class="input"
type="number"
:min="1"
:max="maxFor(style)"
:value="value"
@input="emit('update:value', Math.max(1, Number(($event.target as HTMLInputElement).value) || 1))"
/>
</label>
<label :style="isForever ? 'flex:1' : ''">
<span class="field-sub">{{ t('expire.label') }}</span>
<select class="select" :value="style" @change="onStyleChange">
<option v-for="s in styles" :key="s.value" :value="s.value">
{{ isForever && s.value === 'forever' ? t('expire.foreverOption') : s.value === 'count' ? t('expire.countOption') : styleLabel(s.value, s.label) }}
</option>
</select>
</label>
</div>
<p v-if="overPolicy" class="hint" style="color: var(--c-warn)">{{ overPolicy }}</p>
<p v-else-if="style === 'count'" class="hint">{{ t('expire.countHint') }}</p>
<p v-else-if="!isForever" class="hint">{{ t('expire.timeHint', { value, unit: unitLabel }) }}</p>
<p v-else class="hint">{{ t('expire.foreverHint') }}</p>
</template>
<style scoped>
.field-sub {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--c-text-2);
margin-bottom: 6px;
}
</style>