chore(frontend): introduce prettier + eslint and prune unused code
- Add Prettier 3 with prettier-plugin-organize-imports (sorts/removes unused imports) - Add ESLint 10 flat config with typescript-eslint + eslint-plugin-vue + eslint-config-prettier - Add root scripts: format, format:check, lint, lint:fix - Reformat 257 files across admin-web, ops-web, desktop-client, packages - Remove unused locals/exports flagged by --noUnusedLocals/--noUnusedParameters - Fix duplicate localTabLabel key, surrogate-pair regex u-flag, NBSP literal in regex - Skip server/ (Go) and apps/browser-extension/ (deprecated per ADR) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,47 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import type { KolVariableDefinition } from "@geo/shared-types";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
getKolVariableNumberMax,
|
||||
getKolVariableNumberMin,
|
||||
getKolVariableTextMaxLength,
|
||||
sanitizeKolVariableValue,
|
||||
} from "@/lib/kol-placeholders";
|
||||
} from '@/lib/kol-placeholders'
|
||||
import type { KolVariableDefinition } from '@geo/shared-types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
variables: KolVariableDefinition[];
|
||||
modelValue: Record<string, any>;
|
||||
}>();
|
||||
variables: KolVariableDefinition[]
|
||||
modelValue: Record<string, any>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: Record<string, any>): void;
|
||||
}>();
|
||||
(e: 'update:modelValue', value: Record<string, any>): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
function updateField(variable: KolVariableDefinition, key: string, value: any) {
|
||||
const next = { ...props.modelValue, [key]: sanitizeKolVariableValue(variable, value) };
|
||||
emit("update:modelValue", next);
|
||||
const next = { ...props.modelValue, [key]: sanitizeKolVariableValue(variable, value) }
|
||||
emit('update:modelValue', next)
|
||||
}
|
||||
|
||||
const selectOptions = (variable: KolVariableDefinition) => {
|
||||
return (variable.options || []).map((opt) => ({ label: opt, value: opt }));
|
||||
};
|
||||
return (variable.options || []).map((opt) => ({ label: opt, value: opt }))
|
||||
}
|
||||
|
||||
function fieldKey(variable: KolVariableDefinition): string {
|
||||
return variable.key?.trim() || variable.id;
|
||||
return variable.key?.trim() || variable.id
|
||||
}
|
||||
|
||||
function textLimit(variable: KolVariableDefinition): number {
|
||||
return getKolVariableTextMaxLength(variable);
|
||||
return getKolVariableTextMaxLength(variable)
|
||||
}
|
||||
|
||||
function numberMin(variable: KolVariableDefinition): number {
|
||||
return getKolVariableNumberMin(variable);
|
||||
return getKolVariableNumberMin(variable)
|
||||
}
|
||||
|
||||
function numberMax(variable: KolVariableDefinition): number {
|
||||
return getKolVariableNumberMax(variable);
|
||||
return getKolVariableNumberMax(variable)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,31 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { UserOutlined, TagOutlined, TeamOutlined, ThunderboltOutlined } from "@ant-design/icons-vue";
|
||||
import type { KolPackageSummary } from "@geo/shared-types";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { TagOutlined, TeamOutlined, ThunderboltOutlined, UserOutlined } from '@ant-design/icons-vue'
|
||||
import type { KolPackageSummary } from '@geo/shared-types'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { resolveApiURL } from "@/lib/api";
|
||||
import { resolveApiURL } from '@/lib/api'
|
||||
|
||||
const props = defineProps<{
|
||||
package: KolPackageSummary;
|
||||
}>();
|
||||
package: KolPackageSummary
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "click", id: number): void;
|
||||
}>();
|
||||
(e: 'click', id: number): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const coverUrl = computed(() => resolveApiURL(props.package.cover_url));
|
||||
const { t } = useI18n()
|
||||
const coverUrl = computed(() => resolveApiURL(props.package.cover_url))
|
||||
|
||||
function handleClick() {
|
||||
emit("click", props.package.id);
|
||||
emit('click', props.package.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-card hoverable class="kol-package-card" @click="handleClick">
|
||||
<template #cover>
|
||||
<div v-if="coverUrl" class="card-cover-image" :style="{ backgroundImage: `url(${coverUrl})` }">
|
||||
<div
|
||||
v-if="coverUrl"
|
||||
class="card-cover-image"
|
||||
:style="{ backgroundImage: `url(${coverUrl})` }"
|
||||
>
|
||||
<img alt="cover" :src="coverUrl" />
|
||||
</div>
|
||||
<div v-else class="card-cover-fallback">
|
||||
@@ -46,7 +50,7 @@ function handleClick() {
|
||||
</a-avatar>
|
||||
<span class="kol-name">{{ props.package.kol_display_name }}</span>
|
||||
</div>
|
||||
<div class="industry-badge" v-if="props.package.industry">
|
||||
<div v-if="props.package.industry" class="industry-badge">
|
||||
<a-tag color="blue">
|
||||
<template #icon><TagOutlined /></template>
|
||||
{{ props.package.industry }}
|
||||
@@ -95,7 +99,7 @@ function handleClick() {
|
||||
}
|
||||
|
||||
.card-cover-image::before {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -16px;
|
||||
z-index: 0;
|
||||
@@ -106,7 +110,7 @@ function handleClick() {
|
||||
}
|
||||
|
||||
.card-cover-image::after {
|
||||
content: "";
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
<script setup lang="ts">
|
||||
import { DeleteOutlined, PictureOutlined } from "@ant-design/icons-vue";
|
||||
import { message } from "ant-design-vue";
|
||||
import { computed, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { CreateKolPackageRequest } from "@geo/shared-types";
|
||||
import { DeleteOutlined, PictureOutlined } from '@ant-design/icons-vue'
|
||||
import type { CreateKolPackageRequest } from '@geo/shared-types'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import CoverPickerModal from "@/components/CoverPickerModal.vue";
|
||||
import { imagesApi, resolveApiURL } from "@/lib/api";
|
||||
import { deriveCoverFileName } from "@/lib/cover-requirements";
|
||||
import CoverPickerModal from '@/components/CoverPickerModal.vue'
|
||||
import { imagesApi, resolveApiURL } from '@/lib/api'
|
||||
import { deriveCoverFileName } from '@/lib/cover-requirements'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
initialValue?: Partial<CreateKolPackageRequest>;
|
||||
}>();
|
||||
open: boolean
|
||||
initialValue?: Partial<CreateKolPackageRequest>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:open", value: boolean): void;
|
||||
(e: "submit", value: CreateKolPackageRequest): void;
|
||||
}>();
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'submit', value: CreateKolPackageRequest): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const coverPickerOpen = ref(false);
|
||||
const { t } = useI18n()
|
||||
const coverPickerOpen = ref(false)
|
||||
|
||||
const formState = reactive<CreateKolPackageRequest>({
|
||||
name: "",
|
||||
description: "",
|
||||
industry: "",
|
||||
name: '',
|
||||
description: '',
|
||||
industry: '',
|
||||
tags: [],
|
||||
price_note: "",
|
||||
cover_url: "",
|
||||
});
|
||||
price_note: '',
|
||||
cover_url: '',
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
Object.assign(formState, {
|
||||
name: props.initialValue?.name || "",
|
||||
description: props.initialValue?.description || "",
|
||||
industry: props.initialValue?.industry || "",
|
||||
name: props.initialValue?.name || '',
|
||||
description: props.initialValue?.description || '',
|
||||
industry: props.initialValue?.industry || '',
|
||||
tags: [...(props.initialValue?.tags || [])],
|
||||
price_note: props.initialValue?.price_note || "",
|
||||
cover_url: props.initialValue?.cover_url || "",
|
||||
});
|
||||
return;
|
||||
price_note: props.initialValue?.price_note || '',
|
||||
cover_url: props.initialValue?.cover_url || '',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
coverPickerOpen.value = false;
|
||||
coverPickerOpen.value = false
|
||||
},
|
||||
);
|
||||
)
|
||||
|
||||
const coverPreviewUrl = computed(() => resolveApiURL(formState.cover_url));
|
||||
const coverFileName = computed(() => deriveCoverFileName(formState.cover_url));
|
||||
const coverPreviewUrl = computed(() => resolveApiURL(formState.cover_url))
|
||||
const coverFileName = computed(() => deriveCoverFileName(formState.cover_url))
|
||||
|
||||
function handleOk() {
|
||||
if (!formState.name.trim()) {
|
||||
message.warning(t("kol.manage.packageForm.nameRequired"));
|
||||
return;
|
||||
message.warning(t('kol.manage.packageForm.nameRequired'))
|
||||
return
|
||||
}
|
||||
emit("submit", { ...formState, tags: [...formState.tags] });
|
||||
emit("update:open", false);
|
||||
emit('submit', { ...formState, tags: [...formState.tags] })
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit("update:open", false);
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function handleRemoveCover() {
|
||||
formState.cover_url = "";
|
||||
formState.cover_url = ''
|
||||
}
|
||||
|
||||
function handleCoverConfirmed(payload: { url: string }) {
|
||||
formState.cover_url = resolveApiURL(payload.url);
|
||||
formState.cover_url = resolveApiURL(payload.url)
|
||||
}
|
||||
|
||||
async function uploadCover(file: File) {
|
||||
const uploaded = await imagesApi.upload(file);
|
||||
const uploaded = await imagesApi.upload(file)
|
||||
return {
|
||||
url: uploaded.url,
|
||||
fileName: uploaded.name,
|
||||
assetId: uploaded.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -97,7 +97,7 @@ async function uploadCover(file: File) {
|
||||
<a-form-item :label="t('common.title')" required>
|
||||
<a-input v-model:value="formState.name" :placeholder="t('common.inputPlease')" />
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item :label="t('common.description')">
|
||||
<a-textarea v-model:value="formState.description" :rows="3" />
|
||||
</a-form-item>
|
||||
@@ -124,30 +124,34 @@ async function uploadCover(file: File) {
|
||||
|
||||
<a-form-item :label="t('kol.manage.packageForm.cover')">
|
||||
<div class="package-cover-field">
|
||||
<button
|
||||
type="button"
|
||||
class="package-cover-picker"
|
||||
@click="coverPickerOpen = true"
|
||||
>
|
||||
<button type="button" class="package-cover-picker" @click="coverPickerOpen = true">
|
||||
<template v-if="coverPreviewUrl">
|
||||
<img :src="coverPreviewUrl" :alt="t('kol.manage.packageForm.cover')" class="package-cover-picker__image" />
|
||||
<img
|
||||
:src="coverPreviewUrl"
|
||||
:alt="t('kol.manage.packageForm.cover')"
|
||||
class="package-cover-picker__image"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="package-cover-picker__empty">
|
||||
<PictureOutlined class="package-cover-picker__icon" />
|
||||
<strong>{{ t("kol.manage.packageForm.selectCover") }}</strong>
|
||||
<span>{{ t("kol.manage.packageForm.coverHint") }}</span>
|
||||
<strong>{{ t('kol.manage.packageForm.selectCover') }}</strong>
|
||||
<span>{{ t('kol.manage.packageForm.coverHint') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<div class="package-cover-actions">
|
||||
<a-button @click="coverPickerOpen = true">
|
||||
{{ coverPreviewUrl ? t("kol.manage.packageForm.changeCover") : t("kol.manage.packageForm.selectCover") }}
|
||||
{{
|
||||
coverPreviewUrl
|
||||
? t('kol.manage.packageForm.changeCover')
|
||||
: t('kol.manage.packageForm.selectCover')
|
||||
}}
|
||||
</a-button>
|
||||
<a-button v-if="coverPreviewUrl" danger @click="handleRemoveCover">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
{{ t("kol.manage.packageForm.removeCover") }}
|
||||
{{ t('kol.manage.packageForm.removeCover') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,7 +190,9 @@ async function uploadCover(file: File) {
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
transition:
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.package-cover-picker:hover {
|
||||
|
||||
@@ -1,136 +1,144 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation, useQueryClient } from "@tanstack/vue-query";
|
||||
import { message } from "ant-design-vue";
|
||||
import { EditOutlined, UserOutlined, LoadingOutlined } from "@ant-design/icons-vue";
|
||||
import type { KolProfile } from "@geo/shared-types";
|
||||
import { EditOutlined, LoadingOutlined, UserOutlined } from '@ant-design/icons-vue'
|
||||
import type { KolProfile } from '@geo/shared-types'
|
||||
import { useMutation, useQueryClient } from '@tanstack/vue-query'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { kolManageApi, resolveApiURL } from "@/lib/api";
|
||||
import { formatError } from "@/lib/errors";
|
||||
import { kolManageApi, resolveApiURL } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
|
||||
const props = defineProps<{
|
||||
profile: KolProfile;
|
||||
}>();
|
||||
profile: KolProfile
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useI18n()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const editing = ref<"display_name" | "bio" | null>(null);
|
||||
const draftDisplayName = ref(props.profile.display_name);
|
||||
const draftBio = ref(props.profile.bio ?? "");
|
||||
const avatarUploading = ref(false);
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const editing = ref<'display_name' | 'bio' | null>(null)
|
||||
const draftDisplayName = ref(props.profile.display_name)
|
||||
const draftBio = ref(props.profile.bio ?? '')
|
||||
const avatarUploading = ref(false)
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const avatarURL = computed(() => resolveApiURL(props.profile.avatar_url));
|
||||
const avatarURL = computed(() => resolveApiURL(props.profile.avatar_url))
|
||||
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: kolManageApi.updateProfile,
|
||||
onSuccess: async () => {
|
||||
message.success(t("kol.profile.persona.saveSuccess"));
|
||||
message.success(t('kol.profile.persona.saveSuccess'))
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ["kol", "profile"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["workspace", "kol-cards"] }),
|
||||
queryClient.invalidateQueries({ queryKey: ["kol", "packages"] }),
|
||||
]);
|
||||
editing.value = null;
|
||||
queryClient.invalidateQueries({ queryKey: ['kol', 'profile'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['workspace', 'kol-cards'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['kol', 'packages'] }),
|
||||
])
|
||||
editing.value = null
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(formatError(error) || t("kol.profile.persona.saveError"));
|
||||
draftDisplayName.value = props.profile.display_name;
|
||||
draftBio.value = props.profile.bio ?? "";
|
||||
message.error(formatError(error) || t('kol.profile.persona.saveError'))
|
||||
draftDisplayName.value = props.profile.display_name
|
||||
draftBio.value = props.profile.bio ?? ''
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
function saveFields(patch: { display_name?: string; bio?: string | null; avatar_url?: string | null }) {
|
||||
function saveFields(patch: {
|
||||
display_name?: string
|
||||
bio?: string | null
|
||||
avatar_url?: string | null
|
||||
}) {
|
||||
updateMutation.mutate({
|
||||
display_name: patch.display_name ?? props.profile.display_name,
|
||||
bio: patch.bio !== undefined ? patch.bio : props.profile.bio,
|
||||
avatar_url: patch.avatar_url !== undefined ? patch.avatar_url : props.profile.avatar_url,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function commitDisplayName() {
|
||||
const next = draftDisplayName.value.trim();
|
||||
const next = draftDisplayName.value.trim()
|
||||
if (!next) {
|
||||
message.error(t("kol.profile.persona.displayNameRequired"));
|
||||
return;
|
||||
message.error(t('kol.profile.persona.displayNameRequired'))
|
||||
return
|
||||
}
|
||||
if (next.length > 40) {
|
||||
message.error(t("kol.profile.persona.displayNameTooLong"));
|
||||
return;
|
||||
message.error(t('kol.profile.persona.displayNameTooLong'))
|
||||
return
|
||||
}
|
||||
if (next === props.profile.display_name) {
|
||||
editing.value = null;
|
||||
return;
|
||||
editing.value = null
|
||||
return
|
||||
}
|
||||
saveFields({ display_name: next });
|
||||
saveFields({ display_name: next })
|
||||
}
|
||||
|
||||
function commitBio() {
|
||||
const next = draftBio.value.trim();
|
||||
const next = draftBio.value.trim()
|
||||
if (next.length > 500) {
|
||||
message.error(t("kol.profile.persona.bioTooLong"));
|
||||
return;
|
||||
message.error(t('kol.profile.persona.bioTooLong'))
|
||||
return
|
||||
}
|
||||
const nextVal = next === "" ? null : next;
|
||||
const nextVal = next === '' ? null : next
|
||||
if (nextVal === (props.profile.bio ?? null)) {
|
||||
editing.value = null;
|
||||
return;
|
||||
editing.value = null
|
||||
return
|
||||
}
|
||||
saveFields({ bio: nextVal });
|
||||
saveFields({ bio: nextVal })
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
draftDisplayName.value = props.profile.display_name;
|
||||
draftBio.value = props.profile.bio ?? "";
|
||||
editing.value = null;
|
||||
draftDisplayName.value = props.profile.display_name
|
||||
draftBio.value = props.profile.bio ?? ''
|
||||
editing.value = null
|
||||
}
|
||||
|
||||
function startDisplayNameEdit() {
|
||||
draftDisplayName.value = props.profile.display_name;
|
||||
editing.value = "display_name";
|
||||
draftDisplayName.value = props.profile.display_name
|
||||
editing.value = 'display_name'
|
||||
}
|
||||
|
||||
function startBioEdit() {
|
||||
draftBio.value = props.profile.bio ?? "";
|
||||
editing.value = "bio";
|
||||
draftBio.value = props.profile.bio ?? ''
|
||||
editing.value = 'bio'
|
||||
}
|
||||
|
||||
function triggerAvatarPicker() {
|
||||
fileInput.value?.click();
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
async function handleFilePicked(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
input.value = "";
|
||||
if (!file) return;
|
||||
const input = event.target as HTMLInputElement
|
||||
const file = input.files?.[0]
|
||||
input.value = ''
|
||||
if (!file) return
|
||||
|
||||
if (!/^image\/(png|jpe?g|gif|webp)$/i.test(file.type)) {
|
||||
message.error(t("kol.profile.persona.avatarInvalidType"));
|
||||
return;
|
||||
message.error(t('kol.profile.persona.avatarInvalidType'))
|
||||
return
|
||||
}
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
message.error(t("kol.profile.persona.avatarTooLarge"));
|
||||
return;
|
||||
message.error(t('kol.profile.persona.avatarTooLarge'))
|
||||
return
|
||||
}
|
||||
|
||||
avatarUploading.value = true;
|
||||
avatarUploading.value = true
|
||||
try {
|
||||
const uploaded = await kolManageApi.uploadAvatar(file);
|
||||
saveFields({ avatar_url: uploaded.url });
|
||||
const uploaded = await kolManageApi.uploadAvatar(file)
|
||||
saveFields({ avatar_url: uploaded.url })
|
||||
} catch (error) {
|
||||
message.error(formatError(error) || t("kol.profile.persona.saveError"));
|
||||
message.error(formatError(error) || t('kol.profile.persona.saveError'))
|
||||
} finally {
|
||||
avatarUploading.value = false;
|
||||
avatarUploading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="persona-card">
|
||||
<div class="persona-avatar-wrap" :class="{ uploading: avatarUploading }" @click="triggerAvatarPicker">
|
||||
<div
|
||||
class="persona-avatar-wrap"
|
||||
:class="{ uploading: avatarUploading }"
|
||||
@click="triggerAvatarPicker"
|
||||
>
|
||||
<template v-if="avatarURL">
|
||||
<img :src="avatarURL" class="persona-avatar" alt="" />
|
||||
</template>
|
||||
@@ -141,7 +149,7 @@ async function handleFilePicked(event: Event) {
|
||||
</template>
|
||||
<div class="persona-avatar-mask">
|
||||
<LoadingOutlined v-if="avatarUploading" />
|
||||
<span v-else>{{ t("kol.profile.persona.avatarLabel") }}</span>
|
||||
<span v-else>{{ t('kol.profile.persona.avatarLabel') }}</span>
|
||||
</div>
|
||||
<input
|
||||
ref="fileInput"
|
||||
@@ -154,7 +162,7 @@ async function handleFilePicked(event: Event) {
|
||||
|
||||
<div class="persona-fields">
|
||||
<div class="persona-field">
|
||||
<label class="persona-field-label">{{ t("kol.profile.persona.displayNameLabel") }}</label>
|
||||
<label class="persona-field-label">{{ t('kol.profile.persona.displayNameLabel') }}</label>
|
||||
<template v-if="editing === 'display_name'">
|
||||
<a-input
|
||||
v-model:value="draftDisplayName"
|
||||
@@ -176,7 +184,7 @@ async function handleFilePicked(event: Event) {
|
||||
</div>
|
||||
|
||||
<div class="persona-field">
|
||||
<label class="persona-field-label">{{ t("kol.profile.persona.bioLabel") }}</label>
|
||||
<label class="persona-field-label">{{ t('kol.profile.persona.bioLabel') }}</label>
|
||||
<template v-if="editing === 'bio'">
|
||||
<a-textarea
|
||||
v-model:value="draftBio"
|
||||
@@ -194,14 +202,14 @@ async function handleFilePicked(event: Event) {
|
||||
<template v-else>
|
||||
<div class="persona-field-static persona-field-static-multiline" @click="startBioEdit">
|
||||
<span class="persona-field-value persona-field-value-bio">
|
||||
{{ profile.bio || t("kol.profile.persona.bioPlaceholder") }}
|
||||
{{ profile.bio || t('kol.profile.persona.bioPlaceholder') }}
|
||||
</span>
|
||||
<EditOutlined class="persona-field-edit-icon" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<p class="persona-avatar-hint">{{ t("kol.profile.persona.avatarHint") }}</p>
|
||||
<p class="persona-avatar-hint">{{ t('kol.profile.persona.avatarHint') }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,60 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/vue-query";
|
||||
import { message } from "ant-design-vue";
|
||||
import { LeftOutlined, SaveOutlined, SendOutlined, EyeOutlined } from "@ant-design/icons-vue";
|
||||
import { EyeOutlined, LeftOutlined, SaveOutlined, SendOutlined } from '@ant-design/icons-vue'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import KolVariablePanel from "./KolVariablePanel.vue";
|
||||
import KolVariableConfig from "./KolVariableConfig.vue";
|
||||
import KolPromptEditArea from "./KolPromptEditArea.vue";
|
||||
import KolDynamicForm from "./KolDynamicForm.vue";
|
||||
import EditorAiAssistPanel from "@/components/editor-common/EditorAiAssistPanel.vue";
|
||||
import { kolManageApi } from "@/lib/api";
|
||||
import type { KolAssistRequest, KolAssistStreamEvent, KolCardConfig, KolVariableDefinition } from "@geo/shared-types";
|
||||
import { formatError } from "@/lib/errors";
|
||||
import { buildKolPlatformOptions } from "@/lib/kol-platform-options";
|
||||
import { mergeKolCardConfig, parseKolCardConfig } from "@/lib/kol-card-config";
|
||||
import EditorAiAssistPanel from '@/components/editor-common/EditorAiAssistPanel.vue'
|
||||
import { kolManageApi } from '@/lib/api'
|
||||
import { formatError } from '@/lib/errors'
|
||||
import { mergeKolCardConfig, parseKolCardConfig } from '@/lib/kol-card-config'
|
||||
import {
|
||||
getKolVariableInitialValue,
|
||||
normalizeKolVariableDefinition,
|
||||
syncKolVariablesWithContent,
|
||||
} from "@/lib/kol-placeholders";
|
||||
} from '@/lib/kol-placeholders'
|
||||
import { buildKolPlatformOptions } from '@/lib/kol-platform-options'
|
||||
import type {
|
||||
KolAssistRequest,
|
||||
KolAssistStreamEvent,
|
||||
KolCardConfig,
|
||||
KolVariableDefinition,
|
||||
} from '@geo/shared-types'
|
||||
import KolDynamicForm from './KolDynamicForm.vue'
|
||||
import KolPromptEditArea from './KolPromptEditArea.vue'
|
||||
import KolVariableConfig from './KolVariableConfig.vue'
|
||||
import KolVariablePanel from './KolVariablePanel.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
promptId: number;
|
||||
}>();
|
||||
promptId: number
|
||||
}>()
|
||||
|
||||
type AiOptimizeStatus = "idle" | "generating" | "completed" | "error";
|
||||
type EditorAiAssistPlacement = "bottom" | "top";
|
||||
type AiOptimizeStatus = 'idle' | 'generating' | 'completed' | 'error'
|
||||
type EditorAiAssistPlacement = 'bottom' | 'top'
|
||||
type EditorAiAssistBoundary = {
|
||||
left: number;
|
||||
top: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
};
|
||||
left: number
|
||||
top: number
|
||||
right: number
|
||||
bottom: number
|
||||
}
|
||||
|
||||
type PromptSelectionAiSnapshot = {
|
||||
start: number;
|
||||
end: number;
|
||||
selectedText: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
placement: EditorAiAssistPlacement;
|
||||
boundary: EditorAiAssistBoundary;
|
||||
};
|
||||
start: number
|
||||
end: number
|
||||
selectedText: string
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
placement: EditorAiAssistPlacement
|
||||
boundary: EditorAiAssistBoundary
|
||||
}
|
||||
|
||||
type PromptSelectionAiPanelState = PromptSelectionAiSnapshot & {
|
||||
open: boolean;
|
||||
};
|
||||
open: boolean
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
back: [];
|
||||
}>();
|
||||
back: []
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useI18n()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
function createDefaultPromptSelectionAiPanelState(): PromptSelectionAiPanelState {
|
||||
return {
|
||||
@@ -62,310 +67,312 @@ function createDefaultPromptSelectionAiPanelState(): PromptSelectionAiPanelState
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
placement: "bottom",
|
||||
placement: 'bottom',
|
||||
boundary: { left: 0, top: 0, right: 0, bottom: 0 },
|
||||
start: 0,
|
||||
end: 0,
|
||||
selectedText: "",
|
||||
};
|
||||
selectedText: '',
|
||||
}
|
||||
}
|
||||
|
||||
const content = ref("");
|
||||
const variables = ref<KolVariableDefinition[]>([]);
|
||||
const previewOpen = ref(false);
|
||||
const previewValues = ref<Record<string, any>>({});
|
||||
const aiBusy = ref(false);
|
||||
const selectionAiPanel = ref<PromptSelectionAiPanelState>(createDefaultPromptSelectionAiPanelState());
|
||||
const selectionAiInstruction = ref("");
|
||||
const selectionAiPreview = ref("");
|
||||
const selectionAiError = ref("");
|
||||
const selectionAiStatus = ref<AiOptimizeStatus>("idle");
|
||||
const cardConfig = ref<KolCardConfig>({});
|
||||
const content = ref('')
|
||||
const variables = ref<KolVariableDefinition[]>([])
|
||||
const previewOpen = ref(false)
|
||||
const previewValues = ref<Record<string, any>>({})
|
||||
const aiBusy = ref(false)
|
||||
const selectionAiPanel = ref<PromptSelectionAiPanelState>(
|
||||
createDefaultPromptSelectionAiPanelState(),
|
||||
)
|
||||
const selectionAiInstruction = ref('')
|
||||
const selectionAiPreview = ref('')
|
||||
const selectionAiError = ref('')
|
||||
const selectionAiStatus = ref<AiOptimizeStatus>('idle')
|
||||
const cardConfig = ref<KolCardConfig>({})
|
||||
const variableConfigRef = ref<{
|
||||
focusVariable: (key: string, options?: { focusInput?: boolean }) => void;
|
||||
} | null>(null);
|
||||
focusVariable: (key: string, options?: { focusInput?: boolean }) => void
|
||||
} | null>(null)
|
||||
const metadataForm = reactive({
|
||||
name: "",
|
||||
platform_hint: "通用",
|
||||
name: '',
|
||||
platform_hint: '通用',
|
||||
allow_web_search: false,
|
||||
allow_user_knowledge: true,
|
||||
});
|
||||
})
|
||||
|
||||
const { data: promptDetail } = useQuery({
|
||||
queryKey: computed(() => ["kol", "prompt", props.promptId]),
|
||||
queryKey: computed(() => ['kol', 'prompt', props.promptId]),
|
||||
queryFn: () => kolManageApi.getPrompt(props.promptId),
|
||||
});
|
||||
})
|
||||
|
||||
const platformOptions = computed(() => {
|
||||
const options = buildKolPlatformOptions(t("kol.manage.platformHintDefault"));
|
||||
const currentValue = metadataForm.platform_hint.trim();
|
||||
const options = buildKolPlatformOptions(t('kol.manage.platformHintDefault'))
|
||||
const currentValue = metadataForm.platform_hint.trim()
|
||||
if (currentValue && !options.some((option) => option.value === currentValue)) {
|
||||
return [{ label: currentValue, value: currentValue }, ...options];
|
||||
return [{ label: currentValue, value: currentValue }, ...options]
|
||||
}
|
||||
return options;
|
||||
});
|
||||
const selectionAiStreaming = computed(() => selectionAiStatus.value === "generating");
|
||||
const selectionAiHasPreview = computed(() => selectionAiPreview.value.trim().length > 0);
|
||||
return options
|
||||
})
|
||||
const selectionAiStreaming = computed(() => selectionAiStatus.value === 'generating')
|
||||
const selectionAiHasPreview = computed(() => selectionAiPreview.value.trim().length > 0)
|
||||
const selectionAiUiText = computed(() => ({
|
||||
title: t("article.editor.aiOptimize.title"),
|
||||
promptPlaceholder: t("kol.manage.editor.aiOptimizePromptPlaceholder"),
|
||||
generating: t("article.editor.aiOptimize.generating"),
|
||||
failed: t("article.editor.aiOptimize.messages.failed"),
|
||||
disclaimer: t("kol.manage.editor.aiOptimizeDisclaimer"),
|
||||
regenerate: t("article.editor.aiOptimize.regenerate"),
|
||||
close: t("article.editor.aiOptimize.close"),
|
||||
replace: t("kol.manage.editor.aiOptimizeReplace"),
|
||||
}));
|
||||
title: t('article.editor.aiOptimize.title'),
|
||||
promptPlaceholder: t('kol.manage.editor.aiOptimizePromptPlaceholder'),
|
||||
generating: t('article.editor.aiOptimize.generating'),
|
||||
failed: t('article.editor.aiOptimize.messages.failed'),
|
||||
disclaimer: t('kol.manage.editor.aiOptimizeDisclaimer'),
|
||||
regenerate: t('article.editor.aiOptimize.regenerate'),
|
||||
close: t('article.editor.aiOptimize.close'),
|
||||
replace: t('kol.manage.editor.aiOptimizeReplace'),
|
||||
}))
|
||||
|
||||
watch(
|
||||
() => promptDetail.value,
|
||||
(detail) => {
|
||||
if (detail) {
|
||||
const nextCardConfig = detail.latest_card_config_json ?? {};
|
||||
const parsedCardConfig = parseKolCardConfig(nextCardConfig);
|
||||
const nextCardConfig = detail.latest_card_config_json ?? {}
|
||||
const parsedCardConfig = parseKolCardConfig(nextCardConfig)
|
||||
|
||||
metadataForm.name = detail.name;
|
||||
metadataForm.platform_hint = detail.platform_hint || "通用";
|
||||
metadataForm.allow_web_search = parsedCardConfig.allow_web_search;
|
||||
metadataForm.allow_user_knowledge = parsedCardConfig.allow_user_knowledge;
|
||||
content.value = detail.latest_prompt_content ?? "";
|
||||
metadataForm.name = detail.name
|
||||
metadataForm.platform_hint = detail.platform_hint || '通用'
|
||||
metadataForm.allow_web_search = parsedCardConfig.allow_web_search
|
||||
metadataForm.allow_user_knowledge = parsedCardConfig.allow_user_knowledge
|
||||
content.value = detail.latest_prompt_content ?? ''
|
||||
variables.value = (detail.latest_schema_json?.variables ?? []).map((variable) =>
|
||||
normalizeKolVariableDefinition(variable),
|
||||
);
|
||||
cardConfig.value = { ...nextCardConfig };
|
||||
)
|
||||
cardConfig.value = { ...nextCardConfig }
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
const publishMutation = useMutation({
|
||||
mutationFn: (payload: ReturnType<typeof buildPromptPayload>) =>
|
||||
kolManageApi.publishPrompt(props.promptId, payload),
|
||||
onSuccess: () => {
|
||||
message.success(t("kol.manage.editor.publish") + "成功");
|
||||
invalidatePromptQueries();
|
||||
message.success(t('kol.manage.editor.publish') + '成功')
|
||||
invalidatePromptQueries()
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(formatError(error));
|
||||
message.error(formatError(error))
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const saveMutation = useMutation({
|
||||
mutationFn: (payload: ReturnType<typeof buildPromptPayload>) =>
|
||||
kolManageApi.savePrompt(props.promptId, payload),
|
||||
onSuccess: () => {
|
||||
message.success(t("common.save") + "成功");
|
||||
invalidatePromptQueries();
|
||||
message.success(t('common.save') + '成功')
|
||||
invalidatePromptQueries()
|
||||
},
|
||||
onError: (error) => {
|
||||
message.error(formatError(error));
|
||||
message.error(formatError(error))
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
let aiAssistAbortController: AbortController | null = null;
|
||||
let selectionAiAbortController: AbortController | null = null;
|
||||
let aiAssistAbortController: AbortController | null = null
|
||||
let selectionAiAbortController: AbortController | null = null
|
||||
|
||||
function applyAssistResult(event: KolAssistStreamEvent) {
|
||||
const nextContent = (event.content ?? "").trim();
|
||||
const nextContent = (event.content ?? '').trim()
|
||||
if (!nextContent) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
content.value = nextContent;
|
||||
content.value = nextContent
|
||||
variables.value = syncKolVariablesWithContent(
|
||||
nextContent,
|
||||
event.schema?.variables ?? variables.value,
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
function handleAssistStreamEvent(event: KolAssistStreamEvent): "completed" | "error" | null {
|
||||
if (event.type === "start") {
|
||||
aiBusy.value = true;
|
||||
return null;
|
||||
function handleAssistStreamEvent(event: KolAssistStreamEvent): 'completed' | 'error' | null {
|
||||
if (event.type === 'start') {
|
||||
aiBusy.value = true
|
||||
return null
|
||||
}
|
||||
|
||||
if (event.type === "delta") {
|
||||
aiBusy.value = true;
|
||||
if (typeof event.content === "string") {
|
||||
content.value = event.content;
|
||||
} else if (typeof event.delta === "string" && event.delta) {
|
||||
content.value += event.delta;
|
||||
if (event.type === 'delta') {
|
||||
aiBusy.value = true
|
||||
if (typeof event.content === 'string') {
|
||||
content.value = event.content
|
||||
} else if (typeof event.delta === 'string' && event.delta) {
|
||||
content.value += event.delta
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
if (event.type === "completed") {
|
||||
aiBusy.value = false;
|
||||
applyAssistResult(event);
|
||||
message.success("AI 处理完成");
|
||||
return "completed";
|
||||
if (event.type === 'completed') {
|
||||
aiBusy.value = false
|
||||
applyAssistResult(event)
|
||||
message.success('AI 处理完成')
|
||||
return 'completed'
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
aiBusy.value = false;
|
||||
if (typeof event.content === "string" && event.content.trim()) {
|
||||
content.value = event.content;
|
||||
if (event.type === 'error') {
|
||||
aiBusy.value = false
|
||||
if (typeof event.content === 'string' && event.content.trim()) {
|
||||
content.value = event.content
|
||||
}
|
||||
message.error(event.error || "AI 处理失败");
|
||||
return "error";
|
||||
message.error(event.error || 'AI 处理失败')
|
||||
return 'error'
|
||||
}
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
function stopAiAssistStream() {
|
||||
aiAssistAbortController?.abort();
|
||||
aiAssistAbortController = null;
|
||||
aiBusy.value = false;
|
||||
aiAssistAbortController?.abort()
|
||||
aiAssistAbortController = null
|
||||
aiBusy.value = false
|
||||
}
|
||||
|
||||
async function runAiAssistStream(payload: KolAssistRequest) {
|
||||
stopAiAssistStream();
|
||||
stopAiAssistStream()
|
||||
|
||||
const controller = new AbortController();
|
||||
aiAssistAbortController = controller;
|
||||
aiBusy.value = true;
|
||||
let terminalState: "completed" | "error" | null = null;
|
||||
const controller = new AbortController()
|
||||
aiAssistAbortController = controller
|
||||
aiBusy.value = true
|
||||
let terminalState: 'completed' | 'error' | null = null
|
||||
|
||||
try {
|
||||
await kolManageApi.streamAssist(
|
||||
payload,
|
||||
{
|
||||
onEvent(event) {
|
||||
terminalState = handleAssistStreamEvent(event);
|
||||
terminalState = handleAssistStreamEvent(event)
|
||||
},
|
||||
onClose() {
|
||||
if (aiAssistAbortController === controller && terminalState === null) {
|
||||
aiBusy.value = false;
|
||||
aiBusy.value = false
|
||||
}
|
||||
},
|
||||
},
|
||||
controller.signal,
|
||||
);
|
||||
)
|
||||
|
||||
if (!controller.signal.aborted && terminalState === null) {
|
||||
aiBusy.value = false;
|
||||
message.error("AI 处理失败");
|
||||
aiBusy.value = false
|
||||
message.error('AI 处理失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
aiBusy.value = false;
|
||||
message.error(formatError(error));
|
||||
aiBusy.value = false
|
||||
message.error(formatError(error))
|
||||
} finally {
|
||||
if (aiAssistAbortController === controller) {
|
||||
aiAssistAbortController = null;
|
||||
aiAssistAbortController = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAiGenerate(description: string) {
|
||||
await runAiAssistStream({
|
||||
mode: "generate",
|
||||
mode: 'generate',
|
||||
description,
|
||||
prompt_id: props.promptId,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function resetSelectionAiOutput() {
|
||||
selectionAiPreview.value = "";
|
||||
selectionAiError.value = "";
|
||||
selectionAiStatus.value = "idle";
|
||||
selectionAiPreview.value = ''
|
||||
selectionAiError.value = ''
|
||||
selectionAiStatus.value = 'idle'
|
||||
}
|
||||
|
||||
function stopSelectionAiStream() {
|
||||
selectionAiAbortController?.abort();
|
||||
selectionAiAbortController = null;
|
||||
selectionAiAbortController?.abort()
|
||||
selectionAiAbortController = null
|
||||
|
||||
if (selectionAiStatus.value === "generating") {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? "completed" : "idle";
|
||||
selectionAiError.value = "";
|
||||
if (selectionAiStatus.value === 'generating') {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'idle'
|
||||
selectionAiError.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
function closeSelectionAiPanel() {
|
||||
stopSelectionAiStream();
|
||||
selectionAiPanel.value = createDefaultPromptSelectionAiPanelState();
|
||||
selectionAiInstruction.value = "";
|
||||
resetSelectionAiOutput();
|
||||
stopSelectionAiStream()
|
||||
selectionAiPanel.value = createDefaultPromptSelectionAiPanelState()
|
||||
selectionAiInstruction.value = ''
|
||||
resetSelectionAiOutput()
|
||||
}
|
||||
|
||||
function handleOpenSelectionAiPanel(snapshot: PromptSelectionAiSnapshot) {
|
||||
stopAiAssistStream();
|
||||
stopSelectionAiStream();
|
||||
stopAiAssistStream()
|
||||
stopSelectionAiStream()
|
||||
selectionAiPanel.value = {
|
||||
open: true,
|
||||
...snapshot,
|
||||
};
|
||||
selectionAiInstruction.value = "";
|
||||
resetSelectionAiOutput();
|
||||
}
|
||||
selectionAiInstruction.value = ''
|
||||
resetSelectionAiOutput()
|
||||
}
|
||||
|
||||
function handleSelectionAiStreamEvent(event: KolAssistStreamEvent) {
|
||||
if (event.type === "start") {
|
||||
selectionAiStatus.value = "generating";
|
||||
selectionAiError.value = "";
|
||||
return;
|
||||
if (event.type === 'start') {
|
||||
selectionAiStatus.value = 'generating'
|
||||
selectionAiError.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "delta") {
|
||||
selectionAiStatus.value = "generating";
|
||||
selectionAiError.value = "";
|
||||
if (typeof event.content === "string") {
|
||||
selectionAiPreview.value = event.content;
|
||||
} else if (typeof event.delta === "string") {
|
||||
selectionAiPreview.value += event.delta;
|
||||
if (event.type === 'delta') {
|
||||
selectionAiStatus.value = 'generating'
|
||||
selectionAiError.value = ''
|
||||
if (typeof event.content === 'string') {
|
||||
selectionAiPreview.value = event.content
|
||||
} else if (typeof event.delta === 'string') {
|
||||
selectionAiPreview.value += event.delta
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "completed") {
|
||||
selectionAiStatus.value = "completed";
|
||||
selectionAiError.value = "";
|
||||
if (typeof event.content === "string") {
|
||||
selectionAiPreview.value = event.content;
|
||||
if (event.type === 'completed') {
|
||||
selectionAiStatus.value = 'completed'
|
||||
selectionAiError.value = ''
|
||||
if (typeof event.content === 'string') {
|
||||
selectionAiPreview.value = event.content
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (event.type === "error") {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? "completed" : "error";
|
||||
selectionAiError.value = event.error || t("article.editor.aiOptimize.messages.failed");
|
||||
if (typeof event.content === "string" && event.content.trim()) {
|
||||
selectionAiPreview.value = event.content;
|
||||
if (event.type === 'error') {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'error'
|
||||
selectionAiError.value = event.error || t('article.editor.aiOptimize.messages.failed')
|
||||
if (typeof event.content === 'string' && event.content.trim()) {
|
||||
selectionAiPreview.value = event.content
|
||||
}
|
||||
if (!selectionAiHasPreview.value) {
|
||||
message.error(selectionAiError.value);
|
||||
message.error(selectionAiError.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function generateSelectionAiPreview() {
|
||||
if (!selectionAiPanel.value.open) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const instruction = selectionAiInstruction.value.trim();
|
||||
const instruction = selectionAiInstruction.value.trim()
|
||||
if (!instruction) {
|
||||
message.warning(t("article.editor.aiOptimize.messages.promptRequired"));
|
||||
return;
|
||||
message.warning(t('article.editor.aiOptimize.messages.promptRequired'))
|
||||
return
|
||||
}
|
||||
|
||||
stopSelectionAiStream();
|
||||
selectionAiPreview.value = "";
|
||||
selectionAiError.value = "";
|
||||
selectionAiStatus.value = "generating";
|
||||
stopSelectionAiStream()
|
||||
selectionAiPreview.value = ''
|
||||
selectionAiError.value = ''
|
||||
selectionAiStatus.value = 'generating'
|
||||
|
||||
const syncedVariables = syncVariablesFromContent();
|
||||
const controller = new AbortController();
|
||||
selectionAiAbortController = controller;
|
||||
const syncedVariables = syncVariablesFromContent()
|
||||
const controller = new AbortController()
|
||||
selectionAiAbortController = controller
|
||||
|
||||
try {
|
||||
await kolManageApi.streamAssist(
|
||||
{
|
||||
mode: "optimize",
|
||||
mode: 'optimize',
|
||||
description: instruction,
|
||||
current_content: selectionAiPanel.value.selectedText,
|
||||
schema: { variables: syncedVariables },
|
||||
@@ -375,49 +382,49 @@ async function generateSelectionAiPreview() {
|
||||
onEvent: handleSelectionAiStreamEvent,
|
||||
},
|
||||
controller.signal,
|
||||
);
|
||||
)
|
||||
|
||||
if (!controller.signal.aborted && selectionAiStatus.value === "generating") {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? "completed" : "error";
|
||||
if (!controller.signal.aborted && selectionAiStatus.value === 'generating') {
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'error'
|
||||
}
|
||||
} catch (error) {
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? "completed" : "error";
|
||||
selectionAiError.value = formatError(error);
|
||||
selectionAiStatus.value = selectionAiHasPreview.value ? 'completed' : 'error'
|
||||
selectionAiError.value = formatError(error)
|
||||
if (!selectionAiHasPreview.value) {
|
||||
message.error(selectionAiError.value);
|
||||
message.error(selectionAiError.value)
|
||||
}
|
||||
} finally {
|
||||
if (selectionAiAbortController === controller) {
|
||||
selectionAiAbortController = null;
|
||||
selectionAiAbortController = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applySelectionAiReplacement() {
|
||||
const replacement = selectionAiPreview.value.trim();
|
||||
const replacement = selectionAiPreview.value.trim()
|
||||
if (!selectionAiPanel.value.open || !replacement) {
|
||||
if (!replacement) {
|
||||
message.warning(t("article.editor.aiOptimize.messages.empty"));
|
||||
message.warning(t('article.editor.aiOptimize.messages.empty'))
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const target = { ...selectionAiPanel.value };
|
||||
const currentSelectedText = content.value.slice(target.start, target.end);
|
||||
const target = { ...selectionAiPanel.value }
|
||||
const currentSelectedText = content.value.slice(target.start, target.end)
|
||||
if (currentSelectedText !== target.selectedText) {
|
||||
message.warning(t("article.editor.aiOptimize.messages.selectionChanged"));
|
||||
return;
|
||||
message.warning(t('article.editor.aiOptimize.messages.selectionChanged'))
|
||||
return
|
||||
}
|
||||
|
||||
const nextContent =
|
||||
content.value.slice(0, target.start) + replacement + content.value.slice(target.end);
|
||||
content.value = nextContent;
|
||||
variables.value = syncKolVariablesWithContent(nextContent, variables.value);
|
||||
closeSelectionAiPanel();
|
||||
content.value.slice(0, target.start) + replacement + content.value.slice(target.end)
|
||||
content.value = nextContent
|
||||
variables.value = syncKolVariablesWithContent(nextContent, variables.value)
|
||||
closeSelectionAiPanel()
|
||||
}
|
||||
|
||||
function buildPromptPayload(nextVariables: KolVariableDefinition[] = variables.value) {
|
||||
@@ -431,131 +438,131 @@ function buildPromptPayload(nextVariables: KolVariableDefinition[] = variables.v
|
||||
allow_web_search: metadataForm.allow_web_search,
|
||||
allow_user_knowledge: metadataForm.allow_user_knowledge,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function syncVariablesFromContent(): KolVariableDefinition[] {
|
||||
const syncedVariables = syncKolVariablesWithContent(content.value, variables.value);
|
||||
variables.value = syncedVariables;
|
||||
return syncedVariables;
|
||||
const syncedVariables = syncKolVariablesWithContent(content.value, variables.value)
|
||||
variables.value = syncedVariables
|
||||
return syncedVariables
|
||||
}
|
||||
|
||||
function buildPreviewValues(nextVariables: KolVariableDefinition[]): Record<string, any> {
|
||||
const nextValues: Record<string, any> = {};
|
||||
const nextValues: Record<string, any> = {}
|
||||
|
||||
nextVariables.forEach((variable) => {
|
||||
const fieldKey = variable.key?.trim() || variable.id;
|
||||
nextValues[fieldKey] = getKolVariableInitialValue(variable);
|
||||
});
|
||||
const fieldKey = variable.key?.trim() || variable.id
|
||||
nextValues[fieldKey] = getKolVariableInitialValue(variable)
|
||||
})
|
||||
|
||||
return nextValues;
|
||||
return nextValues
|
||||
}
|
||||
|
||||
function invalidatePromptQueries() {
|
||||
void queryClient.invalidateQueries({ queryKey: ["kol", "prompt", props.promptId] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["kol", "packages"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ['kol', 'prompt', props.promptId] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['kol', 'packages'] })
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["kol", "packages", promptDetail.value?.package_id, "prompts"],
|
||||
});
|
||||
queryKey: ['kol', 'packages', promptDetail.value?.package_id, 'prompts'],
|
||||
})
|
||||
}
|
||||
|
||||
function validateBeforeSubmit(): boolean {
|
||||
if (!metadataForm.name.trim()) {
|
||||
message.warning(t("common.inputPlease") + t("common.title"));
|
||||
return false;
|
||||
message.warning(t('common.inputPlease') + t('common.title'))
|
||||
return false
|
||||
}
|
||||
if (!content.value.trim()) {
|
||||
message.warning("请输入提示词内容");
|
||||
return false;
|
||||
message.warning('请输入提示词内容')
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const syncedVariables = syncVariablesFromContent();
|
||||
const syncedVariables = syncVariablesFromContent()
|
||||
if (!validateBeforeSubmit()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
saveMutation.mutate(buildPromptPayload(syncedVariables));
|
||||
saveMutation.mutate(buildPromptPayload(syncedVariables))
|
||||
}
|
||||
|
||||
function handleSaveShortcut(event: KeyboardEvent) {
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "s") {
|
||||
return;
|
||||
if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 's') {
|
||||
return
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.repeat || saveMutation.isPending.value || publishMutation.isPending.value) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
handleSave();
|
||||
handleSave()
|
||||
}
|
||||
|
||||
function handlePublish() {
|
||||
const syncedVariables = syncVariablesFromContent();
|
||||
const syncedVariables = syncVariablesFromContent()
|
||||
if (!validateBeforeSubmit()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
publishMutation.mutate(buildPromptPayload(syncedVariables));
|
||||
publishMutation.mutate(buildPromptPayload(syncedVariables))
|
||||
}
|
||||
|
||||
function handlePreview() {
|
||||
const syncedVariables = syncVariablesFromContent();
|
||||
previewValues.value = buildPreviewValues(syncedVariables);
|
||||
previewOpen.value = true;
|
||||
const syncedVariables = syncVariablesFromContent()
|
||||
previewValues.value = buildPreviewValues(syncedVariables)
|
||||
previewOpen.value = true
|
||||
}
|
||||
|
||||
async function handleFocusVariable(key: string) {
|
||||
syncVariablesFromContent();
|
||||
await nextTick();
|
||||
variableConfigRef.value?.focusVariable(key, { focusInput: false });
|
||||
syncVariablesFromContent()
|
||||
await nextTick()
|
||||
variableConfigRef.value?.focusVariable(key, { focusInput: false })
|
||||
}
|
||||
|
||||
function getSelectPopupContainer(triggerNode: HTMLElement) {
|
||||
return triggerNode.parentElement ?? triggerNode;
|
||||
return triggerNode.parentElement ?? triggerNode
|
||||
}
|
||||
|
||||
function statusTagColor(status?: string): string {
|
||||
if (status === "active") return "green";
|
||||
if (status === "archived") return "orange";
|
||||
return "default";
|
||||
if (status === 'active') return 'green'
|
||||
if (status === 'archived') return 'orange'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function statusLabel(status?: string): string {
|
||||
if (status === "active") return t("kol.manage.status.active");
|
||||
if (status === "archived") return t("kol.manage.status.archived");
|
||||
return t("kol.manage.status.draft");
|
||||
if (status === 'active') return t('kol.manage.status.active')
|
||||
if (status === 'archived') return t('kol.manage.status.archived')
|
||||
return t('kol.manage.status.draft')
|
||||
}
|
||||
|
||||
function handleWindowPointerDown(event: PointerEvent) {
|
||||
if (!(selectionAiPanel.value.open && selectionAiStatus.value === "idle")) {
|
||||
return;
|
||||
if (!(selectionAiPanel.value.open && selectionAiStatus.value === 'idle')) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = event.target;
|
||||
const target = event.target
|
||||
if (
|
||||
target instanceof Element &&
|
||||
(target.closest(".editor-ai-assist") || target.closest(".prompt-selection-ai-action"))
|
||||
(target.closest('.editor-ai-assist') || target.closest('.prompt-selection-ai-action'))
|
||||
) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
closeSelectionAiPanel();
|
||||
closeSelectionAiPanel()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", handleSaveShortcut);
|
||||
window.addEventListener("pointerdown", handleWindowPointerDown);
|
||||
});
|
||||
window.addEventListener('keydown', handleSaveShortcut)
|
||||
window.addEventListener('pointerdown', handleWindowPointerDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", handleSaveShortcut);
|
||||
window.removeEventListener("pointerdown", handleWindowPointerDown);
|
||||
stopAiAssistStream();
|
||||
stopSelectionAiStream();
|
||||
});
|
||||
window.removeEventListener('keydown', handleSaveShortcut)
|
||||
window.removeEventListener('pointerdown', handleWindowPointerDown)
|
||||
stopAiAssistStream()
|
||||
stopSelectionAiStream()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -563,19 +570,21 @@ onBeforeUnmount(() => {
|
||||
<div class="editor-topbar">
|
||||
<button class="back-btn" type="button" @click="emit('back')">
|
||||
<LeftOutlined />
|
||||
<span>{{ t("common.back") }}</span>
|
||||
<span>{{ t('common.back') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="editor-header">
|
||||
<div class="header-title">
|
||||
<div class="title-row">
|
||||
<span class="prompt-name">{{ metadataForm.name || promptDetail?.name || t("common.loading") }}</span>
|
||||
<span class="prompt-name">
|
||||
{{ metadataForm.name || promptDetail?.name || t('common.loading') }}
|
||||
</span>
|
||||
<a-tag v-if="promptDetail" :color="statusTagColor(promptDetail.status)">
|
||||
{{ statusLabel(promptDetail.status) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<span class="prompt-subtitle">{{ t("kol.manage.editor.basicInfo") }}</span>
|
||||
<span class="prompt-subtitle">{{ t('kol.manage.editor.basicInfo') }}</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<a-button @click="handlePreview">
|
||||
@@ -599,7 +608,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
|
||||
<div class="editor-meta-card">
|
||||
<div class="editor-section-title">{{ t("kol.manage.editor.basicInfo") }}</div>
|
||||
<div class="editor-section-title">{{ t('kol.manage.editor.basicInfo') }}</div>
|
||||
<a-form layout="vertical" class="editor-meta-form">
|
||||
<a-form-item :label="t('common.title')" required>
|
||||
<a-input
|
||||
@@ -628,7 +637,9 @@ onBeforeUnmount(() => {
|
||||
<a-form-item label="生成文章时允许用户使用自己的知识库">
|
||||
<div class="editor-switch-field">
|
||||
<a-switch v-model:checked="metadataForm.allow_user_knowledge" />
|
||||
<span class="editor-switch-hint">默认开启,关闭后用户在生成页无法选择自己的知识库。</span>
|
||||
<span class="editor-switch-hint">
|
||||
默认开启,关闭后用户在生成页无法选择自己的知识库。
|
||||
</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -645,10 +656,7 @@ onBeforeUnmount(() => {
|
||||
@ai-optimize-selection="handleOpenSelectionAiPanel"
|
||||
@focus-variable="handleFocusVariable"
|
||||
/>
|
||||
<KolVariableConfig
|
||||
ref="variableConfigRef"
|
||||
v-model:variables="variables"
|
||||
/>
|
||||
<KolVariableConfig ref="variableConfigRef" v-model:variables="variables" />
|
||||
</div>
|
||||
|
||||
<a-modal
|
||||
@@ -657,10 +665,7 @@ onBeforeUnmount(() => {
|
||||
:footer="null"
|
||||
width="600px"
|
||||
>
|
||||
<KolDynamicForm
|
||||
:variables="variables"
|
||||
v-model:modelValue="previewValues"
|
||||
/>
|
||||
<KolDynamicForm v-model:modelValue="previewValues" :variables="variables" />
|
||||
</a-modal>
|
||||
|
||||
<EditorAiAssistPanel
|
||||
|
||||
@@ -1,65 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { CreateKolPromptRequest } from "@geo/shared-types";
|
||||
import { buildKolPlatformOptions } from "@/lib/kol-platform-options";
|
||||
import { buildKolPlatformOptions } from '@/lib/kol-platform-options'
|
||||
import type { CreateKolPromptRequest } from '@geo/shared-types'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
packageId: number;
|
||||
}>();
|
||||
open: boolean
|
||||
packageId: number
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:open", value: boolean): void;
|
||||
(e: "submit", value: CreateKolPromptRequest): void;
|
||||
}>();
|
||||
(e: 'update:open', value: boolean): void
|
||||
(e: 'submit', value: CreateKolPromptRequest): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const formState = reactive<CreateKolPromptRequest>({
|
||||
name: "",
|
||||
platform_hint: "通用",
|
||||
});
|
||||
name: '',
|
||||
platform_hint: '通用',
|
||||
})
|
||||
|
||||
const platformOptions = computed(() => buildKolPlatformOptions(t("kol.manage.platformHintDefault")));
|
||||
const platformOptions = computed(() => buildKolPlatformOptions(t('kol.manage.platformHintDefault')))
|
||||
|
||||
watch(
|
||||
() => props.open,
|
||||
(isOpen) => {
|
||||
if (isOpen) {
|
||||
formState.name = "";
|
||||
formState.platform_hint = "通用";
|
||||
formState.name = ''
|
||||
formState.platform_hint = '通用'
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
function handleOk() {
|
||||
if (!formState.name.trim()) return;
|
||||
emit("submit", { ...formState });
|
||||
emit("update:open", false);
|
||||
if (!formState.name.trim()) return
|
||||
emit('submit', { ...formState })
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit("update:open", false);
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function getSelectPopupContainer(triggerNode: HTMLElement) {
|
||||
return triggerNode.parentElement ?? triggerNode;
|
||||
return triggerNode.parentElement ?? triggerNode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
:title="t('kol.manage.createPrompt')"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-modal :open="open" :title="t('kol.manage.createPrompt')" @ok="handleOk" @cancel="handleCancel">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item :label="t('common.title')" required>
|
||||
<a-input v-model:value="formState.name" :placeholder="t('common.inputPlease')" />
|
||||
</a-form-item>
|
||||
|
||||
|
||||
<a-form-item :label="t('kol.manage.platformHint')">
|
||||
<a-select
|
||||
v-model:value="formState.platform_hint"
|
||||
@@ -67,7 +62,7 @@ function getSelectPopupContainer(triggerNode: HTMLElement) {
|
||||
option-filter-prop="label"
|
||||
show-search
|
||||
:get-popup-container="getSelectPopupContainer"
|
||||
@change="(val: string) => formState.platform_hint = val"
|
||||
@change="(val: string) => (formState.platform_hint = val)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import type { ComponentPublicInstance } from "vue";
|
||||
import { nextTick, ref } from "vue";
|
||||
import { CloseOutlined, DownOutlined } from "@ant-design/icons-vue";
|
||||
import type { KolVariableDefinition, KolVariableType } from "@geo/shared-types";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
DEFAULT_KOL_NUMBER_MAX,
|
||||
DEFAULT_KOL_NUMBER_MIN,
|
||||
@@ -12,186 +7,191 @@ import {
|
||||
getKolVariableNumberMin,
|
||||
getKolVariableTextMaxLength,
|
||||
normalizeKolVariableDefinition,
|
||||
} from "@/lib/kol-placeholders";
|
||||
} from '@/lib/kol-placeholders'
|
||||
import { CloseOutlined, DownOutlined } from '@ant-design/icons-vue'
|
||||
import type { KolVariableDefinition, KolVariableType } from '@geo/shared-types'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import { nextTick, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
variables: KolVariableDefinition[];
|
||||
}>();
|
||||
variables: KolVariableDefinition[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:variables", value: KolVariableDefinition[]): void;
|
||||
}>();
|
||||
(e: 'update:variables', value: KolVariableDefinition[]): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n();
|
||||
const panelRef = ref<HTMLDivElement | null>(null);
|
||||
const activeVariableKey = ref<string>("");
|
||||
let activeHighlightTimer: number | null = null;
|
||||
const cardRefs = new Map<string, HTMLElement>();
|
||||
const { t } = useI18n()
|
||||
const panelRef = ref<HTMLDivElement | null>(null)
|
||||
const activeVariableKey = ref<string>('')
|
||||
let activeHighlightTimer: number | null = null
|
||||
const cardRefs = new Map<string, HTMLElement>()
|
||||
|
||||
const componentTypes: Array<{ type: KolVariableType }> = [
|
||||
{ type: "input" },
|
||||
{ type: "textarea" },
|
||||
{ type: "select" },
|
||||
{ type: "number" },
|
||||
{ type: "checkbox" },
|
||||
];
|
||||
{ type: 'input' },
|
||||
{ type: 'textarea' },
|
||||
{ type: 'select' },
|
||||
{ type: 'number' },
|
||||
{ type: 'checkbox' },
|
||||
]
|
||||
|
||||
type FocusVariableOptions = {
|
||||
focusInput?: boolean;
|
||||
};
|
||||
focusInput?: boolean
|
||||
}
|
||||
|
||||
function updateVariable(index: number, updates: Partial<KolVariableDefinition>) {
|
||||
const newVariables = [...props.variables];
|
||||
const newVariables = [...props.variables]
|
||||
newVariables[index] = normalizeKolVariableDefinition({
|
||||
...newVariables[index],
|
||||
...updates,
|
||||
});
|
||||
emit("update:variables", newVariables);
|
||||
})
|
||||
emit('update:variables', newVariables)
|
||||
}
|
||||
|
||||
function removeVariable(index: number) {
|
||||
const newVariables = [...props.variables];
|
||||
newVariables.splice(index, 1);
|
||||
emit("update:variables", newVariables);
|
||||
const newVariables = [...props.variables]
|
||||
newVariables.splice(index, 1)
|
||||
emit('update:variables', newVariables)
|
||||
}
|
||||
|
||||
function handleOptionsChange(index: number, value: string) {
|
||||
const options = value
|
||||
.split(",")
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
updateVariable(index, { options });
|
||||
.filter(Boolean)
|
||||
updateVariable(index, { options })
|
||||
}
|
||||
|
||||
function handleTypeChange(index: number, type: KolVariableType) {
|
||||
updateVariable(index, { type });
|
||||
updateVariable(index, { type })
|
||||
}
|
||||
|
||||
function handleTextMaxLengthChange(index: number, value: number | null) {
|
||||
const maxLength =
|
||||
typeof value === "number" && Number.isFinite(value) && value > 0
|
||||
typeof value === 'number' && Number.isFinite(value) && value > 0
|
||||
? Math.trunc(value)
|
||||
: DEFAULT_KOL_TEXT_MAX_LENGTH;
|
||||
const currentVariable = normalizeKolVariableDefinition(props.variables[index]);
|
||||
: DEFAULT_KOL_TEXT_MAX_LENGTH
|
||||
const currentVariable = normalizeKolVariableDefinition(props.variables[index])
|
||||
|
||||
updateVariable(index, {
|
||||
max_length: maxLength,
|
||||
default_value: currentVariable.default_value?.slice(0, maxLength),
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function handleNumberMinChange(index: number, value: number | null) {
|
||||
const currentVariable = normalizeKolVariableDefinition(props.variables[index]);
|
||||
const currentVariable = normalizeKolVariableDefinition(props.variables[index])
|
||||
const minValue =
|
||||
typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_KOL_NUMBER_MIN;
|
||||
const maxValue = Math.max(getKolVariableNumberMax(currentVariable), minValue);
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : DEFAULT_KOL_NUMBER_MIN
|
||||
const maxValue = Math.max(getKolVariableNumberMax(currentVariable), minValue)
|
||||
const defaultNumber =
|
||||
currentVariable.default_number === undefined
|
||||
? undefined
|
||||
: Math.min(maxValue, Math.max(minValue, currentVariable.default_number));
|
||||
: Math.min(maxValue, Math.max(minValue, currentVariable.default_number))
|
||||
|
||||
updateVariable(index, {
|
||||
min_value: minValue,
|
||||
max_value: maxValue,
|
||||
default_number: defaultNumber,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function handleNumberMaxChange(index: number, value: number | null) {
|
||||
const currentVariable = normalizeKolVariableDefinition(props.variables[index]);
|
||||
const minValue = getKolVariableNumberMin(currentVariable);
|
||||
const currentVariable = normalizeKolVariableDefinition(props.variables[index])
|
||||
const minValue = getKolVariableNumberMin(currentVariable)
|
||||
const maxValue =
|
||||
typeof value === "number" && Number.isFinite(value)
|
||||
typeof value === 'number' && Number.isFinite(value)
|
||||
? Math.max(value, minValue)
|
||||
: Math.max(DEFAULT_KOL_NUMBER_MAX, minValue);
|
||||
: Math.max(DEFAULT_KOL_NUMBER_MAX, minValue)
|
||||
const defaultNumber =
|
||||
currentVariable.default_number === undefined
|
||||
? undefined
|
||||
: Math.min(maxValue, Math.max(minValue, currentVariable.default_number));
|
||||
: Math.min(maxValue, Math.max(minValue, currentVariable.default_number))
|
||||
|
||||
updateVariable(index, {
|
||||
min_value: minValue,
|
||||
max_value: maxValue,
|
||||
default_number: defaultNumber,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function handleDefaultNumberChange(index: number, value: number | null) {
|
||||
updateVariable(index, {
|
||||
default_number: value === null ? undefined : value,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function textLimit(variable: KolVariableDefinition): number {
|
||||
return getKolVariableTextMaxLength(variable);
|
||||
return getKolVariableTextMaxLength(variable)
|
||||
}
|
||||
|
||||
function numberMin(variable: KolVariableDefinition): number {
|
||||
return getKolVariableNumberMin(variable);
|
||||
return getKolVariableNumberMin(variable)
|
||||
}
|
||||
|
||||
function numberMax(variable: KolVariableDefinition): number {
|
||||
return getKolVariableNumberMax(variable);
|
||||
return getKolVariableNumberMax(variable)
|
||||
}
|
||||
|
||||
function setCardRef(key: string, el: Element | ComponentPublicInstance | null) {
|
||||
const element =
|
||||
el instanceof HTMLElement
|
||||
? el
|
||||
: el && "$el" in el && el.$el instanceof HTMLElement
|
||||
: el && '$el' in el && el.$el instanceof HTMLElement
|
||||
? el.$el
|
||||
: null;
|
||||
: null
|
||||
|
||||
if (element) {
|
||||
cardRefs.set(key, element);
|
||||
return;
|
||||
cardRefs.set(key, element)
|
||||
return
|
||||
}
|
||||
cardRefs.delete(key);
|
||||
cardRefs.delete(key)
|
||||
}
|
||||
|
||||
async function focusVariable(key: string, options: FocusVariableOptions = {}) {
|
||||
activeVariableKey.value = key;
|
||||
activeVariableKey.value = key
|
||||
|
||||
if (activeHighlightTimer !== null) {
|
||||
window.clearTimeout(activeHighlightTimer);
|
||||
window.clearTimeout(activeHighlightTimer)
|
||||
}
|
||||
activeHighlightTimer = window.setTimeout(() => {
|
||||
if (activeVariableKey.value === key) {
|
||||
activeVariableKey.value = "";
|
||||
activeVariableKey.value = ''
|
||||
}
|
||||
activeHighlightTimer = null;
|
||||
}, 1800);
|
||||
activeHighlightTimer = null
|
||||
}, 1800)
|
||||
|
||||
await nextTick();
|
||||
await nextTick()
|
||||
|
||||
const card = cardRefs.get(key);
|
||||
const panel = panelRef.value;
|
||||
const card = cardRefs.get(key)
|
||||
const panel = panelRef.value
|
||||
if (!card || !panel) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
const targetTop = Math.max(card.offsetTop - 16, 0);
|
||||
const targetTop = Math.max(card.offsetTop - 16, 0)
|
||||
panel.scrollTo({
|
||||
top: targetTop,
|
||||
behavior: "smooth",
|
||||
});
|
||||
behavior: 'smooth',
|
||||
})
|
||||
|
||||
if (options.focusInput === false) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
const input = card.querySelector("input");
|
||||
const input = card.querySelector('input')
|
||||
if (input instanceof HTMLInputElement) {
|
||||
input.focus();
|
||||
input.select();
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focusVariable,
|
||||
});
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -230,14 +230,16 @@ defineExpose({
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-body">
|
||||
<div class="form-item">
|
||||
<label>{{ t('kol.manage.variable.label') }}</label>
|
||||
<a-input
|
||||
:value="variable.label"
|
||||
size="small"
|
||||
@input="(e: Event) => updateVariable(index, { label: (e.target as HTMLInputElement).value })"
|
||||
@input="
|
||||
(e: Event) => updateVariable(index, { label: (e.target as HTMLInputElement).value })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -255,7 +257,10 @@ defineExpose({
|
||||
<a-input
|
||||
:value="variable.placeholder"
|
||||
size="small"
|
||||
@input="(e: Event) => updateVariable(index, { placeholder: (e.target as HTMLInputElement).value })"
|
||||
@input="
|
||||
(e: Event) =>
|
||||
updateVariable(index, { placeholder: (e.target as HTMLInputElement).value })
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -280,7 +285,10 @@ defineExpose({
|
||||
size="small"
|
||||
:maxlength="textLimit(variable)"
|
||||
show-count
|
||||
@input="(e: Event) => updateVariable(index, { default_value: (e.target as HTMLInputElement).value })"
|
||||
@input="
|
||||
(e: Event) =>
|
||||
updateVariable(index, { default_value: (e.target as HTMLInputElement).value })
|
||||
"
|
||||
/>
|
||||
<a-textarea
|
||||
v-else
|
||||
@@ -369,7 +377,10 @@ defineExpose({
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
transition: border-color 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
|
||||
transition:
|
||||
border-color 0.25s ease,
|
||||
box-shadow 0.25s ease,
|
||||
background 0.25s ease;
|
||||
}
|
||||
|
||||
.variable-card--active {
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
FontSizeOutlined,
|
||||
EditOutlined,
|
||||
OrderedListOutlined,
|
||||
NumberOutlined,
|
||||
CheckSquareOutlined,
|
||||
} from "@ant-design/icons-vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
EditOutlined,
|
||||
FontSizeOutlined,
|
||||
NumberOutlined,
|
||||
OrderedListOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t } = useI18n()
|
||||
|
||||
const componentTypes = [
|
||||
{ type: "input", icon: FontSizeOutlined },
|
||||
{ type: "textarea", icon: EditOutlined },
|
||||
{ type: "select", icon: OrderedListOutlined },
|
||||
{ type: "number", icon: NumberOutlined },
|
||||
{ type: "checkbox", icon: CheckSquareOutlined },
|
||||
] as const;
|
||||
{ type: 'input', icon: FontSizeOutlined },
|
||||
{ type: 'textarea', icon: EditOutlined },
|
||||
{ type: 'select', icon: OrderedListOutlined },
|
||||
{ type: 'number', icon: NumberOutlined },
|
||||
{ type: 'checkbox', icon: CheckSquareOutlined },
|
||||
] as const
|
||||
|
||||
function onDragStart(event: DragEvent, type: string) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData("application/kol-variable-type", type);
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData('application/kol-variable-type', type)
|
||||
event.dataTransfer.effectAllowed = 'copy'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user