| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978 |
- <template>
- <div class="table-box">
- <div class="card table-search">
- <el-form ref="elSearchFormRef" :inline="true" size="small" :model="searchForm" class="demo-form-inline" @keyup.enter="onSubmit">
- <el-form-item label="任务名称">
- <el-input v-model="searchForm.title" placeholder="任务名称" style="width:150px"></el-input>
- </el-form-item>
- <el-form-item label="分类">
- <el-select v-model="searchForm.category_id" placeholder="分类" clearable style="width:120px">
- <el-option v-for="item in categoryOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="状态">
- <el-select v-model="searchForm.status" placeholder="状态" clearable style="width:100px">
- <el-option label="启用" :value="1" />
- <el-option label="禁用" :value="0" />
- </el-select>
- </el-form-item>
- <div class="operation">
- <el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
- <el-button icon="refresh" @click="onResetSearch">重置</el-button>
- </div>
- </el-form>
- </div>
- <div class="card table-main">
- <div class="table-header">
- <div class="header-button-lf">
- <el-button type="primary" icon="Plus" @click="openDialog()">新增任务</el-button>
- </div>
- <div class="header-button-ri">
- <el-button :icon="Refresh" circle @click="refresh" />
- </div>
- </div>
- <el-table ref="myTable" :data="tableData" border size="small">
- <el-table-column prop="id" label="ID" align="center" width="80" />
- <el-table-column prop="title" label="任务名称" align="center" min-width="150" />
- <el-table-column prop="categoryId" label="分类" align="center" width="100">
- <template #default="{ row }">
- {{ getCategoryName(row.categoryId) }}
- </template>
- </el-table-column>
- <el-table-column prop="cover" label="图标" align="center" width="80">
- <template #default="{ row }">
- <el-image v-if="row.cover" :src="row.cover" style="width: 40px; height: 40px" fit="contain" />
- </template>
- </el-table-column>
- <el-table-column prop="rewardAmount" label="奖励金额(USDT)" align="center" min-width="130">
- <template #default="{ row }">
- <span class="text-success">{{ formatAmount(row.rewardAmount) }}</span>
- </template>
- </el-table-column>
- <el-table-column prop="totalCount" label="任务总量" align="center" width="90" />
- <el-table-column prop="remainCount" label="剩余数量" align="center" width="90" />
- <el-table-column prop="dailyLimit" label="每日限制" align="center" width="90" />
- <el-table-column prop="totalLimit" label="总限制" align="center" width="90" />
- <el-table-column prop="completedCount" label="已完成" align="center" width="80" />
- <el-table-column prop="minLevel" label="最低等级" align="center" width="80" />
- <el-table-column label="完成条件" align="center" min-width="150">
- <template #default="{ row }">
- <el-tag v-if="row.requirePhone" type="primary" size="small" style="margin: 2px">手机</el-tag>
- <el-tag v-if="row.requireRealname" type="success" size="small" style="margin: 2px">实名</el-tag>
- <el-tag v-if="row.requireIdcard" type="warning" size="small" style="margin: 2px">身份</el-tag>
- <span v-if="!row.requirePhone && !row.requireRealname && !row.requireIdcard" class="text-muted">无</span>
- </template>
- </el-table-column>
- <el-table-column prop="sort" label="排序" align="center" width="70" />
- <el-table-column prop="status" label="状态" align="center" width="80">
- <template #default="{ row }">
- <el-tag :type="row.status === 1 ? 'success' : 'danger'">
- {{ row.status === 1 ? '启用' : '禁用' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column prop="createdAt" label="创建时间" align="center" min-width="160">
- <template #default="{ row }">
- <span v-if="row.createdAt">{{ formatUnix(row.createdAt) }}</span>
- </template>
- </el-table-column>
- <el-table-column fixed="right" label="操作" align="center" width="240">
- <template #default="scope">
- <el-button type="primary" link @click="openDialog(scope.row)">编辑</el-button>
- <el-button type="info" link @click="openStepDialog(scope.row)">步骤</el-button>
- <el-button type="warning" link @click="openExampleDialog(scope.row)">样例</el-button>
- <el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- <Pagination :pageable="pageable" @handleCurrent="handleCurrent" />
- </div>
- </div>
- <!-- 编辑任务弹窗 -->
- <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑任务' : '新增任务'" width="600px" center destroy-on-close>
- <el-form ref="formRef" :model="formData" :rules="formRules" label-width="120px">
- <el-form-item label="任务名称" prop="title">
- <el-input v-model="formData.title" placeholder="请输入任务名称" />
- </el-form-item>
- <el-form-item label="分类" prop="category_id">
- <el-select v-model="formData.category_id" placeholder="请选择分类" style="width: 100%">
- <el-option v-for="item in categoryOptions" :key="item.id" :label="item.name" :value="item.id" />
- </el-select>
- </el-form-item>
- <el-form-item label="图标" prop="icon">
- <div class="icon-upload-wrapper">
- <el-upload
- class="icon-uploader"
- action="#"
- :show-file-list="false"
- :http-request="handleIconUpload"
- :before-upload="beforeIconUpload"
- accept="image/*"
- >
- <el-image v-if="formData.icon" :src="formData.icon" class="icon-preview-img" fit="contain" />
- <el-icon v-else class="icon-uploader-icon"><Plus /></el-icon>
- </el-upload>
- <div class="icon-actions">
- <el-button type="primary" size="small" @click="openMaterialPicker">从素材库选择</el-button>
- <el-button v-if="formData.icon" type="danger" size="small" @click="formData.icon = ''">删除</el-button>
- </div>
- </div>
- <div class="icon-url-input">
- <el-input v-model="formData.icon" placeholder="或直接输入图标URL" size="small" clearable />
- </div>
- <div class="upload-tip">建议尺寸: 100 x 100 px,支持 jpg/png/gif 格式</div>
- </el-form-item>
- <el-form-item label="描述" prop="description">
- <el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入描述" />
- </el-form-item>
- <el-form-item label="任务链接" prop="url">
- <el-input v-model="formData.url" placeholder="请输入任务链接" />
- </el-form-item>
- <el-form-item label="奖励金额(USDT)" prop="reward_amount">
- <el-input-number v-model="formData.reward_amount" :min="0" :precision="2" :step="0.1" style="width: 100%" />
- </el-form-item>
- <el-form-item label="任务总量" prop="total_count">
- <el-input-number v-model="formData.total_count" :min="1" style="width: 100%" />
- <div class="form-tip">任务可被领取的总次数</div>
- </el-form-item>
- <el-form-item label="每日限制" prop="daily_limit">
- <el-input-number v-model="formData.daily_limit" :min="0" style="width: 100%" />
- </el-form-item>
- <el-form-item label="总限制" prop="total_limit">
- <el-input-number v-model="formData.total_limit" :min="0" style="width: 100%" />
- </el-form-item>
- <el-form-item label="最低等级" prop="min_level">
- <el-input-number v-model="formData.min_level" :min="0" style="width: 100%" />
- </el-form-item>
- <el-form-item label="排序" prop="sort">
- <el-input-number v-model="formData.sort" :min="0" style="width: 100%" />
- </el-form-item>
- <el-form-item label="完成条件">
- <el-checkbox-group v-model="formData.require_conditions">
- <el-checkbox label="requirePhone">手机认证</el-checkbox>
- <el-checkbox label="requireRealname">实名认证</el-checkbox>
- <el-checkbox label="requireIdcard">身份认证</el-checkbox>
- </el-checkbox-group>
- <div class="form-tip">用户需满足选中的条件才能领取该任务</div>
- </el-form-item>
- <el-form-item label="状态" prop="status">
- <el-radio-group v-model="formData.status">
- <el-radio :value="1">启用</el-radio>
- <el-radio :value="0">禁用</el-radio>
- </el-radio-group>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
- </template>
- </el-dialog>
- <!-- 任务步骤弹窗 -->
- <el-dialog v-model="stepDialogVisible" title="任务步骤" width="900px" center destroy-on-close>
- <div class="step-header">
- <el-button type="primary" size="small" icon="Plus" @click="openStepEditDialog()">新增步骤</el-button>
- </div>
- <el-table :data="stepList" border size="small">
- <el-table-column prop="stepNo" label="步骤序号" align="center" width="80" />
- <el-table-column prop="image" label="步骤图片" align="center" width="100">
- <template #default="{ row }">
- <el-image v-if="row.image" :src="row.image" style="width: 60px; height: 60px" fit="contain" :preview-src-list="[row.image]" />
- <span v-else class="text-muted">暂无</span>
- </template>
- </el-table-column>
- <el-table-column prop="title" label="步骤标题" align="center" min-width="120" />
- <el-table-column prop="description" label="描述" align="center" min-width="200" show-overflow-tooltip />
- <el-table-column prop="requireUpload" label="需上传凭证" align="center" width="100">
- <template #default="{ row }">
- <el-tag :type="row.requireUpload === 1 ? 'danger' : 'info'" size="small">
- {{ row.requireUpload === 1 ? '是' : '否' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column fixed="right" label="操作" align="center" width="150">
- <template #default="scope">
- <el-button type="primary" link @click="openStepEditDialog(scope.row)">编辑</el-button>
- <el-button type="danger" link @click="handleDeleteStep(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </el-dialog>
- <!-- 编辑步骤弹窗 -->
- <el-dialog v-model="stepEditDialogVisible" :title="isStepEdit ? '编辑步骤' : '新增步骤'" width="600px" center destroy-on-close>
- <el-form ref="stepFormRef" :model="stepFormData" :rules="stepFormRules" label-width="100px">
- <el-form-item label="步骤序号" prop="stepNo">
- <el-input-number v-model="stepFormData.stepNo" :min="1" style="width: 100%" />
- </el-form-item>
- <el-form-item label="步骤标题" prop="title">
- <el-input v-model="stepFormData.title" placeholder="请输入步骤标题" />
- </el-form-item>
- <el-form-item label="描述" prop="description">
- <el-input v-model="stepFormData.description" type="textarea" :rows="4" placeholder="请输入步骤描述,可详细说明该步骤的操作要求" />
- </el-form-item>
- <el-form-item label="步骤图片" prop="image">
- <div class="step-image-wrapper">
- <el-input v-model="stepFormData.image" placeholder="请输入图片URL或选择素材" style="flex: 1" />
- <el-button type="primary" @click="openStepMaterialPicker">选择素材</el-button>
- </div>
- <div v-if="stepFormData.image" class="step-image-preview">
- <el-image :src="stepFormData.image" style="width: 120px; height: 120px" fit="contain" />
- <el-button type="danger" size="small" @click="stepFormData.image = ''">删除</el-button>
- </div>
- <div class="form-tip">用于展示该步骤的操作截图</div>
- </el-form-item>
- <el-form-item label="需上传凭证" prop="requireUpload">
- <el-radio-group v-model="stepFormData.requireUpload">
- <el-radio :value="1">是</el-radio>
- <el-radio :value="0">否</el-radio>
- </el-radio-group>
- <div class="form-tip">开启后,用户需上传完成凭证才能进入下一步</div>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="stepEditDialogVisible = false">取消</el-button>
- <el-button type="primary" :loading="stepSubmitLoading" @click="handleStepSubmit">确定</el-button>
- </template>
- </el-dialog>
- <!-- 审核样例弹窗 -->
- <el-dialog v-model="exampleDialogVisible" title="审核样例" width="900px" center destroy-on-close>
- <div class="example-header">
- <el-button type="primary" size="small" icon="Plus" @click="openExampleEditDialog()">新增样例</el-button>
- <span class="example-tip">审核样例用于展示任务完成示例,帮助用户了解如何正确完成任务</span>
- </div>
- <el-table :data="exampleList" border size="small">
- <el-table-column prop="id" label="ID" align="center" width="80" />
- <el-table-column prop="image" label="样例图片" align="center" width="120">
- <template #default="{ row }">
- <el-image v-if="row.image" :src="row.image" style="width: 80px; height: 80px" fit="contain" :preview-src-list="[row.image]" />
- <span v-else class="text-muted">暂无</span>
- </template>
- </el-table-column>
- <el-table-column prop="description" label="样例描述" align="center" min-width="200" show-overflow-tooltip />
- <el-table-column prop="sort" label="排序" align="center" width="80" />
- <el-table-column fixed="right" label="操作" align="center" width="150">
- <template #default="scope">
- <el-button type="primary" link @click="openExampleEditDialog(scope.row)">编辑</el-button>
- <el-button type="danger" link @click="handleDeleteExample(scope.row)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </el-dialog>
- <!-- 编辑审核样例弹窗 -->
- <el-dialog v-model="exampleEditDialogVisible" :title="isExampleEdit ? '编辑样例' : '新增样例'" width="600px" center destroy-on-close>
- <el-form ref="exampleFormRef" :model="exampleFormData" :rules="exampleFormRules" label-width="100px">
- <el-form-item label="样例图片" prop="image">
- <div class="step-image-wrapper">
- <el-input v-model="exampleFormData.image" placeholder="请输入图片URL或选择素材" style="flex: 1" />
- <el-button type="primary" @click="openExampleMaterialPicker">选择素材</el-button>
- </div>
- <div v-if="exampleFormData.image" class="step-image-preview">
- <el-image :src="exampleFormData.image" style="width: 120px; height: 120px" fit="contain" />
- <el-button type="danger" size="small" @click="exampleFormData.image = ''">删除</el-button>
- </div>
- <div class="form-tip">用于展示任务完成后的截图示例</div>
- </el-form-item>
- <el-form-item label="样例描述" prop="description">
- <el-input v-model="exampleFormData.description" type="textarea" :rows="3" placeholder="请输入样例描述,说明该截图展示的内容" />
- </el-form-item>
- <el-form-item label="排序" prop="sort">
- <el-input-number v-model="exampleFormData.sort" :min="0" style="width: 100%" />
- <div class="form-tip">数值越小排序越靠前</div>
- </el-form-item>
- </el-form>
- <template #footer>
- <el-button @click="exampleEditDialogVisible = false">取消</el-button>
- <el-button type="primary" :loading="exampleSubmitLoading" @click="handleExampleSubmit">确定</el-button>
- </template>
- </el-dialog>
- <!-- 步骤素材选择器 -->
- <MaterialPicker
- v-model="stepMaterialPickerVisible"
- :accept-types="['image', 'video']"
- @confirm="handleStepMaterialSelect"
- />
- <!-- 审核样例素材选择器 -->
- <MaterialPicker
- v-model="exampleMaterialPickerVisible"
- :accept-types="['image']"
- @confirm="handleExampleMaterialSelect"
- />
- <!-- 素材选择器 -->
- <MaterialPicker
- v-model="materialPickerVisible"
- :accept-types="['image', 'video']"
- @confirm="handleMaterialSelect"
- />
- </template>
- <script setup>
- import { ref, reactive, onMounted } from "vue";
- import dayjs from "dayjs";
- import { Refresh, Plus } from "@element-plus/icons-vue";
- import MaterialPicker from "@/components/MaterialPicker/index.vue";
- import { ElNotification, ElMessageBox } from "element-plus";
- import Pagination from "@/components/Pangination/Pagination.vue";
- import {
- getTaskList, createTask, updateTask, deleteTask,
- getTaskCategoryList, getTaskStepList, createTaskStep, updateTaskStep, deleteTaskStep,
- getTaskExampleList, createTaskExample, updateTaskExample, deleteTaskExample,
- createMaterial, getMaterialGroupList
- } from "@/api/modules/daytask.js";
- import { uploadImg } from "@/api/modules/upload.js";
- const searchForm = ref({ title: null, category_id: null, status: null });
- const tableData = ref([]);
- const pageable = reactive({ pageNum: 1, pageSize: 30, total: 0 });
- const categoryOptions = ref([]);
- const taskIconGroupId = ref(0); // task_icon 分组ID
- const dialogVisible = ref(false);
- const isEdit = ref(false);
- const formRef = ref(null);
- const materialPickerVisible = ref(false);
- const submitLoading = ref(false);
- const formData = ref({
- id: null,
- title: "",
- category_id: null,
- icon: "",
- description: "",
- url: "",
- reward_amount: 0,
- total_count: 100,
- daily_limit: 1,
- total_limit: 1,
- min_level: 0,
- sort: 0,
- require_conditions: [], // 完成条件:requirePhone, requireRealname, requireIdcard
- status: 1
- });
- const formRules = ref({
- title: [{ required: true, message: "请输入任务名称", trigger: "blur" }],
- category_id: [{ required: true, message: "请选择分类", trigger: "change" }],
- reward_amount: [{ required: true, message: "请输入奖励金额", trigger: "blur" }],
- status: [{ required: true, message: "请选择状态", trigger: "change" }]
- });
- // 步骤相关
- const stepDialogVisible = ref(false);
- const currentTaskId = ref(null);
- const stepList = ref([]);
- const stepEditDialogVisible = ref(false);
- const isStepEdit = ref(false);
- const stepFormRef = ref(null);
- const stepSubmitLoading = ref(false);
- const stepFormData = ref({
- id: null,
- taskId: null,
- stepNo: 1,
- title: "",
- description: "",
- image: "",
- requireUpload: 1
- });
- const stepMaterialPickerVisible = ref(false);
- const stepFormRules = ref({
- stepNo: [{ required: true, message: "请输入步骤序号", trigger: "blur" }],
- title: [{ required: true, message: "请输入步骤标题", trigger: "blur" }]
- });
- // 审核样例相关
- const exampleDialogVisible = ref(false);
- const exampleList = ref([]);
- const exampleEditDialogVisible = ref(false);
- const isExampleEdit = ref(false);
- const exampleFormRef = ref(null);
- const exampleSubmitLoading = ref(false);
- const exampleFormData = ref({
- id: null,
- taskId: null,
- image: "",
- description: "",
- sort: 0
- });
- const exampleMaterialPickerVisible = ref(false);
- const exampleFormRules = ref({
- image: [{ required: true, message: "请选择样例图片", trigger: "change" }]
- });
- const formatUnix = (val) => {
- if (!val) return "";
- return dayjs.unix(val).format("YYYY-MM-DD HH:mm:ss");
- };
- const formatAmount = (val) => {
- if (val === null || val === undefined) return "0.00";
- return (val / 100).toFixed(2);
- };
- const getCategoryName = (id) => {
- const item = categoryOptions.value.find(c => c.id === id);
- return item ? item.name : "-";
- };
- onMounted(() => {
- getList();
- getCategoryList();
- getTaskIconGroupId();
- });
- // 获取 task_icon 分组ID
- const getTaskIconGroupId = async () => {
- try {
- const res = await getMaterialGroupList({ code: "task_icon", pageSize: 1 });
- if (res.code === 200 && res.data.list?.length > 0) {
- taskIconGroupId.value = res.data.list[0].id;
- }
- } catch (e) {
- console.error("获取task_icon分组失败", e);
- }
- };
- const getCategoryList = async () => {
- try {
- const res = await getTaskCategoryList({ pageSize: 100 });
- if (res.code === 200) {
- categoryOptions.value = res.data.list || [];
- }
- } catch (error) {
- console.error("获取分类列表失败", error);
- }
- };
- const getList = async () => {
- tableData.value = [];
- const params = {
- current: pageable.pageNum,
- pageSize: pageable.pageSize,
- order: "sort asc, id desc"
- };
- // 搜索参数映射为后端字段名
- if (searchForm.value.title) {
- params.title = searchForm.value.title;
- }
- if (searchForm.value.category_id) {
- params.categoryId = searchForm.value.category_id;
- }
- if (searchForm.value.status !== null && searchForm.value.status !== undefined) {
- params.status = searchForm.value.status;
- }
- try {
- const res = await getTaskList(params);
- if (res.code === 200) {
- tableData.value = res.data.list || [];
- pageable.total = res.data.paging?.total || 0;
- }
- } catch (error) {
- console.error("获取列表失败", error);
- }
- };
- const onSubmit = () => {
- pageable.pageNum = 1;
- getList();
- };
- const refresh = () => getList();
- const onResetSearch = () => {
- searchForm.value = { title: null, category_id: null, status: null };
- getList();
- };
- const handleCurrent = (data) => {
- pageable.pageNum = data.current;
- pageable.pageSize = data.pageSize;
- getList();
- };
- const openDialog = (row) => {
- isEdit.value = !!row;
- if (row) {
- // 后端字段映射到前端表单字段
- // 解析完成条件
- const conditions = [];
- if (row.requirePhone) conditions.push('requirePhone');
- if (row.requireRealname) conditions.push('requireRealname');
- if (row.requireIdcard) conditions.push('requireIdcard');
- formData.value = {
- id: row.id,
- title: row.title || "",
- category_id: row.categoryId || null,
- icon: row.cover || "",
- description: row.description || "",
- url: row.targetUrl || "",
- reward_amount: row.rewardAmount || 0,
- total_count: row.totalCount || 100,
- daily_limit: row.dailyLimit || 1,
- total_limit: row.totalLimit || 1,
- min_level: row.minLevel || 0,
- sort: row.sort || 0,
- require_conditions: conditions,
- status: row.status ?? 1
- };
- } else {
- formData.value = {
- id: null,
- title: "",
- category_id: null,
- icon: "",
- description: "",
- url: "",
- reward_amount: 0,
- total_count: 100,
- daily_limit: 1,
- total_limit: 1,
- min_level: 0,
- sort: 0,
- require_conditions: [],
- status: 1
- };
- }
- dialogVisible.value = true;
- if (formRef.value) {
- formRef.value.clearValidate();
- }
- };
- const handleSubmit = async () => {
- if (!formRef.value) return;
- await formRef.value.validate(async (valid) => {
- if (!valid) return;
- submitLoading.value = true;
- try {
- const api = isEdit.value ? updateTask : createTask;
- // 后端期望 { id, data: {...} } 格式,字段名使用camelCase(与后端json tag一致)
- const { id } = formData.value;
- // 解析完成条件多选框
- const conditions = formData.value.require_conditions || [];
- const data = {
- title: formData.value.title,
- categoryId: formData.value.category_id,
- cover: formData.value.icon,
- description: formData.value.description,
- targetUrl: formData.value.url,
- rewardAmount: formData.value.reward_amount,
- totalCount: formData.value.total_count,
- remainCount: isEdit.value ? undefined : formData.value.total_count, // 新建任务时,剩余数量=任务总量
- dailyLimit: formData.value.daily_limit,
- totalLimit: formData.value.total_limit,
- minLevel: formData.value.min_level,
- sort: formData.value.sort,
- // 完成条件
- requirePhone: conditions.includes('requirePhone') ? 1 : 0,
- requireRealname: conditions.includes('requireRealname') ? 1 : 0,
- requireIdcard: conditions.includes('requireIdcard') ? 1 : 0,
- status: formData.value.status
- };
- const params = isEdit.value ? { id, data } : { data };
- const res = await api(params);
- if (res.code === 200) {
- ElNotification.success(isEdit.value ? "更新成功" : "创建成功");
- dialogVisible.value = false;
- getList();
- } else {
- ElNotification.error(res.msg || (isEdit.value ? "更新失败" : "创建失败"));
- }
- } catch (error) {
- ElNotification.error(isEdit.value ? "更新失败" : "创建失败");
- } finally {
- submitLoading.value = false;
- }
- });
- };
- // ==================== 素材选择器 ====================
- const openMaterialPicker = () => {
- materialPickerVisible.value = true;
- };
- const handleMaterialSelect = (material) => {
- formData.value.icon = material.url;
- };
- // ==================== 图标上传 ====================
- const beforeIconUpload = (file) => {
- const isImage = file.type.startsWith("image/");
- const isLt5M = file.size / 1024 / 1024 < 5;
- if (!isImage) {
- ElNotification.warning("只能上传图片文件");
- return false;
- }
- if (!isLt5M) {
- ElNotification.warning("图片大小不能超过 5MB");
- return false;
- }
- return true;
- };
- const handleIconUpload = async (options) => {
- const uploadFormData = new FormData();
- uploadFormData.append("file", options.file);
- uploadFormData.append("autoSave", "false"); // 禁止后端自动保存,由前端控制
- try {
- const res = await uploadImg(uploadFormData);
- if (res.code === 200) {
- formData.value.icon = res.data.path;
- // 保存到素材库,使用 task_icon 分组ID
- const fileName = options.file.name; // 保留完整文件名
- const file = options.file;
- try {
- await createMaterial({
- data: {
- name: fileName,
- type: "image",
- url: res.data.path,
- size: file.size || 0,
- mimeType: file.type || "",
- groupId: taskIconGroupId.value || 0,
- status: 1
- }
- });
- ElNotification.success("上传成功并已保存到素材库");
- } catch (e) {
- ElNotification.success("图片上传成功");
- }
- } else {
- ElNotification.error(res.msg || "图片上传失败");
- }
- } catch (error) {
- console.error("上传失败", error);
- ElNotification.error("图片上传失败");
- }
- };
- const handleDelete = async (row) => {
- try {
- await ElMessageBox.confirm("确认删除该任务吗?", "提示", {
- type: "warning",
- confirmButtonText: "确定",
- cancelButtonText: "取消"
- });
- const res = await deleteTask({ id: row.id });
- if (res.code === 200) {
- ElNotification.success("删除成功");
- getList();
- } else {
- ElNotification.error(res.msg || "删除失败");
- }
- } catch (error) {
- if (error !== "cancel" && error !== "close") {
- ElNotification.error("删除失败");
- }
- }
- };
- // 步骤相关方法
- const openStepDialog = async (row) => {
- currentTaskId.value = row.id;
- stepDialogVisible.value = true;
- await getStepList();
- };
- const getStepList = async () => {
- try {
- const res = await getTaskStepList({ taskId: currentTaskId.value, pageSize: 100, order: "step_no asc" });
- if (res.code === 200) {
- stepList.value = res.data.list || [];
- }
- } catch (error) {
- console.error("获取步骤列表失败", error);
- }
- };
- const openStepEditDialog = (row) => {
- isStepEdit.value = !!row;
- if (row) {
- stepFormData.value = { ...row };
- } else {
- stepFormData.value = {
- id: null,
- taskId: currentTaskId.value,
- stepNo: stepList.value.length + 1,
- title: "",
- description: "",
- image: "",
- requireUpload: 1
- };
- }
- stepEditDialogVisible.value = true;
- if (stepFormRef.value) {
- stepFormRef.value.clearValidate();
- }
- };
- // 步骤素材选择器
- const openStepMaterialPicker = () => {
- stepMaterialPickerVisible.value = true;
- };
- const handleStepMaterialSelect = (material) => {
- stepFormData.value.image = material.url;
- };
- const handleStepSubmit = async () => {
- if (!stepFormRef.value) return;
- await stepFormRef.value.validate(async (valid) => {
- if (!valid) return;
- stepSubmitLoading.value = true;
- try {
- const api = isStepEdit.value ? updateTaskStep : createTaskStep;
- // 后端期望 { id, data: {...} } 格式
- const { id } = stepFormData.value;
- const data = {
- taskId: stepFormData.value.taskId || currentTaskId.value,
- stepNo: stepFormData.value.stepNo,
- title: stepFormData.value.title,
- description: stepFormData.value.description,
- image: stepFormData.value.image,
- requireUpload: stepFormData.value.requireUpload
- };
- const params = isStepEdit.value ? { id, data } : { data };
- const res = await api(params);
- if (res.code === 200) {
- ElNotification.success(isStepEdit.value ? "更新成功" : "创建成功");
- stepEditDialogVisible.value = false;
- getStepList();
- } else {
- ElNotification.error(res.msg || (isStepEdit.value ? "更新失败" : "创建失败"));
- }
- } catch (error) {
- ElNotification.error(isStepEdit.value ? "更新失败" : "创建失败");
- } finally {
- stepSubmitLoading.value = false;
- }
- });
- };
- const handleDeleteStep = async (row) => {
- try {
- await ElMessageBox.confirm("确认删除该步骤吗?", "提示", {
- type: "warning",
- confirmButtonText: "确定",
- cancelButtonText: "取消"
- });
- const res = await deleteTaskStep({ id: row.id });
- if (res.code === 200) {
- ElNotification.success("删除成功");
- getStepList();
- } else {
- ElNotification.error(res.msg || "删除失败");
- }
- } catch (error) {
- if (error !== "cancel" && error !== "close") {
- ElNotification.error("删除失败");
- }
- }
- };
- // ==================== 审核样例相关方法 ====================
- const openExampleDialog = async (row) => {
- currentTaskId.value = row.id;
- exampleDialogVisible.value = true;
- await getExampleList();
- };
- const getExampleList = async () => {
- try {
- const res = await getTaskExampleList({ taskId: currentTaskId.value, pageSize: 100, order: "sort asc, id asc" });
- if (res.code === 200) {
- exampleList.value = res.data.list || [];
- }
- } catch (error) {
- console.error("获取审核样例列表失败", error);
- }
- };
- const openExampleEditDialog = (row) => {
- isExampleEdit.value = !!row;
- if (row) {
- exampleFormData.value = { ...row };
- } else {
- exampleFormData.value = {
- id: null,
- taskId: currentTaskId.value,
- image: "",
- description: "",
- sort: 0
- };
- }
- exampleEditDialogVisible.value = true;
- if (exampleFormRef.value) {
- exampleFormRef.value.clearValidate();
- }
- };
- // 审核样例素材选择器
- const openExampleMaterialPicker = () => {
- exampleMaterialPickerVisible.value = true;
- };
- const handleExampleMaterialSelect = (material) => {
- exampleFormData.value.image = material.url;
- };
- const handleExampleSubmit = async () => {
- if (!exampleFormRef.value) return;
- await exampleFormRef.value.validate(async (valid) => {
- if (!valid) return;
- exampleSubmitLoading.value = true;
- try {
- const api = isExampleEdit.value ? updateTaskExample : createTaskExample;
- const { id } = exampleFormData.value;
- const data = {
- taskId: exampleFormData.value.taskId || currentTaskId.value,
- image: exampleFormData.value.image,
- description: exampleFormData.value.description,
- sort: exampleFormData.value.sort
- };
- const params = isExampleEdit.value ? { id, data } : { data };
- const res = await api(params);
- if (res.code === 200) {
- ElNotification.success(isExampleEdit.value ? "更新成功" : "创建成功");
- exampleEditDialogVisible.value = false;
- getExampleList();
- } else {
- ElNotification.error(res.msg || (isExampleEdit.value ? "更新失败" : "创建失败"));
- }
- } catch (error) {
- ElNotification.error(isExampleEdit.value ? "更新失败" : "创建失败");
- } finally {
- exampleSubmitLoading.value = false;
- }
- });
- };
- const handleDeleteExample = async (row) => {
- try {
- await ElMessageBox.confirm("确认删除该审核样例吗?", "提示", {
- type: "warning",
- confirmButtonText: "确定",
- cancelButtonText: "取消"
- });
- const res = await deleteTaskExample({ id: row.id });
- if (res.code === 200) {
- ElNotification.success("删除成功");
- getExampleList();
- } else {
- ElNotification.error(res.msg || "删除失败");
- }
- } catch (error) {
- if (error !== "cancel" && error !== "close") {
- ElNotification.error("删除失败");
- }
- }
- };
- </script>
- <style scoped>
- .operation {
- display: inline-block;
- vertical-align: middle;
- }
- .text-success {
- color: #67c23a;
- font-weight: bold;
- }
- .step-header {
- margin-bottom: 16px;
- }
- .icon-upload-wrapper {
- display: flex;
- align-items: flex-start;
- gap: 10px;
- }
- .icon-actions {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .icon-uploader {
- border: 1px dashed #d9d9d9;
- border-radius: 6px;
- cursor: pointer;
- overflow: hidden;
- transition: border-color 0.3s;
- }
- .icon-uploader:hover {
- border-color: #409eff;
- }
- .icon-uploader :deep(.el-upload) {
- width: 80px;
- height: 80px;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- .icon-preview-img {
- width: 80px;
- height: 80px;
- }
- .icon-uploader-icon {
- font-size: 24px;
- color: #8c939d;
- }
- .icon-url-input {
- margin-top: 10px;
- width: 100%;
- }
- .upload-tip {
- font-size: 12px;
- color: #909399;
- margin-top: 5px;
- }
- .step-image-wrapper {
- display: flex;
- gap: 10px;
- width: 100%;
- }
- .step-image-preview {
- display: flex;
- align-items: flex-end;
- gap: 10px;
- margin-top: 10px;
- }
- .form-tip {
- color: #909399;
- font-size: 12px;
- line-height: 1.5;
- margin-top: 4px;
- }
- .text-muted {
- color: #c0c4cc;
- font-size: 12px;
- }
- .example-header {
- margin-bottom: 16px;
- display: flex;
- align-items: center;
- gap: 16px;
- }
- .example-tip {
- color: #909399;
- font-size: 12px;
- }
- </style>
|