feat(frontend): promote best answers to experience candidates
- questions admin: 转为经验候选 action on published best answers, moderator-gated, confirm dialog, idempotent feedback
This commit is contained in:
@@ -82,6 +82,12 @@ export const selectBestAnswer = (questionId: number, answerId: number): Promise<
|
||||
url: `/api/aihr/question-admin/questions/${questionId}/best-answer`, method: 'post', data: { answerId }, headers: { repeatSubmit: false }
|
||||
});
|
||||
|
||||
export type PromoteCaseCandidateResult = { caseId: number; created: boolean };
|
||||
|
||||
export const promoteAnswerToCaseCandidate = (answerId: number): Promise<ApiResult<PromoteCaseCandidateResult>> => request({
|
||||
url: `/api/aihr/question-admin/answers/${answerId}/to-case-candidate`, method: 'post', headers: { repeatSubmit: false }
|
||||
});
|
||||
|
||||
export const listIncentiveRules = (): Promise<ApiResult<IncentiveRule[]>> => request({
|
||||
url: '/api/aihr/incentive-admin/rules', method: 'get'
|
||||
});
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
<el-button v-if="answer.status === 'PENDING'" link type="success" @click="reviewAnswer(answer.id, 'APPROVE')">通过</el-button>
|
||||
<el-button v-if="answer.status === 'PENDING'" link type="danger" @click="reviewAnswer(answer.id, 'REJECT')">拒绝</el-button>
|
||||
<el-button v-if="answer.status === 'PUBLISHED' && !selected.bestAnswerId" link type="warning" @click="makeBest(answer.id)">设为最佳</el-button>
|
||||
<el-button v-if="canPromoteAnswer(answer)" link type="primary" :loading="promotingAnswerId === answer.id" :disabled="promotedAnswerIds.has(answer.id)" @click="promoteToCase(answer)">{{ promotedAnswerIds.has(answer.id) ? '已转候选' : '转为经验候选' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -71,9 +72,14 @@
|
||||
|
||||
<script setup name="CommunityQuestions" lang="ts">
|
||||
import { Refresh } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { createBountyQuestion, listAdminQuestions, listIncentiveRules, reviewQuestion, reviewQuestionAnswer, saveIncentiveRule, selectBestAnswer, type AdminQuestion, type IncentiveRule } from '@/api/aihr/questions';
|
||||
import { createBountyQuestion, listAdminQuestions, listIncentiveRules, promoteAnswerToCaseCandidate, reviewQuestion, reviewQuestionAnswer, saveIncentiveRule, selectBestAnswer, type AdminQuestion, type AdminQuestionAnswer, type IncentiveRule } from '@/api/aihr/questions';
|
||||
import auth from '@/plugins/auth';
|
||||
import { operationsConsoleRoles } from '@/router/access';
|
||||
// 页面/路由未单独做 moderator 按钮控制;本页路由 meta.roles 已限定 operationsConsoleRoles(superadmin/hr_operator),
|
||||
// 两者均在后端 AihrCommunityPolicy.canModerate 名单内,且后端接口仍按 canModerate 二次强校验。
|
||||
const canModerate = computed(() => auth.hasRoleOr([...operationsConsoleRoles]));
|
||||
const activeTab=ref('questions'); const loading=ref(false); const saving=ref(false); const status=ref(''); const questions=ref<AdminQuestion[]>([]); const rules=ref<IncentiveRule[]>([]); const selected=ref<AdminQuestion|null>(null); const drawerVisible=ref(false); const bountyVisible=ref(false); const ruleVisible=ref(false);
|
||||
const rewardUnit=(accountType?:string)=>accountType==='STUDY_CREDIT'?'学分':'积分';
|
||||
const bounty=reactive({title:'',content:'',category:'服务提升',rewardPoints:20,deadline:''}); const emptyRule=():IncentiveRule=>({ruleCode:'',displayName:'',accountType:'POINTS',value:0,dailyCap:null,enabled:true}); const ruleForm=reactive<IncentiveRule>(emptyRule()); const ruleDailyCap=computed({get:()=>ruleForm.dailyCap??0,set:(value:number)=>{ruleForm.dailyCap=value||null;}});
|
||||
@@ -83,6 +89,17 @@ const selectQuestion=(row:AdminQuestion)=>{selected.value=row;drawerVisible.valu
|
||||
const reviewSelected=async(decision:'APPROVE'|'REJECT')=>{if(!selected.value)return;await reviewQuestion(selected.value.id,decision);ElMessage.success('审核结果已保存');await refreshSelected();};
|
||||
const reviewAnswer=async(id:number,decision:'APPROVE'|'REJECT')=>{await reviewQuestionAnswer(id,decision);ElMessage.success('回答审核已保存');await refreshSelected();};
|
||||
const makeBest=async(answerId:number)=>{if(!selected.value)return;await selectBestAnswer(selected.value.id,answerId);ElMessage.success('最佳答案奖励已按账户规则幂等记账');await refreshSelected();};
|
||||
const promotedAnswerIds=ref<Set<number>>(new Set()); const promotingAnswerId=ref<number|null>(null);
|
||||
const canPromoteAnswer=(answer:AdminQuestionAnswer)=>canModerate.value&&answer.status==='PUBLISHED'&&answer.best;
|
||||
const promoteToCase=async(answer:AdminQuestionAnswer)=>{
|
||||
try{await ElMessageBox.confirm('将基于该最佳答案创建待审核的经验候选(来源:社区问答),创建后仍需人工审核发布。是否继续?','转为经验候选',{confirmButtonText:'确认转为候选',cancelButtonText:'取消',type:'warning'});}catch{return;}
|
||||
promotingAnswerId.value=answer.id;
|
||||
try{
|
||||
const result=(await promoteAnswerToCaseCandidate(answer.id)).data;
|
||||
promotedAnswerIds.value=new Set([...promotedAnswerIds.value,answer.id]);
|
||||
ElMessage.success(result?.created?'已创建待审核经验候选,请在案例管理中继续审核发布':'该答案已转为经验候选,未重复创建');
|
||||
}finally{promotingAnswerId.value=null;}
|
||||
};
|
||||
const openBounty=()=>{Object.assign(bounty,{title:'',content:'',category:'服务提升',rewardPoints:20,deadline:''});bountyVisible.value=true;};
|
||||
const submitBounty=async()=>{if(!bounty.title.trim()||!bounty.content.trim())return ElMessage.warning('请完整填写征题内容');saving.value=true;try{await createBountyQuestion({...bounty,title:bounty.title.trim(),content:bounty.content.trim()});bountyVisible.value=false;ElMessage.success('有奖征题已发布');await loadQuestions();}finally{saving.value=false;}};
|
||||
const editRule=(row?:IncentiveRule)=>{Object.assign(ruleForm,emptyRule(),row||{});ruleVisible.value=true;}; const submitRule=async()=>{if(!ruleForm.ruleCode.trim()||!ruleForm.displayName.trim())return ElMessage.warning('请填写规则名称和编码');saving.value=true;try{await saveIncentiveRule({...ruleForm});ruleVisible.value=false;ElMessage.success('奖励规则已保存');await loadRules();}finally{saving.value=false;}};
|
||||
|
||||
Reference in New Issue
Block a user