mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:48:45 +08:00
fix(admin): i18n account-test media uploads and shared file pickers
Replace native file-input labels with translated choose-file buttons, localize upload errors and previews, and fix ImageUpload defaults for zh/en.
This commit is contained in:
@@ -103,16 +103,38 @@
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ imageUploadLabel }}
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
class="block w-full text-sm text-gray-600 file:mr-3 file:rounded-lg file:border-0 file:bg-primary-50 file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-primary-700 hover:file:bg-primary-100 dark:text-gray-300 dark:file:bg-primary-500/20 dark:file:text-primary-300"
|
||||
:disabled="status === 'connecting'"
|
||||
@change="onImageFileChange"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.grok.imageUploadHint') }}</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm shrink-0"
|
||||
:disabled="status === 'connecting'"
|
||||
@click="imageFileInput?.click()"
|
||||
>
|
||||
{{ t('admin.accounts.grok.chooseImageFile') }}
|
||||
</button>
|
||||
<span class="min-w-0 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
uploadImageName
|
||||
? t('common.selectedFile', { name: uploadImageName })
|
||||
: t('common.noFileSelected')
|
||||
}}
|
||||
</span>
|
||||
<input
|
||||
ref="imageFileInput"
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp,image/gif"
|
||||
class="hidden"
|
||||
:disabled="status === 'connecting'"
|
||||
@change="onImageFileChange"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ imageUploadHint }}</p>
|
||||
<div v-if="uploadImagePreview" class="overflow-hidden rounded-lg border border-gray-200 dark:border-dark-500">
|
||||
<img :src="uploadImagePreview" alt="upload preview" class="max-h-40 w-full object-contain bg-gray-50 dark:bg-dark-700" />
|
||||
<img
|
||||
:src="uploadImagePreview"
|
||||
:alt="t('admin.accounts.grok.uploadPreviewAlt')"
|
||||
class="max-h-40 w-full object-contain bg-gray-50 dark:bg-dark-700"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -120,29 +142,32 @@
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.accounts.grok.audioUploadLabel') }}
|
||||
</label>
|
||||
<input
|
||||
type="file"
|
||||
accept="audio/*,.wav,.mp3,.m4a,.ogg,.webm"
|
||||
class="block w-full text-sm text-gray-600 file:mr-3 file:rounded-lg file:border-0 file:bg-primary-50 file:px-3 file:py-1.5 file:text-sm file:font-medium file:text-primary-700 hover:file:bg-primary-100 dark:text-gray-300 dark:file:bg-primary-500/20 dark:file:text-primary-300"
|
||||
:disabled="status === 'connecting'"
|
||||
@change="onAudioFileChange"
|
||||
/>
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary btn-sm shrink-0"
|
||||
:disabled="status === 'connecting'"
|
||||
@click="audioFileInput?.click()"
|
||||
>
|
||||
{{ t('admin.accounts.grok.chooseAudioFile') }}
|
||||
</button>
|
||||
<span class="min-w-0 truncate text-xs text-gray-500 dark:text-gray-400">
|
||||
{{
|
||||
uploadAudioName
|
||||
? t('common.selectedFile', { name: uploadAudioName })
|
||||
: t('common.noFileSelected')
|
||||
}}
|
||||
</span>
|
||||
<input
|
||||
ref="audioFileInput"
|
||||
type="file"
|
||||
accept="audio/*,.wav,.mp3,.m4a,.ogg,.webm"
|
||||
class="hidden"
|
||||
:disabled="status === 'connecting'"
|
||||
@change="onAudioFileChange"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.grok.audioUploadHint') }}</p>
|
||||
<p v-if="uploadAudioName" class="text-xs text-gray-600 dark:text-gray-300">{{ uploadAudioName }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="supportsVideoUploadURL" class="space-y-1.5">
|
||||
<label class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ t('admin.accounts.grok.videoUploadUrlLabel') }}
|
||||
</label>
|
||||
<input
|
||||
v-model="videoUploadURL"
|
||||
type="url"
|
||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:border-primary-500 focus:ring-1 focus:ring-primary-500 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-100"
|
||||
:placeholder="t('admin.accounts.grok.videoUploadUrlPlaceholder')"
|
||||
:disabled="status === 'connecting'"
|
||||
/>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">{{ t('admin.accounts.grok.videoUploadUrlHint') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Terminal Output -->
|
||||
@@ -210,7 +235,11 @@
|
||||
class="group/img relative cursor-pointer overflow-hidden rounded-xl border border-gray-200 bg-white shadow-sm transition hover:border-primary-300 hover:shadow-md dark:border-dark-500 dark:bg-dark-700"
|
||||
@click="previewImageUrl = image.url"
|
||||
>
|
||||
<img :src="image.url" :alt="`test-image-${index + 1}`" class="max-h-[360px] w-full object-contain" />
|
||||
<img
|
||||
:src="image.url"
|
||||
:alt="t('admin.accounts.imagePreviewAlt', { index: index + 1 })"
|
||||
class="max-h-[360px] w-full object-contain"
|
||||
/>
|
||||
<div class="absolute inset-0 flex items-center justify-center bg-black/0 transition-colors group-hover/img:bg-black/20">
|
||||
<Icon name="eye" size="lg" class="text-white opacity-0 drop-shadow-lg transition-opacity group-hover/img:opacity-100" :stroke-width="2" />
|
||||
</div>
|
||||
@@ -267,7 +296,7 @@
|
||||
</button>
|
||||
<img
|
||||
:src="previewImageUrl"
|
||||
alt="preview"
|
||||
:alt="t('admin.accounts.imageLightboxAlt')"
|
||||
class="max-h-[90vh] max-w-[90vw] rounded-lg object-contain shadow-2xl"
|
||||
/>
|
||||
</div>
|
||||
@@ -388,9 +417,11 @@ const testMode = ref<'default' | 'compact'>('default')
|
||||
const grokTestMode = ref<'text' | 'image' | 'video' | 'search' | 'tts' | 'stt' | 'realtime'>('text')
|
||||
const uploadImageDataURL = ref('')
|
||||
const uploadImagePreview = ref('')
|
||||
const uploadImageName = ref('')
|
||||
const uploadAudioDataURL = ref('')
|
||||
const uploadAudioName = ref('')
|
||||
const videoUploadURL = ref('')
|
||||
const imageFileInput = ref<HTMLInputElement | null>(null)
|
||||
const audioFileInput = ref<HTMLInputElement | null>(null)
|
||||
const isOpenAIAccount = computed(() => props.account?.platform === 'openai')
|
||||
const isGrokAccount = computed(() => props.account?.platform === 'grok')
|
||||
const openAITestModeOptions = computed(() => [
|
||||
@@ -481,18 +512,23 @@ const supportsImageUpload = computed(
|
||||
() => isGrokAccount.value && (grokTestMode.value === 'image' || grokTestMode.value === 'video')
|
||||
)
|
||||
const supportsAudioUpload = computed(() => isGrokAccount.value && grokTestMode.value === 'stt')
|
||||
const supportsVideoUploadURL = computed(() => isGrokAccount.value && grokTestMode.value === 'video')
|
||||
const imageUploadLabel = computed(() =>
|
||||
grokTestMode.value === 'video'
|
||||
? t('admin.accounts.grok.videoFirstFrameLabel')
|
||||
: t('admin.accounts.grok.imageUploadLabel')
|
||||
)
|
||||
|
||||
const imageUploadHint = computed(() =>
|
||||
grokTestMode.value === 'video'
|
||||
? t('admin.accounts.grok.videoFirstFrameHint')
|
||||
: t('admin.accounts.grok.imageUploadHint')
|
||||
)
|
||||
|
||||
const readFileAsDataURL = (file: File): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(String(reader.result || ''))
|
||||
reader.onerror = () => reject(new Error('Failed to read file'))
|
||||
reader.onerror = () => reject(new Error(t('admin.accounts.grok.fileReadFailed')))
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
|
||||
@@ -502,6 +538,7 @@ const onImageFileChange = async (event: Event) => {
|
||||
if (!file) {
|
||||
uploadImageDataURL.value = ''
|
||||
uploadImagePreview.value = ''
|
||||
uploadImageName.value = ''
|
||||
return
|
||||
}
|
||||
if (file.size > 6 * 1024 * 1024) {
|
||||
@@ -514,9 +551,13 @@ const onImageFileChange = async (event: Event) => {
|
||||
const dataURL = await readFileAsDataURL(file)
|
||||
uploadImageDataURL.value = dataURL
|
||||
uploadImagePreview.value = dataURL
|
||||
uploadImageName.value = file.name
|
||||
} catch {
|
||||
uploadImageDataURL.value = ''
|
||||
uploadImagePreview.value = ''
|
||||
uploadImageName.value = ''
|
||||
errorMessage.value = t('admin.accounts.grok.fileReadFailed')
|
||||
status.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -540,15 +581,19 @@ const onAudioFileChange = async (event: Event) => {
|
||||
} catch {
|
||||
uploadAudioDataURL.value = ''
|
||||
uploadAudioName.value = ''
|
||||
errorMessage.value = t('admin.accounts.grok.fileReadFailed')
|
||||
status.value = 'error'
|
||||
}
|
||||
}
|
||||
|
||||
const clearMediaUploads = () => {
|
||||
uploadImageDataURL.value = ''
|
||||
uploadImagePreview.value = ''
|
||||
uploadImageName.value = ''
|
||||
uploadAudioDataURL.value = ''
|
||||
uploadAudioName.value = ''
|
||||
videoUploadURL.value = ''
|
||||
if (imageFileInput.value) imageFileInput.value.value = ''
|
||||
if (audioFileInput.value) audioFileInput.value.value = ''
|
||||
}
|
||||
|
||||
const promptInputLabel = computed(() => {
|
||||
@@ -805,7 +850,6 @@ const startTest = async () => {
|
||||
mode?: string
|
||||
image_data_url?: string
|
||||
audio_data_url?: string
|
||||
video_upload_url?: string
|
||||
} = {
|
||||
model_id: showModelSelect.value ? selectedModelId.value : '',
|
||||
prompt: supportsPromptInput.value ? testPrompt.value.trim() : ''
|
||||
@@ -831,9 +875,6 @@ const startTest = async () => {
|
||||
if (uploadAudioDataURL.value && grokTestMode.value === 'stt') {
|
||||
requestBody.audio_data_url = uploadAudioDataURL.value
|
||||
}
|
||||
if (videoUploadURL.value.trim() && grokTestMode.value === 'video') {
|
||||
requestBody.video_upload_url = videoUploadURL.value.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Use the configured API base; EventSource does not support POST.
|
||||
@@ -857,7 +898,7 @@ const startTest = async () => {
|
||||
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
throw new Error('No response body')
|
||||
throw new Error(t('admin.accounts.grok.noResponseBody'))
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder()
|
||||
@@ -891,9 +932,9 @@ const startTest = async () => {
|
||||
return
|
||||
}
|
||||
status.value = 'error'
|
||||
const msg = error instanceof Error ? error.message : 'Unknown error'
|
||||
const msg = error instanceof Error ? error.message : t('common.unknownError')
|
||||
errorMessage.value = msg
|
||||
addLine(`Error: ${msg}`, 'text-red-400')
|
||||
addLine(t('admin.accounts.errorPrefix', { message: msg }), 'text-red-400')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -991,13 +1032,13 @@ const handleEvent = (event: {
|
||||
status.value = 'success'
|
||||
} else {
|
||||
status.value = 'error'
|
||||
errorMessage.value = event.error || 'Test failed'
|
||||
errorMessage.value = event.error || t('admin.accounts.testFailed')
|
||||
}
|
||||
break
|
||||
|
||||
case 'error':
|
||||
status.value = 'error'
|
||||
errorMessage.value = event.error || 'Unknown error'
|
||||
errorMessage.value = event.error || t('common.unknownError')
|
||||
if (streamingContent.value) {
|
||||
addLine(streamingContent.value, 'text-green-300')
|
||||
streamingContent.value = ''
|
||||
|
||||
@@ -156,7 +156,7 @@ const readFileAsText = async (sourceFile: File): Promise<string> => {
|
||||
return await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => resolve(String(reader.result ?? ''))
|
||||
reader.onerror = () => reject(reader.error || new Error('Failed to read file'))
|
||||
reader.onerror = () => reject(reader.error || new Error(t('common.fileReadFailed')))
|
||||
reader.readAsText(sourceFile)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
@change="handleUpload"
|
||||
/>
|
||||
<Icon name="upload" size="sm" class="mr-1.5" :stroke-width="2" />
|
||||
{{ uploadLabel }}
|
||||
{{ resolvedUploadLabel }}
|
||||
</label>
|
||||
<button
|
||||
v-if="modelValue"
|
||||
@@ -59,7 +59,7 @@
|
||||
@click="$emit('update:modelValue', '')"
|
||||
>
|
||||
<Icon name="trash" size="sm" class="mr-1.5" :stroke-width="2" />
|
||||
{{ removeLabel }}
|
||||
{{ resolvedRemoveLabel }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="hint" class="text-xs text-gray-500 dark:text-gray-400">{{ hint }}</p>
|
||||
@@ -70,9 +70,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { sanitizeSvg } from '@/utils/sanitize'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string
|
||||
mode?: 'image' | 'svg'
|
||||
@@ -84,8 +87,8 @@ const props = withDefaults(defineProps<{
|
||||
}>(), {
|
||||
mode: 'image',
|
||||
size: 'md',
|
||||
uploadLabel: 'Upload',
|
||||
removeLabel: 'Remove',
|
||||
uploadLabel: '',
|
||||
removeLabel: '',
|
||||
hint: '',
|
||||
maxSize: 300 * 1024,
|
||||
})
|
||||
@@ -96,6 +99,9 @@ const emit = defineEmits<{
|
||||
|
||||
const error = ref('')
|
||||
|
||||
const resolvedUploadLabel = computed(() => props.uploadLabel || t('common.upload'))
|
||||
const resolvedRemoveLabel = computed(() => props.removeLabel || t('common.remove'))
|
||||
|
||||
const acceptTypes = computed(() => props.mode === 'svg' ? '.svg' : 'image/*')
|
||||
|
||||
const sanitizedValue = computed(() =>
|
||||
@@ -114,7 +120,10 @@ function handleUpload(event: Event) {
|
||||
if (!file) return
|
||||
|
||||
if (props.maxSize && file.size > props.maxSize) {
|
||||
error.value = `File too large (${(file.size / 1024).toFixed(1)} KB), max ${(props.maxSize / 1024).toFixed(0)} KB`
|
||||
error.value = t('common.fileTooLargeKb', {
|
||||
size: (file.size / 1024).toFixed(1),
|
||||
max: (props.maxSize / 1024).toFixed(0)
|
||||
})
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
@@ -128,7 +137,7 @@ function handleUpload(event: Event) {
|
||||
reader.readAsText(file)
|
||||
} else {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
error.value = 'Please select an image file'
|
||||
error.value = t('common.selectImageFile')
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
@@ -139,7 +148,7 @@ function handleUpload(event: Event) {
|
||||
}
|
||||
|
||||
reader.onerror = () => {
|
||||
error.value = 'Failed to read file'
|
||||
error.value = t('common.fileReadFailed')
|
||||
}
|
||||
input.value = ''
|
||||
}
|
||||
|
||||
@@ -645,14 +645,17 @@ export default {
|
||||
videoFirstFrameLabel: 'First-frame / reference image (optional)',
|
||||
imageUploadHint:
|
||||
'PNG/JPEG recommended, both sides ≥ 8 px, under ~4 MB for edits. Uploading a source image switches to /images/edits (image-to-image). Leave empty for text-to-image /images/generations.',
|
||||
videoFirstFrameHint:
|
||||
'Optional first-frame / reference image for image-to-video. PNG/JPEG recommended, both sides ≥ 8 px.',
|
||||
audioUploadLabel: 'Audio file (optional for STT)',
|
||||
audioUploadHint:
|
||||
'Upload a real audio clip to transcribe. Without a file, a silent WAV is used for connectivity only.',
|
||||
videoUploadUrlLabel: 'Video output.upload_url (ZDR, optional)',
|
||||
videoUploadUrlPlaceholder: 'https://your-public-host/v1/media/uploads/<token>',
|
||||
videoUploadUrlHint:
|
||||
'Required for full video generation on Zero Data Retention teams: a public HTTPS URL that accepts PUT of the finished MP4. Without it, the test only verifies connectivity.',
|
||||
mediaTooLarge: 'File is too large (max ~6 MB for admin test uploads).'
|
||||
mediaTooLarge: 'File is too large (max ~6 MB for admin test uploads).',
|
||||
chooseImageFile: 'Choose image',
|
||||
chooseAudioFile: 'Choose audio',
|
||||
uploadPreviewAlt: 'Upload preview',
|
||||
fileReadFailed: 'Failed to read the selected file',
|
||||
noResponseBody: 'No response body from server'
|
||||
},
|
||||
anthropic: {
|
||||
apiKeyPassthrough: 'Auto passthrough (auth only)',
|
||||
@@ -1337,6 +1340,9 @@ export default {
|
||||
reAuthorizedSuccess: 'Account re-authorized successfully',
|
||||
// Test Modal
|
||||
testAccountConnection: 'Test Account Connection',
|
||||
errorPrefix: 'Error: {message}',
|
||||
imagePreviewAlt: 'Test image {index}',
|
||||
imageLightboxAlt: 'Image preview',
|
||||
account: 'Account',
|
||||
readyToTest: 'Ready to test. Click "Start Test" to begin...',
|
||||
connectingToApi: 'Connecting to API...',
|
||||
@@ -1361,13 +1367,13 @@ export default {
|
||||
imagePromptPlaceholder: 'Example: Generate an orange cat astronaut sticker in pixel-art style on a solid background.',
|
||||
imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.',
|
||||
imageTestHint:
|
||||
'Calls standalone /v1/images/generations (Grok uses response_format=b64_json for ZDR-safe previews) and shows the returned image below.',
|
||||
'Calls standalone /v1/images/generations and shows the returned image below.',
|
||||
imageTestMode: 'Mode: Image generation test',
|
||||
videoPromptLabel: 'Video prompt',
|
||||
videoPromptPlaceholder: 'Example: A red ball bouncing once on a white floor, short simple motion.',
|
||||
videoPromptDefault: 'A red ball bouncing once on a white floor, short simple motion.',
|
||||
videoTestHint:
|
||||
'Calls standalone /v1/videos/generations. Zero Data Retention (ZDR) teams require a public output.upload_url so xAI can PUT the finished MP4 — this admin probe does not host that receiver, so ZDR accounts get connectivity-only success without producing a video file.',
|
||||
'Calls standalone /v1/videos/generations, polls until done, then downloads the finished video for on-page preview.',
|
||||
videoTestMode: 'Mode: Video generation test',
|
||||
sendingVideoRequest: 'Sending video generation request...',
|
||||
imagePreview: 'Generated images:',
|
||||
|
||||
@@ -81,6 +81,13 @@ export default {
|
||||
view: 'View',
|
||||
settings: 'Settings',
|
||||
chooseFile: 'Choose File',
|
||||
upload: 'Upload',
|
||||
remove: 'Remove',
|
||||
noFileSelected: 'No file selected',
|
||||
selectedFile: 'Selected: {name}',
|
||||
fileReadFailed: 'Failed to read file',
|
||||
selectImageFile: 'Please select an image file',
|
||||
fileTooLargeKb: 'File too large ({size} KB), max {max} KB',
|
||||
copy: 'Copy',
|
||||
notAvailable: 'N/A',
|
||||
now: 'Now',
|
||||
|
||||
@@ -715,13 +715,16 @@ export default {
|
||||
videoFirstFrameLabel: '首帧 / 参考图(可选)',
|
||||
imageUploadHint:
|
||||
'建议 PNG/JPEG,宽高均 ≥ 8 像素,编辑建议小于约 4MB。上传源图会走 /images/edits(图生图);不上传则走 /images/generations 文生图。',
|
||||
videoFirstFrameHint:
|
||||
'可选首帧/参考图用于图生视频。建议 PNG/JPEG,宽高均 ≥ 8 像素。',
|
||||
audioUploadLabel: '音频文件(STT 可选)',
|
||||
audioUploadHint: '上传真实音频做转写;不上传则用静音 WAV 仅测连通。',
|
||||
videoUploadUrlLabel: '视频 output.upload_url(ZDR,可选)',
|
||||
videoUploadUrlPlaceholder: 'https://your-public-host/v1/media/uploads/<token>',
|
||||
videoUploadUrlHint:
|
||||
'零数据保留(ZDR)账号完整出片需要:公网 HTTPS 地址,接受成品 MP4 的 PUT。不填则只验证连通性,不会真正出视频。',
|
||||
mediaTooLarge: '文件过大(管理端测试上传上限约 6MB)。'
|
||||
mediaTooLarge: '文件过大(管理端测试上传上限约 6MB)。',
|
||||
chooseImageFile: '选择图片',
|
||||
chooseAudioFile: '选择音频',
|
||||
uploadPreviewAlt: '上传预览',
|
||||
fileReadFailed: '读取所选文件失败',
|
||||
noResponseBody: '服务器未返回响应体'
|
||||
},
|
||||
anthropic: {
|
||||
apiKeyPassthrough: '自动透传(仅替换认证)',
|
||||
@@ -1389,6 +1392,9 @@ export default {
|
||||
reAuthorizedSuccess: '账号重新授权成功',
|
||||
// Test Modal
|
||||
testAccountConnection: '测试账号连接',
|
||||
errorPrefix: '错误:{message}',
|
||||
imagePreviewAlt: '测试图片 {index}',
|
||||
imageLightboxAlt: '图片预览',
|
||||
account: '账号',
|
||||
readyToTest: '准备测试。点击"开始测试"按钮开始...',
|
||||
connectingToApi: '连接 API 中...',
|
||||
@@ -1411,13 +1417,13 @@ export default {
|
||||
imagePromptPlaceholder: '例如:生成一只戴宇航员头盔的橘猫,像素插画风格,纯色背景。',
|
||||
imagePromptDefault: 'Generate a cute orange cat astronaut sticker on a clean pastel background.',
|
||||
imageTestHint:
|
||||
'调用独立 /v1/images/generations 生图(Grok 使用 response_format=b64_json 以兼容 ZDR),并在下方预览返回图片。',
|
||||
'调用独立 /v1/images/generations 生图,并在下方预览返回图片。',
|
||||
imageTestMode: '模式:生图测试',
|
||||
videoPromptLabel: '视频提示词',
|
||||
videoPromptPlaceholder: '例如:一只红球在白地板上弹跳一次,动作简短。',
|
||||
videoPromptDefault: 'A red ball bouncing once on a white floor, short simple motion.',
|
||||
videoTestHint:
|
||||
'调用独立 /v1/videos/generations。零数据保留(ZDR)账号必须提供公网可 PUT 的 output.upload_url,xAI 才会把成品 MP4 写回去;管理端测试不托管该上传接收地址,因此 ZDR 账号只会得到「连通性成功」,不会实际生成视频文件。',
|
||||
'调用独立 /v1/videos/generations,轮询至完成后下载成品视频并在页面上预览。',
|
||||
videoTestMode: '模式:视频生成测试',
|
||||
sendingVideoRequest: '正在发送视频生成测试请求...',
|
||||
imagePreview: '生成结果:',
|
||||
|
||||
@@ -81,6 +81,13 @@ export default {
|
||||
view: '查看',
|
||||
settings: '设置',
|
||||
chooseFile: '选择文件',
|
||||
upload: '上传',
|
||||
remove: '移除',
|
||||
noFileSelected: '未选择文件',
|
||||
selectedFile: '已选:{name}',
|
||||
fileReadFailed: '读取文件失败',
|
||||
selectImageFile: '请选择图片文件',
|
||||
fileTooLargeKb: '文件过大({size} KB),上限 {max} KB',
|
||||
copy: '复制',
|
||||
notAvailable: '不可用',
|
||||
now: '现在',
|
||||
|
||||
Reference in New Issue
Block a user