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 全绿;二进制端到端冒烟通过
123 lines
4.6 KiB
Vue
123 lines
4.6 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* 有效期选择器(需求⑧:按 config 的 expireStyle / max_save_seconds / max_save_count 动态渲染):
|
||
* - 选项来自 config.expireStyle 白名单
|
||
* - style=count 时数值上限 = max_save_count(0=不限),并展示提示
|
||
* - 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_count(0=不限) */
|
||
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>
|