diff --git a/in-app/v1/src/App/Tickets/Ticket/Evaluation/index.tsx b/in-app/v1/src/App/Tickets/Ticket/Evaluation/index.tsx index 78d09bbce..cc2e50fa5 100644 --- a/in-app/v1/src/App/Tickets/Ticket/Evaluation/index.tsx +++ b/in-app/v1/src/App/Tickets/Ticket/Evaluation/index.tsx @@ -1,6 +1,8 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import cx from 'classnames'; +import { useEvaluationTag } from '@/api/evaluation'; import { Radio } from '@/components/Form'; import { Button } from '@/components/Button'; import CheckIcon from '@/icons/Check'; @@ -32,7 +34,8 @@ export function Evaluated({ onClickChangeButton }: EvaluatedProps) { interface EvaluationData { star: 0 | 1; - content: string; + content?: string; + selections?: string[]; } export interface NewEvaluationProps { @@ -43,29 +46,47 @@ export interface NewEvaluationProps { export function NewEvaluation({ initData, loading, onSubmit }: NewEvaluationProps) { const { t } = useTranslation(); - const [star, setStar] = useState<0 | 1>(); - const [content, setContent] = useState(''); + const [evaluation, setEvaluation] = useState>({}); useEffect(() => { if (initData) { - setStar(initData.star); - setContent(initData.content); + setEvaluation(initData); } }, [initData]); + const { data: tag, isLoading: isLoadingTag } = useEvaluationTag(); + const handleCommit = () => { - if (star !== undefined) { - onSubmit({ star, content }); + if (evaluation.star !== undefined) { + onSubmit(evaluation as EvaluationData); } }; + if (isLoadingTag) { + return null; + } + + const currentTag = + tag && evaluation.star !== undefined + ? evaluation.star + ? tag.positive + : tag.negative + : undefined; + + const displayInput = currentTag?.required + ? !!evaluation.selections?.length + : evaluation.star !== undefined; + return (
{t('evaluation.title')}
-
+
- setStar(1)}> + setEvaluation({ ...evaluation, star: 1, selections: [] })} + > {t('evaluation.useful')} @@ -73,7 +94,10 @@ export function NewEvaluation({ initData, loading, onSubmit }: NewEvaluationProp - setStar(0)}> + setEvaluation({ ...evaluation, star: 0, selections: [] })} + > {t('evaluation.useless')} @@ -82,21 +106,54 @@ export function NewEvaluation({ initData, loading, onSubmit }: NewEvaluationProp
-
- setContent(e.target.value)} - /> - -
+ {currentTag && currentTag.options.length > 0 && ( +
+ {currentTag.options.map((option) => { + const active = evaluation.selections?.includes(option); + return ( + + ); + })} +
+ )} + + {displayInput && ( +
+ setEvaluation({ ...evaluation, content: e.target.value })} + /> + +
+ )}
); } diff --git a/in-app/v1/src/api/evaluation.ts b/in-app/v1/src/api/evaluation.ts new file mode 100644 index 000000000..b87564168 --- /dev/null +++ b/in-app/v1/src/api/evaluation.ts @@ -0,0 +1,17 @@ +import { useQuery } from 'react-query'; + +import { http } from '@/leancloud'; +import { EvaluationTag } from '@/types'; + +export async function getEvaluationTag() { + const res = await http.get('/api/2/evaluation-tag'); + return res.data; +} + +export function useEvaluationTag() { + return useQuery({ + queryKey: ['EvaluationTag'], + queryFn: getEvaluationTag, + staleTime: Infinity, + }); +} diff --git a/in-app/v1/src/types/index.ts b/in-app/v1/src/types/index.ts index ece7c20e1..06e963c28 100644 --- a/in-app/v1/src/types/index.ts +++ b/in-app/v1/src/types/index.ts @@ -20,6 +20,17 @@ export interface Evaluation { content: string; } +export interface EvaluationTag { + positive: { + options: string[]; + required: boolean; + }; + negative: { + options: string[]; + required: boolean; + }; +} + export interface TicketListItem { id: string; nid: number; diff --git a/next/api/src/controller/config.ts b/next/api/src/controller/config.ts index 2f54edb14..6dbb6f7cc 100644 --- a/next/api/src/controller/config.ts +++ b/next/api/src/controller/config.ts @@ -4,6 +4,11 @@ import { BadRequestError, Body, Controller, Get, Param, Put, UseMiddlewares } fr import { Config } from '@/config'; import { auth, customerServiceOnly } from '@/middleware'; +const evaluationTagSchema = z.object({ + options: z.array(z.string()), + required: z.boolean(), +}); + const CONFIG_SCHEMAS: Record> = { weekday: z.array(z.number()), work_time: z.object({ @@ -20,6 +25,13 @@ const CONFIG_SCHEMAS: Record> = { }), evaluation: z.object({ timeLimit: z.number().int().min(0), + tag: z + .object({ + positive: evaluationTagSchema, + negative: evaluationTagSchema, + }) + .partial() + .optional(), }), }; diff --git a/next/api/src/controller/evaluation-tag.ts b/next/api/src/controller/evaluation-tag.ts new file mode 100644 index 000000000..3911f8246 --- /dev/null +++ b/next/api/src/controller/evaluation-tag.ts @@ -0,0 +1,40 @@ +import _ from 'lodash'; + +import { Controller, Get } from '@/common/http'; +import { Locales, ILocale } from '@/common/http/handler/param/locale'; +import { Config } from '@/config'; +import { dynamicContentService } from '@/dynamic-content'; + +@Controller('evaluation-tag') +export class EvaluationTagController { + @Get() + async getTag(@Locales() locale: ILocale) { + const value = { + positive: { + options: [] as string[], + required: false, + }, + negative: { + options: [] as string[], + required: false, + }, + }; + + const config = await Config.get('evaluation'); + if (config && config.tag) { + _.merge(value, config.tag); + } + + await dynamicContentService.renderTasks( + [value.positive.options, value.negative.options].flatMap((options) => + options.map((option, index) => ({ + getText: () => option, + setText: (option: string) => (options[index] = option), + })) + ), + locale.locales + ); + + return value; + } +} diff --git a/next/api/src/dynamic-content/dynamic-content.service.ts b/next/api/src/dynamic-content/dynamic-content.service.ts index 0f2c348b0..f767735df 100644 --- a/next/api/src/dynamic-content/dynamic-content.service.ts +++ b/next/api/src/dynamic-content/dynamic-content.service.ts @@ -14,6 +14,11 @@ interface RenderObjectConfig { template: StringTemplate; } +interface RenderTask { + getText: () => string; + setText: (text: string) => void; +} + class DynamicContentService { private fullContentCache: Cache; @@ -63,6 +68,25 @@ class DynamicContentService { } } + async renderTasks(tasks: RenderTask[], locales?: string[]) { + const extendedTasks = tasks.map((task) => ({ + ...task, + tmpl: new StringTemplate(task.getText()), + })); + + const templates = extendedTasks.map((task) => task.tmpl); + if (templates.length) { + const renderer = new AsyncDeepRenderer(templates, { + dc: (names) => this.getContentMap(names, locales), + }); + await renderer.render(); + } + + for (const task of extendedTasks) { + task.setText(task.tmpl.source); + } + } + async clearContentCache(name: string | string[]) { await this.fullContentCache.del(name); } diff --git a/next/web/src/App/Admin/Settings/Evaluation/index.tsx b/next/web/src/App/Admin/Settings/Evaluation/index.tsx index f13e031ac..a7cbe2bdb 100644 --- a/next/web/src/App/Admin/Settings/Evaluation/index.tsx +++ b/next/web/src/App/Admin/Settings/Evaluation/index.tsx @@ -1,9 +1,38 @@ +import { Fragment } from 'react'; +import { Button, Checkbox, Form, InputNumber, Select } from 'antd'; +import { merge } from 'lodash-es'; + import { useConfig, useUpdateConfig } from '@/api/config'; import { LoadingCover } from '@/components/common'; -import { Button, Form, InputNumber } from 'antd'; + +const defaultValue = { + timeLimit: 0, + tag: { + positive: { + options: [], + required: false, + }, + negative: { + options: [], + required: false, + }, + }, +}; export function EvaluationConfig() { - const { data, isLoading } = useConfig<{ timeLimit: number }>('evaluation'); + const { data, isLoading } = useConfig<{ + timeLimit: number; + tag?: { + positive?: { + options: string[]; + required: boolean; + }; + negative?: { + options: string[]; + required: boolean; + }; + }; + }>('evaluation'); const { mutate, isLoading: isSaving } = useUpdateConfig('evaluation'); @@ -13,24 +42,43 @@ export function EvaluationConfig() { {!isLoading && (
- mutate({ - ...data, - timeLimit: data.timeLimit * 1000 * 60, - }) - } + initialValues={merge({}, defaultValue, data)} + onFinish={(data) => mutate(merge({}, defaultValue, data))} + style={{ maxWidth: 600 }} > ({ + value: Math.floor(value / (1000 * 60 * 60)), + })} + getValueFromEvent={(value) => value * 1000 * 60 * 60} > - + + {[ + { name: 'positive', label: '好评选项' }, + { name: 'negative', label: '差评选项' }, + ].map(({ name, label }) => ( + + +