deepchart后台管理系统
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

756 lines
20 KiB

2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
2 months ago
  1. <template>
  2. <div class="page-container">
  3. <div class="search-container">
  4. <el-button type="danger" @click="add">添加</el-button>
  5. </div>
  6. <!-- 数据 -->
  7. <el-table
  8. :data="tableData"
  9. style="width: 100%; margin-top: 20px"
  10. header-cell-class-name="table-header"
  11. @sort-change="handleSortChange"
  12. :default-sort="{ prop: null, order: null }"
  13. class="table-rounded"
  14. :loading="tableLoading"
  15. >
  16. <el-table-column
  17. prop="id"
  18. label="序号"
  19. align="center"
  20. header-align="center"
  21. width="80"
  22. >
  23. <template #default="scope">
  24. {{ (currentPage - 1) * pageSize + scope.$index + 1 }}
  25. </template>
  26. </el-table-column>
  27. <el-table-column
  28. prop="prize_type"
  29. label="类型"
  30. align="center"
  31. header-align="center"
  32. >
  33. <template #default="scope">
  34. {{ prizeTypeOptions.find(item => item.value === scope.row.prize_type)?.label }}
  35. </template>
  36. </el-table-column>
  37. <el-table-column
  38. prop="prize_name"
  39. label="物品名称"
  40. align="center"
  41. header-align="center"
  42. />
  43. <el-table-column
  44. prop="stick_type"
  45. label="福签"
  46. align="center"
  47. header-align="center"
  48. >
  49. <template #default="scope">
  50. {{ typeOptions.find(item => item.value === scope.row.stick_type)?.label }}
  51. </template>
  52. </el-table-column>
  53. <el-table-column
  54. prop="probability"
  55. label="概率"
  56. align="center"
  57. header-align="center"
  58. >
  59. <template #default="scope"> {{ scope.row.probability }}% </template>
  60. </el-table-column>
  61. <el-table-column label="状态" prop="status" align="center" header-align="center">
  62. <template #default="scope">
  63. <el-switch
  64. v-model="scope.row.status"
  65. :active-value="1"
  66. :inactive-value="0"
  67. inline-prompt
  68. style="
  69. --el-switch-on-color: #13ce66;
  70. --el-switch-off-color: #ff4949;
  71. "
  72. active-text="ON"
  73. inactive-text="OFF"
  74. :before-change="() => beforeChangeState(scope.row)"
  75. >
  76. </el-switch>
  77. </template>
  78. </el-table-column>
  79. <el-table-column
  80. prop="time"
  81. label="时间"
  82. align="center"
  83. header-align="center"
  84. />
  85. <el-table-column label="操作" align="center" header-align="center">
  86. <template #default="scope">
  87. <el-button type="text" @click="deleteDraw(scope.row)">删除</el-button>
  88. <el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
  89. </template>
  90. </el-table-column>
  91. </el-table>
  92. <!-- 分页组件 -->
  93. <div class="demo-pagination-block">
  94. <el-pagination
  95. @size-change="handleSizeChange"
  96. @current-change="handleCurrentChange"
  97. :current-page="currentPage"
  98. :page-sizes="[10, 20, 50, 100]"
  99. :page-size="pageSize"
  100. layout="total, sizes, prev, pager, next, jumper"
  101. :total="datatotal"
  102. />
  103. </div>
  104. <el-dialog v-model="dialogFormVisible" width="500" :show-close="false">
  105. <el-form
  106. :model="form"
  107. style="width: 400px; margin: 0 auto"
  108. :rules="rules"
  109. ref="formRef"
  110. >
  111. <el-form-item label="类型" prop="prize_type">
  112. <el-select v-model="form.prize_type" placeholder="请选择类型" clearable>
  113. <el-option
  114. v-for="item in prizeTypeOptions"
  115. :key="item.value"
  116. :label="item.label"
  117. :value="item.value"
  118. />
  119. </el-select>
  120. </el-form-item>
  121. <el-form-item :label="nameConfig.label" :prop="nameConfig.prop">
  122. <el-select
  123. v-if="form.prize_type === 5"
  124. v-model="form.templateName"
  125. placeholder="请选择模板"
  126. clearable
  127. :loading="isRegionLoading"
  128. >
  129. <el-option
  130. v-for="item in templateList"
  131. :key="item.id"
  132. :label="item.name"
  133. :value="item.id"
  134. />
  135. </el-select>
  136. <el-input
  137. v-else
  138. v-model="form[nameConfig.prop]"
  139. :type="nameConfig.type"
  140. autocomplete="off"
  141. :placeholder="nameConfig.placeholder"
  142. clearable
  143. />
  144. </el-form-item>
  145. <el-form-item v-if="form.prize_type === 5" label="期限" prop="term_value">
  146. <el-input
  147. v-model.number="form.term_value"
  148. placeholder="请输入模板期限"
  149. clearable
  150. >
  151. <template #append>
  152. <el-select
  153. v-model="form.time_unit"
  154. placeholder="单位"
  155. style="width: 60px"
  156. >
  157. <el-option label="日" :value=3 />
  158. <el-option label="月" :value=2 />
  159. <el-option label="年" :value=1 />
  160. </el-select>
  161. </template>
  162. </el-input>
  163. </el-form-item>
  164. <el-form-item label="概率" prop="probability">
  165. <el-input
  166. v-model.number="form.probability"
  167. type="number"
  168. autocomplete="off"
  169. placeholder="请输入概率"
  170. clearable
  171. >
  172. <template #append><span style="width: 20px;">%</span></template>
  173. </el-input>
  174. <div class="tip">(小于等于100%)</div>
  175. </el-form-item>
  176. <el-form-item label="福签" prop="stick_type">
  177. <el-select
  178. v-model="form.stick_type"
  179. placeholder="请选择类型"
  180. clearable
  181. >
  182. <el-option
  183. v-for="item in typeOptions"
  184. :key="item.value"
  185. :label="item.label"
  186. :value="item.value"
  187. />
  188. </el-select>
  189. </el-form-item>
  190. <el-form-item label="图片" prop="img">
  191. <el-upload
  192. ref="uploadRef"
  193. v-model:file-list="fileList"
  194. class="avatar-uploader"
  195. :action="uploadUrl"
  196. :limit="1"
  197. list-type="picture-card"
  198. :on-success="handleSuccess"
  199. :before-upload="beforeUpload"
  200. :on-remove="handleRemove"
  201. :on-exceed="handleExceed"
  202. >
  203. <el-icon><Plus /></el-icon>
  204. </el-upload>
  205. <div class="tip">
  206. 大小180*180像素支持PNGJPG格式图片需小于100K
  207. </div>
  208. </el-form-item>
  209. <el-form-item label="必中用户" prop="dccodes">
  210. <el-input
  211. v-model="form.dccodes"
  212. type="textarea"
  213. :rows="4"
  214. placeholder="请输入精网号或以Excel的格式粘贴精网号&#10;示例:&#10;90042088&#10;90023488&#10;90046788"
  215. clearable
  216. />
  217. </el-form-item>
  218. </el-form>
  219. <template #footer>
  220. <div class="dialog-footer">
  221. <el-button @click="dialogFormVisible = false">取消</el-button>
  222. <el-button type="danger" @click="submitForm(formRef)">
  223. 提交
  224. </el-button>
  225. </div>
  226. </template>
  227. </el-dialog>
  228. </div>
  229. </template>
  230. <script setup>
  231. import { ref, reactive, onMounted, computed, watch, nextTick } from "vue";
  232. import { ElMessage, genFileId, ElMessageBox } from "element-plus";
  233. import router from "../../router";
  234. import {
  235. getContentListApi,
  236. addDrawConfigApi,
  237. deleteDrawApi,
  238. changeStatusApi,
  239. getTemplateApi,
  240. } from "../../api/eventManagement";
  241. const uploadUrl = import.meta.env.VITE_API_BASE_URLXXCG + "hljw/api/aws/upload";
  242. const uploadRef = ref();
  243. const fileList = ref([]);
  244. const formRef = ref();
  245. const isEdit = ref(false); // 是否为编辑状态
  246. const form = reactive({
  247. id: undefined,
  248. stick_type: "",
  249. prize_type: "",
  250. item_name: "",
  251. num: null,
  252. probability: null,
  253. img: "",
  254. templateName: "",
  255. term_value: null,
  256. time_unit: 3,
  257. dccodes: "",
  258. });
  259. const isRegionLoading = ref(false);
  260. const templateList = ref([]);
  261. const typeOptions = ref([
  262. { label: "好运签", value: 1 },
  263. { label: "福气签", value: 2 },
  264. { label: "富贵签", value: 3 },
  265. { label: "财神签", value: 4 },
  266. { label: "上上签", value: 5 },
  267. { label: "锦鲤签", value: 6 },
  268. ]);
  269. const prizeTypeOptions = ref([
  270. { label: "金币", value: 2 },
  271. { label: "金豆", value: 3 },
  272. { label: "Token", value: 1 },
  273. { label: "实物", value: 4 },
  274. { label: "模板", value: 5 },
  275. ]);
  276. const nameConfig = computed(() => {
  277. switch (form.prize_type) {
  278. case 1: // Token
  279. return { label: "数量", placeholder: "请输入Token数量", prop: "num", type: "number" };
  280. case 2: // 金币
  281. return { label: "数量", placeholder: "请输入金币数量", prop: "num", type: "number" };
  282. case 3: // 金豆
  283. return { label: "数量", placeholder: "请输入金豆数量", prop: "num", type: "number" };
  284. case 5: // 模板
  285. return { label: "名称", placeholder: "请输入模板名称", prop: "templateName", type: "text" };
  286. default: // 默认情况(未选择或实物)
  287. return { label: "名称", placeholder: "请输入物品名称", prop: "item_name", type: "text" };
  288. }
  289. });
  290. const handleSuccess = (response, uploadFile) => {
  291. form.img = response.data.url;
  292. };
  293. const beforeUpload = (rawFile) => {
  294. if (!rawFile.type.startsWith("image/")) {
  295. ElMessage.error("请上传图片文件!");
  296. return false;
  297. } else if (rawFile.size / 1024 > 100) {
  298. ElMessage.error("图片大小必须小于100K!");
  299. return false;
  300. }
  301. return true;
  302. };
  303. const handleRemove = (file, fileList) => {
  304. form.img = "";
  305. };
  306. const handleExceed = (files) => {
  307. // 1. 清空当前文件列表
  308. uploadRef.value.clearFiles();
  309. const file = files[0];
  310. // 2. 必须生成新的 uid,否则可能导致 key 冲突无法上传
  311. file.uid = genFileId();
  312. // 3. 手动选择文件
  313. uploadRef.value.handleStart(file);
  314. // 4. 手动触发上传
  315. uploadRef.value.submit();
  316. };
  317. // token
  318. const token = localStorage.getItem("token");
  319. const dialogFormVisible = ref(false);
  320. const rules = computed(() => {
  321. const baseRules = {
  322. stick_type: [{ required: true, message: "请选择类型", trigger: "change" }],
  323. prize_type: [{ required: true, message: "请选择类型", trigger: "change" }],
  324. probability: [
  325. { required: true, message: "请输入概率", trigger: "blur" },
  326. // 为负数时提示
  327. { validator: validateNum, trigger: "blur" },
  328. ],
  329. img: [{ required: true, message: "请上传图片", trigger: "change" }], // 上传通常用 change
  330. };
  331. if ([1, 2, 3].includes(form.prize_type)) {
  332. return {
  333. ...baseRules,
  334. num: [
  335. { required: true, message: "请输入数量", trigger: "blur" },
  336. // 为负数时提示
  337. { validator: validateNum, trigger: "blur" },
  338. ],
  339. };
  340. } else if (form.prize_type === 5) {
  341. return {
  342. ...baseRules,
  343. templateName: [
  344. { required: true, message: "请选择模板", trigger: "change" },
  345. ],
  346. term_value: [
  347. { required: true, message: "请输入期限", trigger: "blur" },
  348. // 非正整数时提示
  349. { validator: validateTerm, trigger: "blur" },
  350. ],
  351. };
  352. } else {
  353. return {
  354. ...baseRules,
  355. item_name: [
  356. { required: true, message: "请输入物品名称", trigger: "blur" },
  357. ],
  358. };
  359. }
  360. });
  361. watch(
  362. () => form.prize_type,
  363. (newVal) => {
  364. if (isEdit.value || !newVal) {
  365. return;
  366. }
  367. // 切换类型时,清除之前的输入值,避免校验报错或数据混乱
  368. form.item_name = "";
  369. form.templateName = "";
  370. form.num = null;
  371. form.term_value = null;
  372. form.time_unit = 3;
  373. // 也可以清除校验状态
  374. if (formRef.value) {
  375. formRef.value.clearValidate(["item_name", "templateName", "num"]);
  376. }
  377. }
  378. );
  379. const validateNum = (rule, value, callback) => {
  380. // 如果值为空,交给 required 规则处理
  381. if (value === "" || value === null || value === undefined) {
  382. callback();
  383. return;
  384. }
  385. // 转换为数字进行判断
  386. if (Number(value) < 0) {
  387. callback(new Error("不能为负数"));
  388. } else {
  389. callback();
  390. }
  391. };
  392. const validateTerm = (rule, value, callback) => {
  393. // 如果值为空,交给 required 规则处理
  394. if (value === "" || value === null || value === undefined) {
  395. callback();
  396. return;
  397. }
  398. // 转换为数字进行判断
  399. if (!Number.isInteger(Number(value)) || Number(value) <= 0) {
  400. return callback(new Error("必须为正整数"));
  401. } else {
  402. callback();
  403. }
  404. };
  405. // 表格数据
  406. const tableData = ref([]);
  407. const tableLoading = ref(false);
  408. const datatotal = ref(0);
  409. // 分页参数
  410. const currentPage = ref(1);
  411. const pageSize = ref(10);
  412. const beforeChangeState = (row) => {
  413. return new Promise(async (resolve, reject) => {
  414. try {
  415. const targetStatus = row.status === 1 ? 0 : 1;
  416. await changeStatusApi({
  417. token: token,
  418. id: row.id,
  419. status: targetStatus,
  420. });
  421. ElMessage.success("状态更新成功");
  422. resolve(true);
  423. // fetchTableData();
  424. } catch (error) {
  425. // reject()拒绝操作,switch 组件会自动回滚状态
  426. reject(new Error("状态更新失败"));
  427. }
  428. });
  429. };
  430. // 重置表单数据
  431. const resetForm = () => {
  432. form.id = undefined;
  433. form.stick_type = "";
  434. form.prize_type = "";
  435. form.item_name = "";
  436. form.templateName = "";
  437. form.term_value = null;
  438. form.time_unit = 3;
  439. form.num = null;
  440. form.probability = null;
  441. form.img = "";
  442. form.dccodes = "";
  443. fileList.value = [];
  444. };
  445. // 添加按钮
  446. const add = () => {
  447. isEdit.value = false;
  448. resetForm();
  449. dialogFormVisible.value = true;
  450. // 清除校验红字
  451. /* nextTick dialogFormVisible = true <el-dialog>
  452. 但由于弹窗可能有动画或者内部组件是懒加载的 nextTick 执行的那一瞬间<el-form> 可能还没有真正渲染到 DOM
  453. 此时 formRef.value undefined所以 clearValidate() 其实根本没有执行成功*/
  454. // nextTick(() => {
  455. // formRef.value?.clearValidate();
  456. // });
  457. //setTimeout 会将任务推到宏任务队列,确保在 DOM 渲染、弹窗打开动画开始之后再执行,保证 formRef 一定存在。
  458. setTimeout(() => {
  459. if (formRef.value) {
  460. formRef.value.clearValidate();
  461. }
  462. }, 0);
  463. };
  464. const submitForm = async () => {
  465. try {
  466. await formRef.value.validate();
  467. const requestParams = {
  468. token: token,
  469. stick_type: form.stick_type,
  470. prize_type: form.prize_type,
  471. probability: form.probability,
  472. img: form.img,
  473. dccodes: form.dccodes,
  474. };
  475. if (form.id) {
  476. requestParams.id = form.id;
  477. }
  478. if ([1, 2, 3].includes(form.prize_type)) {
  479. requestParams.num = Number(form.num);
  480. } else if (form.prize_type === 5) {
  481. requestParams.indicator_id = form.templateName;
  482. requestParams.num = Number(form.term_value);
  483. requestParams.time_unit = form.time_unit;
  484. const selectedItem = templateList.value.find(item => item.id === form.templateName);
  485. requestParams.item_name = selectedItem ? selectedItem.name : '';
  486. } else {
  487. requestParams.item_name = form.item_name;
  488. }
  489. const data = await addDrawConfigApi(requestParams);
  490. ElMessage.success(form.id ? "修改成功" : "添加成功");
  491. dialogFormVisible.value = false;
  492. fetchTableData();
  493. } catch (error) {}
  494. };
  495. const deleteDraw = async (row) => {
  496. try {
  497. await ElMessageBox.confirm("确定要删除吗?", "确认删除", {
  498. confirmButtonText: "确定",
  499. cancelButtonText: "取消",
  500. type: "warning",
  501. confirmButtonClass: "custom-confirm-btn",
  502. });
  503. const requestParams = {
  504. token: token,
  505. id: row.id,
  506. };
  507. const data = await deleteDrawApi(requestParams);
  508. ElMessage.success("删除成功");
  509. fetchTableData();
  510. } catch (error) {
  511. if (error === "cancel") {
  512. // 用户点击取消
  513. ElMessage.info("已取消删除");
  514. } else {
  515. // 删除操作失败
  516. ElMessage.error("删除失败");
  517. }
  518. }
  519. };
  520. const handleEdit = (row) => {
  521. isEdit.value = true;
  522. resetForm();
  523. form.id = row.id;
  524. form.stick_type = row.stick_type;
  525. form.prize_type = row.prize_type; // 触发 watch,可能会清空字段,所以需要在nextTick后赋值
  526. form.probability = row.probability;
  527. form.img = row.img;
  528. form.dccodes = row.dccodes || "";
  529. // 图片回显 (让 Upload 组件显示图片)
  530. if (row.img) {
  531. fileList.value = [{ name: 'img', url: row.img }];
  532. }
  533. // 根据类型回显不同字段
  534. if ([1, 2, 3].includes(row.prize_type)) {
  535. form.num = row.num;
  536. } else if (row.prize_type === 5) {
  537. // 模板类型:回显模板ID和期限
  538. form.templateName = row.indicator_id;
  539. form.term_value = row.num;
  540. form.time_unit = row.time_unit;
  541. } else {
  542. // 实物类型
  543. form.item_name = row.item_name;
  544. }
  545. dialogFormVisible.value = true;
  546. // 6. 在 DOM 更新后,解除锁并清除校验
  547. nextTick(() => {
  548. isEdit.value = false; // 解除锁
  549. if (formRef.value) {
  550. formRef.value.clearValidate(); // 去掉一打开就红一片的校验信息
  551. }
  552. });
  553. };
  554. // 获取模板列表
  555. const fetchTemplateList = async () => {
  556. try {
  557. isRegionLoading.value = true;
  558. const data = await getTemplateApi({
  559. token: token,
  560. app_form: "en",
  561. });
  562. templateList.value = data.list;
  563. } catch (error) {
  564. console.error("获取模板列表失败:", error);
  565. templateList.value = [];
  566. } finally {
  567. isRegionLoading.value = false;
  568. }
  569. };
  570. // 获取表格数据
  571. const fetchTableData = async () => {
  572. try {
  573. tableLoading.value = true;
  574. const requestParams = {
  575. token: token,
  576. page: currentPage.value,
  577. page_size: pageSize.value,
  578. };
  579. const data = await getContentListApi(requestParams);
  580. tableData.value = data.list;
  581. datatotal.value = data.total;
  582. } catch (error) {
  583. console.error("获取表格数据失败:", error);
  584. tableData.value = [];
  585. datatotal.value = 0;
  586. } finally {
  587. tableLoading.value = false;
  588. }
  589. };
  590. // 组件挂载时:获取地区列表 + 初始化表格数据
  591. onMounted(() => {
  592. fetchTemplateList();
  593. fetchTableData();
  594. });
  595. // 分页方法
  596. const handleSizeChange = (val) => {
  597. pageSize.value = val;
  598. fetchTableData();
  599. console.log(`每页 ${val}`);
  600. };
  601. const handleCurrentChange = (val) => {
  602. currentPage.value = val;
  603. fetchTableData();
  604. console.log(`当前页: ${val}`);
  605. };
  606. </script>
  607. <style scoped>
  608. /* 父容器 */
  609. .page-container {
  610. position: relative;
  611. min-height: 600px;
  612. }
  613. /* 搜索区域 */
  614. .search-container {
  615. display: flex;
  616. height: auto;
  617. flex-direction: column;
  618. justify-content: center;
  619. align-items: flex-start;
  620. gap: 12px;
  621. align-self: stretch;
  622. border-radius: 8px;
  623. background: #fefaf9;
  624. box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.25);
  625. padding: 15px;
  626. margin-bottom: 20px;
  627. }
  628. /* 表格样式 */
  629. .table-rounded {
  630. border-radius: 12px !important;
  631. overflow: hidden !important;
  632. border: 1px solid #e4e7ed !important;
  633. height: 750px;
  634. }
  635. .table-header {
  636. text-align: center !important;
  637. font-weight: 800 !important;
  638. font-size: 15px !important;
  639. color: #333 !important;
  640. background-color: #f8f9fa !important;
  641. }
  642. .el-table__cell {
  643. border-right: none !important;
  644. border-bottom: 1px solid #e4e7ed !important;
  645. }
  646. .el-table__header th.el-table__cell {
  647. border-right: none !important;
  648. border-bottom: 1px solid #e4e7ed !important;
  649. }
  650. .el-table__row:hover .el-table__cell {
  651. background-color: #fafafa !important;
  652. }
  653. /* 分页组件样式 */
  654. .demo-pagination-block {
  655. display: flex;
  656. width: 100%;
  657. height: 44px;
  658. padding: 0 16px;
  659. align-items: center;
  660. gap: 16px;
  661. position: absolute;
  662. margin-top: 10px;
  663. border-radius: 0 0 3px 3px;
  664. border-top: 1px solid #eaeaea;
  665. background: #fefbfb;
  666. box-sizing: border-box;
  667. }
  668. .tip {
  669. font-size: 12px;
  670. color: #8c939d;
  671. }
  672. .avatar-uploader .avatar {
  673. width: 120px;
  674. height: 120px;
  675. display: block;
  676. }
  677. </style>
  678. <style>
  679. .custom-confirm-btn {
  680. background: #e13d52;
  681. border-color: #e13d52;
  682. color: white !important;
  683. border-radius: 6px !important;
  684. padding: 8px 16px !important;
  685. }
  686. .custom-confirm-btn:hover {
  687. background: #d88b95;
  688. border-color: #d88b95;
  689. }
  690. .avatar-uploader .el-upload {
  691. border: 1px dashed var(--el-border-color);
  692. border-radius: 6px;
  693. cursor: pointer;
  694. position: relative;
  695. overflow: hidden;
  696. transition: var(--el-transition-duration-fast);
  697. }
  698. .avatar-uploader .el-upload:hover {
  699. border-color: var(--el-color-primary);
  700. }
  701. .el-icon.avatar-uploader-icon {
  702. font-size: 28px;
  703. color: #8c939d;
  704. width: 120px;
  705. height: 120px;
  706. text-align: center;
  707. }
  708. </style>