feat(kol): expand variable schema and polish KOL UI
- Add length/range limits to KOL variable definitions: max_length and default_value for input/textarea, min_value/max_value/default_number for number. Backend validates and normalizes; frontend renders limit inputs in KolVariableConfig and applies limits at runtime in KolDynamicForm. - Sort workspace cards by most recent prompt/package update, falling back to granted_at. Adds updated_at to KolWorkspaceCard. - Polish TemplatesView, WorkspaceView, and KOL package management UI; route create/update/publish/archive/delete toasts through i18n and expand package form copy and status labels. - Remove stale KnowledgeView.vue.patch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { reactive, watch } from "vue";
|
||||
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 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>;
|
||||
@@ -14,6 +20,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
const coverPickerOpen = ref(false);
|
||||
|
||||
const formState = reactive<CreateKolPackageRequest>({
|
||||
name: "",
|
||||
@@ -32,29 +39,57 @@ watch(
|
||||
name: props.initialValue?.name || "",
|
||||
description: props.initialValue?.description || "",
|
||||
industry: props.initialValue?.industry || "",
|
||||
tags: props.initialValue?.tags || [],
|
||||
tags: [...(props.initialValue?.tags || [])],
|
||||
price_note: props.initialValue?.price_note || "",
|
||||
cover_url: props.initialValue?.cover_url || "",
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
coverPickerOpen.value = false;
|
||||
},
|
||||
);
|
||||
|
||||
const coverPreviewUrl = computed(() => resolveApiURL(formState.cover_url));
|
||||
const coverFileName = computed(() => deriveCoverFileName(formState.cover_url));
|
||||
|
||||
function handleOk() {
|
||||
if (!formState.name.trim()) return;
|
||||
emit("submit", { ...formState });
|
||||
if (!formState.name.trim()) {
|
||||
message.warning(t("kol.manage.packageForm.nameRequired"));
|
||||
return;
|
||||
}
|
||||
emit("submit", { ...formState, tags: [...formState.tags] });
|
||||
emit("update:open", false);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit("update:open", false);
|
||||
}
|
||||
|
||||
function handleRemoveCover() {
|
||||
formState.cover_url = "";
|
||||
}
|
||||
|
||||
function handleCoverConfirmed(payload: { url: string }) {
|
||||
formState.cover_url = resolveApiURL(payload.url);
|
||||
}
|
||||
|
||||
async function uploadCover(file: File) {
|
||||
const uploaded = await imagesApi.upload(file);
|
||||
return {
|
||||
url: uploaded.url,
|
||||
fileName: uploaded.name,
|
||||
assetId: uploaded.id,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-modal
|
||||
:open="open"
|
||||
:title="initialValue ? t('kol.manage.editPackage') : t('kol.manage.createPackage')"
|
||||
:ok-text="t('common.confirm')"
|
||||
:cancel-text="t('common.cancel')"
|
||||
@ok="handleOk"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
@@ -71,17 +106,132 @@ function handleCancel() {
|
||||
<a-input v-model:value="formState.industry" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Tags">
|
||||
<a-select v-model:value="formState.tags" mode="tags" style="width: 100%" placeholder="Add tags" />
|
||||
<a-form-item :label="t('kol.manage.packageForm.tags')">
|
||||
<a-select
|
||||
v-model:value="formState.tags"
|
||||
mode="tags"
|
||||
style="width: 100%"
|
||||
:placeholder="t('kol.manage.packageForm.tagsPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Price Note">
|
||||
<a-input v-model:value="formState.price_note" />
|
||||
<a-form-item :label="t('kol.manage.packageForm.priceNote')">
|
||||
<a-input
|
||||
v-model:value="formState.price_note"
|
||||
:placeholder="t('kol.manage.packageForm.priceNotePlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="Cover URL">
|
||||
<a-input v-model:value="formState.cover_url" />
|
||||
<a-form-item :label="t('kol.manage.packageForm.cover')">
|
||||
<div class="package-cover-field">
|
||||
<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" />
|
||||
</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>
|
||||
</div>
|
||||
</template>
|
||||
</button>
|
||||
|
||||
<div class="package-cover-actions">
|
||||
<a-button @click="coverPickerOpen = true">
|
||||
{{ 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") }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<CoverPickerModal
|
||||
v-model:open="coverPickerOpen"
|
||||
:article-id="null"
|
||||
:platform-ids="[]"
|
||||
:current-url="coverPreviewUrl"
|
||||
:current-file-name="coverFileName"
|
||||
:title="t('kol.manage.packageForm.coverPicker.title')"
|
||||
:local-tab-label="t('kol.manage.packageForm.coverPicker.localTab')"
|
||||
:empty-upload-title="t('kol.manage.packageForm.coverPicker.emptyTitle')"
|
||||
:empty-upload-hint="t('kol.manage.packageForm.coverPicker.emptyHint')"
|
||||
:upload-handler="uploadCover"
|
||||
@confirmed="handleCoverConfirmed"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.package-cover-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.package-cover-picker {
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
border: 1px dashed #d0d5dd;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.package-cover-picker:hover {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.08);
|
||||
}
|
||||
|
||||
.package-cover-picker__image {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.package-cover-picker__empty {
|
||||
display: flex;
|
||||
min-height: 180px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 20px;
|
||||
color: #475467;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.package-cover-picker__empty strong {
|
||||
color: #111827;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.package-cover-picker__empty span {
|
||||
max-width: 320px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.package-cover-picker__icon {
|
||||
font-size: 28px;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.package-cover-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user