Skip to content

Commit

Permalink
feat: evaluation tag
Browse files Browse the repository at this point in the history
  • Loading branch information
sdjdd committed Dec 12, 2023
1 parent 81cad9e commit a5826d2
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 38 deletions.
107 changes: 82 additions & 25 deletions in-app/v1/src/App/Tickets/Ticket/Evaluation/index.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -32,7 +34,8 @@ export function Evaluated({ onClickChangeButton }: EvaluatedProps) {

interface EvaluationData {
star: 0 | 1;
content: string;
content?: string;
selections?: string[];
}

export interface NewEvaluationProps {
Expand All @@ -43,37 +46,58 @@ 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<Partial<EvaluationData>>({});

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 (
<div className="py-5 px-4 border-t border-dashed border-gray-300 text-sm">
<div className="text-gray-600">{t('evaluation.title')}</div>

<div className="py-6">
<div className="my-6">
<span>
<Radio checked={star === 1} onChange={() => setStar(1)}>
<Radio
checked={evaluation.star === 1}
onChange={() => setEvaluation({ ...evaluation, star: 1, selections: [] })}
>
<span className="inline-flex items-center text-[#FF8156]">
<ThumbUpIcon className="w-[14px] h-[14px] inline-block mr-1" />
{t('evaluation.useful')}
</span>
</Radio>
</span>
<span className="ml-14">
<Radio checked={star === 0} onChange={() => setStar(0)}>
<Radio
checked={evaluation.star === 0}
onChange={() => setEvaluation({ ...evaluation, star: 0, selections: [] })}
>
<span className="inline-flex items-center text-[#3AB1F3]">
<ThumbDownIcon className="w-[14px] h-[14px] inline-block mr-1" />
{t('evaluation.useless')}
Expand All @@ -82,21 +106,54 @@ export function NewEvaluation({ initData, loading, onSubmit }: NewEvaluationProp
</span>
</div>

<div className="flex items-center">
<input
className="grow leading-[16px] border rounded-full placeholder-[#BFBFBF] px-3 py-[7px]"
placeholder={t('evaluation.content_hint')}
value={content}
onChange={(e) => setContent(e.target.value)}
/>
<Button
className="ml-2 text-[13px] leading-[30px]"
disabled={star === undefined || loading}
onClick={handleCommit}
>
{t('general.commit')}
</Button>
</div>
{currentTag && currentTag.options.length > 0 && (
<div className="flex flex-wrap gap-2">
{currentTag.options.map((option) => {
const active = evaluation.selections?.includes(option);
return (
<button
className={cx(
'px-2 py-1 rounded',
active ? 'bg-tapBlue text-white font-bold' : 'bg-[#F5F7F8] text-[#868C92]'
)}
onClick={() => {
if (active) {
setEvaluation({
...evaluation,
selections: evaluation.selections?.filter((t) => t !== option),
});
} else {
setEvaluation({
...evaluation,
selections: [option],
});
}
}}
>
{option}
</button>
);
})}
</div>
)}

{displayInput && (
<div className="flex items-center mt-6">
<input
className="grow leading-[16px] border rounded-full placeholder-[#BFBFBF] px-3 py-[7px]"
placeholder={t('evaluation.content_hint')}
value={evaluation.content}
onChange={(e) => setEvaluation({ ...evaluation, content: e.target.value })}
/>
<Button
className="ml-2 text-[13px] leading-[30px]"
disabled={loading}
onClick={handleCommit}
>
{t('general.commit')}
</Button>
</div>
)}
</div>
);
}
17 changes: 17 additions & 0 deletions in-app/v1/src/api/evaluation.ts
Original file line number Diff line number Diff line change
@@ -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<EvaluationTag>('/api/2/evaluation-tag');
return res.data;
}

export function useEvaluationTag() {
return useQuery({
queryKey: ['EvaluationTag'],
queryFn: getEvaluationTag,
staleTime: Infinity,
});
}
11 changes: 11 additions & 0 deletions in-app/v1/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions next/api/src/controller/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, z.Schema<any>> = {
weekday: z.array(z.number()),
work_time: z.object({
Expand All @@ -20,6 +25,13 @@ const CONFIG_SCHEMAS: Record<string, z.Schema<any>> = {
}),
evaluation: z.object({
timeLimit: z.number().int().min(0),
tag: z
.object({
positive: evaluationTagSchema,
negative: evaluationTagSchema,
})
.partial()
.optional(),
}),
};

Expand Down
40 changes: 40 additions & 0 deletions next/api/src/controller/evaluation-tag.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
24 changes: 24 additions & 0 deletions next/api/src/dynamic-content/dynamic-content.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ interface RenderObjectConfig {
template: StringTemplate;
}

interface RenderTask {
getText: () => string;
setText: (text: string) => void;
}

class DynamicContentService {
private fullContentCache: Cache;

Expand Down Expand Up @@ -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);
}
Expand Down
74 changes: 61 additions & 13 deletions next/web/src/App/Admin/Settings/Evaluation/index.tsx
Original file line number Diff line number Diff line change
@@ -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');

Expand All @@ -13,24 +42,43 @@ export function EvaluationConfig() {
{!isLoading && (
<Form
layout="vertical"
initialValues={{
...data,
timeLimit: Math.floor((data?.timeLimit || 0) / 1000 / 60),
}}
onFinish={(data) =>
mutate({
...data,
timeLimit: data.timeLimit * 1000 * 60,
})
}
initialValues={merge({}, defaultValue, data)}
onFinish={(data) => mutate(merge({}, defaultValue, data))}
style={{ maxWidth: 600 }}
>
<Form.Item
name="timeLimit"
label="评价时限"
extra="工单关闭后多长时间内允许用户评价,设为 0 表示没有限制"
getValueProps={(value) => ({
value: Math.floor(value / (1000 * 60 * 60)),
})}
getValueFromEvent={(value) => value * 1000 * 60 * 60}
>
<InputNumber min={0} addonAfter="分钟" />
<InputNumber min={0} addonAfter="小时" />
</Form.Item>
{[
{ name: 'positive', label: '好评选项' },
{ name: 'negative', label: '差评选项' },
].map(({ name, label }) => (
<Fragment key={name}>
<Form.Item
name={['tag', name, 'options']}
label={label}
extra="可使用动态内容占位符"
style={{ marginBottom: 0 }}
>
<Select mode="tags" />
</Form.Item>
<Form.Item
name={['tag', name, 'required']}
valuePropName="checked"
initialValue={false}
>
<Checkbox>必选</Checkbox>
</Form.Item>
</Fragment>
))}
<Button type="primary" htmlType="submit" loading={isSaving}>
保存
</Button>
Expand Down

0 comments on commit a5826d2

Please sign in to comment.