This commit is contained in:
2025-05-10 15:59:02 +08:00
parent d6b32af2b2
commit 396fd4b934
21 changed files with 662 additions and 83 deletions

View File

@@ -15,6 +15,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"plyr": "^3.7.8",
"tinymce": "^7.8.0", "tinymce": "^7.8.0",
"uqrcodejs": "^4.0.7", "uqrcodejs": "^4.0.7",
"uuid": "^11.1.0", "uuid": "^11.1.0",

34
pnpm-lock.yaml generated
View File

@@ -23,6 +23,9 @@ importers:
pinia-plugin-persistedstate: pinia-plugin-persistedstate:
specifier: ^4.2.0 specifier: ^4.2.0
version: 4.2.0(pinia@3.0.2(vue@3.5.13)) version: 4.2.0(pinia@3.0.2(vue@3.5.13))
plyr:
specifier: ^3.7.8
version: 3.7.8
tinymce: tinymce:
specifier: ^7.8.0 specifier: ^7.8.0
version: 7.8.0 version: 7.8.0
@@ -1412,6 +1415,9 @@ packages:
csstype@3.1.3: csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
custom-event-polyfill@1.0.7:
resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==}
dayjs@1.11.13: dayjs@1.11.13:
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
@@ -1895,6 +1901,9 @@ packages:
lines-and-columns@1.2.4: lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
loadjs@4.3.0:
resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==}
local-pkg@1.1.1: local-pkg@1.1.1:
resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -2121,6 +2130,9 @@ packages:
pkg-types@2.1.0: pkg-types@2.1.0:
resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==} resolution: {integrity: sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==}
plyr@3.7.8:
resolution: {integrity: sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==}
possible-typed-array-names@1.1.0: possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2187,6 +2199,9 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
rangetouch@2.0.1:
resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==}
rc9@2.1.2: rc9@2.1.2:
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
@@ -2519,6 +2534,9 @@ packages:
uqrcodejs@4.0.7: uqrcodejs@4.0.7:
resolution: {integrity: sha512-84+aZmD2godCVI+93lxE3YUAPNY8zAJvNA7xRS7R7U+q57KzMDepBSfNCwoRUhWOfR6eHFoAOcHRPwsP6ka1cA==} resolution: {integrity: sha512-84+aZmD2godCVI+93lxE3YUAPNY8zAJvNA7xRS7R7U+q57KzMDepBSfNCwoRUhWOfR6eHFoAOcHRPwsP6ka1cA==}
url-polyfill@1.1.13:
resolution: {integrity: sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ==}
util-deprecate@1.0.2: util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -4095,6 +4113,8 @@ snapshots:
csstype@3.1.3: {} csstype@3.1.3: {}
custom-event-polyfill@1.0.7: {}
dayjs@1.11.13: {} dayjs@1.11.13: {}
debug@4.4.0: debug@4.4.0:
@@ -4560,6 +4580,8 @@ snapshots:
lines-and-columns@1.2.4: {} lines-and-columns@1.2.4: {}
loadjs@4.3.0: {}
local-pkg@1.1.1: local-pkg@1.1.1:
dependencies: dependencies:
mlly: 1.7.4 mlly: 1.7.4
@@ -4764,6 +4786,14 @@ snapshots:
exsolve: 1.0.5 exsolve: 1.0.5
pathe: 2.0.3 pathe: 2.0.3
plyr@3.7.8:
dependencies:
core-js: 3.42.0
custom-event-polyfill: 1.0.7
loadjs: 4.3.0
rangetouch: 2.0.1
url-polyfill: 1.1.13
possible-typed-array-names@1.1.0: {} possible-typed-array-names@1.1.0: {}
postcss-import@15.1.0(postcss@8.5.3): postcss-import@15.1.0(postcss@8.5.3):
@@ -4817,6 +4847,8 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
rangetouch@2.0.1: {}
rc9@2.1.2: rc9@2.1.2:
dependencies: dependencies:
defu: 6.1.4 defu: 6.1.4
@@ -5199,6 +5231,8 @@ snapshots:
uqrcodejs@4.0.7: {} uqrcodejs@4.0.7: {}
url-polyfill@1.1.13: {}
util-deprecate@1.0.2: {} util-deprecate@1.0.2: {}
util@0.12.5: util@0.12.5:

View File

@@ -232,6 +232,69 @@ const merchant = {
data: {id} data: {id}
}); });
}, },
addChildrenComment: async (data) => {
return request({
url: '/index/task/addChildrenComment',
method: Method.POST,
data: data
});
},
getChildrenComment: async (id) => {
return request({
url: '/index/task/getChildrenComment',
method: Method.POST,
data: {id}
});
},
delChildrenComment: async (id) => {
return request({
url: '/index/task/delChildrenComment',
method: Method.POST,
data: {id}
});
},
editChildrenMaterial: async (data) => {
return request({
url: '/index/task/editChildrenMaterial',
method: Method.POST,
data: data
});
},
editChildrenMaterimal: async (data) => {
return request({
url: '/index/task/editChildrenMaterimal',
method: Method.POST,
data: data
});
},
startTask: async (data) => {
return request({
url: '/index/task/startTask',
method: Method.POST,
data: data
});
},
moneyToTask: async (id) => {
return request({
url: '/index/task/moneyToTask',
method: Method.POST,
data: {id}
});
},
stopTask: async (id) => {
return request({
url: '/index/task/stopTask',
method: Method.POST,
data: {id}
});
},
endTask: async (id) => {
return request({
url: '/index/task/endTask',
method: Method.POST,
data: {id}
});
}
} }
export default merchant; export default merchant;

BIN
src/assets/images/back.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

View File

@@ -0,0 +1,46 @@
<script setup>
import Api from "../../api/index.js";
import {Message} from "@arco-design/web-vue";
const emits = defineEmits(['success']);
const {data, hideDelete} = defineProps({
data: {
type: Object,
default: {}
},
hideDelete: {
type: Boolean,
default: false,
}
});
const del = async (id) => {
const {msg} = await Api.merchant.delChildrenComment(id);
Message.success(msg);
emits('success');
}
</script>
<template>
<div v-for="(item, index) in data" :key="item.id" class="flex items-center">
<div class="mr-[5px] min-w-[15px]">{{ index + 1 }}.</div>
<div class="box">
<div class="flex items-center whitespace-nowrap" v-if="item.pid>0">
<img class="size-[9px] mr-[5px]" src="../../assets/images/back.png" alt=""/>
回复{{ data.findIndex(k => k.pid === item.pid) }}:
</div>
<img class="size-[32px]" v-if="item.image" :src="item.image" alt=""/>
<div class="whitespace-nowrap">{{ item.intro }}</div>
</div>
<a-popconfirm content="确认删除吗?" @ok="del(item.id)" v-if="!hideDelete">
<a-button size="mini" shape="circle" type="text">
<icon-delete/>
</a-button>
</a-popconfirm>
</div>
</template>
<style scoped lang="scss">
.box {
@apply p-[4px] bg-[#F2F3F5] flex items-center gap-[10px] pr-[15px] mr-[10px] min-h-[40px] flex-grow;
}
</style>

View File

@@ -16,11 +16,11 @@ const UserStore = useUserStore();
<a-dropdown> <a-dropdown>
<a-link <a-link
:hoverable="false"> :hoverable="false">
{{ UserStore.userInfo.nickname || UserStore.userInfo.mobile }} {{ UserStore?.userInfo?.nickname || UserStore?.userInfo?.mobile }}
<icon-down/> <icon-down/>
</a-link> </a-link>
<template #content> <template #content>
<a-doption @click="UserStore.logout">退出登陆</a-doption> <a-doption @click="UserStore?.logout">退出登陆</a-doption>
</template> </template>
</a-dropdown> </a-dropdown>
</div> </div>

View File

@@ -1,7 +1,7 @@
<script setup> <script setup>
import {reactive, ref} from "vue"; import {reactive, ref} from "vue";
import {Message} from "@arco-design/web-vue"; import {Message} from "@arco-design/web-vue";
import Api from "../../../api/index.js"; import Api from "../../api/index.ts";
const {money} = defineProps({ const {money} = defineProps({
money: { money: {

View File

@@ -0,0 +1,89 @@
<script setup>
import {ref} from "vue";
const payInfo = ref(null);
const visible = ref(false);
const emits = defineEmits(['close']);
let successFun = () => {
};
const open = (options) => {
const {props, success} = options;
successFun = success;
console.log('?????', options)
payInfo.value = props.payInfo;
visible.value = true;
}
const close = () => {
visible.value = false;
}
const success = () => {
successFun && successFun.apply();
}
defineExpose({
open,
close
});
</script>
<template>
<a-modal
@ok="success"
ok-text="确认支付"
:ok-button-props="{
disabled: payInfo?.user_money < payInfo?.total_money
}"
:width="600"
id="PayTask-Modal"
title-align="start"
title="开始任务"
v-model:visible="visible">
<a-alert>平台提示该款项不会直接打给达人只有您对子任务确认结算后才会打款给达人</a-alert>
<div class="px-[30px] py-[16px] flex flex-col gap-[8px] justify-start items-start" v-if="payInfo">
<div class="text-[#4E5969]">支付详情</div>
<div class="flex">
<div class="w-[100px]">任务金额(</div>
<span class="text-[#4E5969]">{{ (payInfo.total_money - payInfo.serve_money).toFixed(2) }}</span>
</div>
<div class="flex">
<div class="w-[100px]">服务费(</div>
<span class="text-[#4E5969]">{{ payInfo.serve_money.toFixed(2) }}</span>
</div>
<div class="flex">
<div class="w-[100px]">合计(</div>
<span class="text-[#4E5969]">{{ payInfo.total_money.toFixed(2) }}</span>
</div>
<div class="text-[12px] text-[#86909C]">
提示①服务费按照实际消耗任务金额的百分比进行收取<br/>
②若任务未被领取或者未支付全部金额则只收取实际支付部分的服务费<br/>
③剩余任务金额和服务费将退回钱包
</div>
<div>支付方式</div>
<div class="mt-[5px]">
<a-radio
:disabled="payInfo.user_money < payInfo?.total_money"
:default-checked="payInfo.user_money > payInfo?.total_money">
钱包余额
(可用¥{{ payInfo?.user_money?.toFixed(2) }})
</a-radio>
</div>
<div class="text-[12px] text-[#86909C] pl-[20px]">
*余额不足本次任务所需,请充值后再进行支付
</div>
<a-button class="mt-[20px]" type="primary">去充值</a-button>
</div>
</a-modal>
</template>
<style lang="scss">
#PayTask-Modal {
.arco-modal-body {
padding: 0;
}
}
</style>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,30 @@
<script setup>
import {onMounted, ref} from "vue";
import Plyr from "plyr";
import 'plyr/dist/plyr.css';
const {src} = defineProps({
src: {
type: String,
default: "",
}
});
const plyrContainer = ref();
onMounted(() => {
new Plyr(plyrContainer.value, {});
});
</script>
<template>
<div class="plyrContainer">
<video ref="plyrContainer">
<source :src="src">
</video>
</div>
</template>
<style scoped lang="scss">
.plyrContainer {
width: 70%;
}
</style>

View File

@@ -1,7 +1,9 @@
<script setup> <script setup>
import XImage from "../../components/XImage/Index.vue";
import {reactive, ref, watch} from 'vue'; import {reactive, ref, watch} from 'vue';
import Api from "../../api/index.ts"; import Api from "../../api/index.ts";
import {Message} from "@arco-design/web-vue"; import {Message} from "@arco-design/web-vue";
import Comment from "../Comment/index.vue";
const emits = defineEmits(['success']); const emits = defineEmits(['success']);
const {id} = defineProps({ const {id} = defineProps({
@@ -12,7 +14,7 @@ const {id} = defineProps({
}); });
const visible = ref(false); const visible = ref(false);
const detail = reactive({}); const detail = reactive({});
const activeKey = ref(1); const activeKey = ref(0);
watch( watch(
() => visible.value, () => visible.value,
@@ -28,12 +30,14 @@ watch(
const passTaskChildren = async () => { const passTaskChildren = async () => {
const {code, msg} = await Api.admin.passTaskChildren(id); const {code, msg} = await Api.admin.passTaskChildren(id);
if (code === 1) Message.success(msg); if (code === 1) Message.success(msg);
visible.value = false;
emits('success'); emits('success');
} }
const refuseTaskChildren = async () => { const refuseTaskChildren = async () => {
const {code, msg} = await Api.admin.refuseTaskChildren(id); const {code, msg} = await Api.admin.refuseTaskChildren(id);
if (code === 1) Message.success(msg); if (code === 1) Message.success(msg);
visible.value = false;
emits('success'); emits('success');
} }
</script> </script>
@@ -64,41 +68,37 @@ const refuseTaskChildren = async () => {
</a-textarea> </a-textarea>
</a-form-item> </a-form-item>
<a-form-item label="话题"> <a-form-item label="话题">
<div id="tag-list" class="w-full bg-[var(--color-neutral-2)] p-[4px]"> <div v-if="item.tags_arr.length > 0" id="tag-list"
class="w-full bg-[var(--color-neutral-2)] p-[4px]">
<a-tag v-for="v in item.tags_arr">#{{ v }}</a-tag> <a-tag v-for="v in item.tags_arr">#{{ v }}</a-tag>
</div> </div>
<div v-else>
暂无话题
</div>
</a-form-item> </a-form-item>
<a-form-item label="素材"> <a-form-item label="素材">
<div class="flex flex-wrap gap-[16px]"> <div v-if="item.materia_arr.length > 0" class="flex flex-wrap gap-[16px]">
<a-image <x-image
v-for="v in item.materia_arr" v-for="(v, index) in item.materia_arr"
:hide-delete="true"
:key="index"
width="60px" width="60px"
height="60px" height="60px"
:src="v"> :src="v">
</a-image> </x-image>
</div>
<div v-else>
暂无话题
</div> </div>
</a-form-item> </a-form-item>
<a-form-item label="评论区内容"> <a-form-item label="评论区内容">
<div class="flex flex-col gap-[8px] w-full"> <div v-if="item.comment.length > 0" class="flex flex-col gap-[8px] w-full">
<div class="w-full flex gap-[10px] items-center" v-for="v in detail.comment"> <comment :data="item.comment" :hide-delete="true"></comment>
{{ v.id }}. </div>
<div class="flex-grow bg-[var(--color-neutral-2)] p-[4px] h-[40px] flex items-center"> <div v-else>
<div class="text-[#4E5969] text-[14px] mr-[4px]" v-if="v.pid !== 0"> 暂无评论区内容
<icon-redo/>
回复{{ v.pid }}
</div>
<a-image
v-if="v.image"
class="mr-[12px]"
width="30px"
height="30px"
:src="v.image">
</a-image>
第三个很好用大家可以在网上搜同款
</div>
</div>
</div> </div>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -107,14 +107,18 @@ const refuseTaskChildren = async () => {
<template #footer> <template #footer>
<div class="flex items-center gap-[8px]"> <div class="flex items-center gap-[8px]">
<a-checkbox-group> <!-- <a-checkbox-group>-->
<template v-for="(item, index) in detail.content" :key="item"> <!-- <template v-for="(item, index) in detail.content" :key="item">-->
<a-checkbox v-show="activeKey === index" :value="item">选中</a-checkbox> <!-- <a-checkbox v-show="activeKey === index" :value="item">选中</a-checkbox>-->
</template> <!-- </template>-->
</a-checkbox-group> <!-- </a-checkbox-group>-->
<a-button @click="passTaskChildren" type="primary" class="ml-auto">通过</a-button> <template v-if="detail.check_status === 0">
<a-button @click="refuseTaskChildren">拒绝</a-button> <a-button @click="passTaskChildren" type="primary" class="ml-auto">通过
</a-button>
<a-button @click="refuseTaskChildren">拒绝</a-button>
</template>
<a-button v-else @click="visible=false" class="ml-auto">关闭</a-button>
</div> </div>
</template> </template>
</a-modal> </a-modal>

View File

@@ -1,9 +1,11 @@
<script setup> <script setup>
import {ref} from "vue"; import {ref} from "vue";
import {Modal} from "@arco-design/web-vue"; import {Modal} from "@arco-design/web-vue";
import {determineMediaType} from "../../utils/index.js";
import Plyr from "../Plyr/Plyr.vue";
const emits = defineEmits(['delete']); const emits = defineEmits(['delete']);
const {width, height, aspect} = defineProps({ const {width, height, aspect, hideDelete, src} = defineProps({
width: { width: {
type: String, type: String,
default: '80px' default: '80px'
@@ -15,6 +17,14 @@ const {width, height, aspect} = defineProps({
aspect: { aspect: {
type: Boolean, type: Boolean,
default: false, default: false,
},
hideDelete: {
type: Boolean,
default: false,
},
src: {
type: String,
default: "",
} }
}); });
const previewVisible = ref(false); const previewVisible = ref(false);
@@ -33,28 +43,61 @@ const deleteImage = () => {
<template> <template>
<div class="x-image-box" :style="{aspectRatio: aspect ? '1 / 1' : ''}"> <div class="x-image-box" :style="{aspectRatio: aspect ? '1 / 1' : ''}">
<a-image <a-image
v-if="determineMediaType(src) === 'Image'"
fit="contain" fit="contain"
v-model:preview-visible="previewVisible" v-model:preview-visible="previewVisible"
@preview-visible-change="(v)=>previewVisible=v" @preview-visible-change="(v)=>previewVisible=v"
v-bind="$attrs" v-bind="$attrs"
:src="src"
:width="width" :width="width"
:height="height"> :height="height">
</a-image> </a-image>
<video class="video" v-else-if="determineMediaType(src) === 'Video'" :src="src">
</video>
<transition name="fade">
<div class="video-preview-mask" v-if="determineMediaType(src) === 'Video' && previewVisible"
@click="previewVisible=false">
<plyr :src="src" @click.stop></plyr>
</div>
</transition>
<div class="mask"> <div class="mask">
<icon-eye @click="previewVisible=true" class="icon" style="color: #fff"/> <icon-eye @click="previewVisible=true" class="icon" style="color: #fff"/>
<icon-delete @click="deleteImage" class="icon" style="color: #fff"/> <icon-delete @click="deleteImage" v-if="!hideDelete" class="icon" style="color: #fff"/>
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s;
position: relative;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
position: absolute;
}
.x-image-box { .x-image-box {
@apply cursor-pointer relative box-border rounded-[6px] overflow-hidden; @apply cursor-pointer relative box-border rounded-[6px] overflow-hidden;
outline: 1px solid #E5E6EB; outline: 1px solid #E5E6EB;
width: v-bind(width); width: v-bind(width);
height: v-bind(height); height: v-bind(height);
.video {
width: v-bind(width);
height: v-bind(height);
object-fit: contain;
}
.video-preview-mask {
@apply size-full fixed z-[999] left-0 top-0 bg-[rgba(0,0,0,.6)] flex justify-center items-center;
}
&:hover .mask { &:hover .mask {
background-color: rgba(0, 0, 0, .4); background-color: rgba(0, 0, 0, .4);

View File

@@ -0,0 +1,44 @@
<script setup>
const {list, size} = defineProps({
list: {
type: Array,
default: []
},
size: {
type: String,
default: "40px"
}
});
</script>
<template>
<div class="x-image-small-list">
<a-image :width="size" :height="size" :src="list[0]" v-bind="$attrs"></a-image>
<div class="x-image-small-list-mask" v-if="list.length > 1">
+{{ list.length - 1 }}
</div>
</div>
</template>
<style scoped lang="scss">
.x-image-small-list {
width: v-bind(size);
height: v-bind(size);
overflow: hidden;
position: relative;
border-radius: 6px;
.x-image-small-list-mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
background-color: rgba(0, 0, 0, .3);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

16
src/hooks/usePayTask.js Normal file
View File

@@ -0,0 +1,16 @@
import {createApp} from "vue";
import PayTask from "../components/Pay/PayTask.vue";
export const usePayTask = () => {
const container = document.createElement('div');
document.body.appendChild(container);
const app = createApp(PayTask);
const vm = app.mount(container);
const {open} = vm;
return {
open
};
}

View File

@@ -3,20 +3,38 @@ import XSelect from "../../../components/XSelect/index.vue";
import {reactive, ref} from "vue"; import {reactive, ref} from "vue";
import UploadAvatar from "../../../components/upload/UploadAvatar.vue"; import UploadAvatar from "../../../components/upload/UploadAvatar.vue";
import Api from "../../../api/index.js"; import Api from "../../../api/index.js";
import {Message} from "@arco-design/web-vue";
const {id} = defineProps({ const emits = defineEmits(['success']);
id: { const {item, material} = defineProps({
type: Number, item: {
type: Object,
default: null,
},
material: {
type: Object,
default: null, default: null,
} }
}); });
const form = reactive({ const form = reactive({
type: 0, is_reply: 0,
files: [] pid: null,
files: [],
intro: null,
}); });
const visible = ref(false); const visible = ref(false);
const success = async () => { const success = async () => {
const {msg} = await Api.merchant.addChildrenComment({
id: material.id,
material_id: item.id,
pid: form.pid,
image: form.files,
intro: form.intro,
is_reply: form.is_reply,
});
Message.success(msg);
visible.value = false;
emits('success');
} }
</script> </script>
@@ -34,14 +52,18 @@ const success = async () => {
v-model:visible="visible" v-model:visible="visible"
@ok="success"> @ok="success">
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item label="是否回复之前评论"> <a-form-item label="是否回复之前评论" v-if="item.comment.length !== 0">
<a-radio-group v-model:model-value="form.type"> <a-radio-group v-model:model-value="form.is_reply">
<a-radio :value="0"></a-radio> <a-radio :value="0"></a-radio>
<a-radio :value="1"></a-radio> <a-radio :value="1"></a-radio>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item label="需要回复的评论" v-if="form.type===1"> <a-form-item label="需要回复的评论" v-if="form.is_reply===1">
<x-select api=""></x-select> <x-select
v-model:model-value="form.pid"
:apiPo="item.id"
:api="Api.merchant.getChildrenComment">
</x-select>
</a-form-item> </a-form-item>
<a-form-item label="添加图片"> <a-form-item label="添加图片">
<UploadAvatar <UploadAvatar
@@ -50,7 +72,7 @@ const success = async () => {
</UploadAvatar> </UploadAvatar>
</a-form-item> </a-form-item>
<a-form-item label="添加文字"> <a-form-item label="添加文字">
<a-textarea placeholder="请输入评论内容"></a-textarea> <a-textarea v-model:model-value="form.intro" placeholder="请输入评论内容"></a-textarea>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>

View File

@@ -1,18 +1,24 @@
<script setup> <script setup>
import {reactive, ref} from "vue"; import {reactive, ref, watch} from "vue";
import XImage from "../../../components/XImage/Index.vue"; import XImage from "../../../components/XImage/Index.vue";
import UploadButton from "../../../components/upload/UploadButton.vue"; import UploadButton from "../../../components/upload/UploadButton.vue";
import Api from "../../../api/index.js"; import Api from "../../../api/index.js";
import {v4} from "uuid"; import {v4} from "uuid";
import MaterialSource from "./MaterialSource.vue"; import MaterialSource from "./MaterialSource.vue";
import {Message} from "@arco-design/web-vue";
const MaterialSourceRef = ref(); const MaterialSourceRef = ref();
const visible = ref(false); const visible = ref(false);
const targetList = reactive([]); const targetList = reactive([]);
const {id} = defineProps({ const emits = defineEmits(['success']);
const {id, material} = defineProps({
id: { id: {
type: Number, type: Number,
default: null, default: null,
},
material: {
type: Object,
default: null,
} }
}); });
@@ -23,8 +29,33 @@ const uploadSuccess = (url) => {
}); });
} }
const success = async () => { watch(
() => visible.value,
(val) => {
if (val) {
MaterialSourceRef.value.getMaterialList();
targetList.length = 0;
material.material_arr.forEach(v => uploadSuccess(v));
}
}
)
const open = () => {
visible.value = true;
}
defineExpose({
open
});
const success = async () => {
const {msg} = await Api.merchant.editChildrenMaterial({
id: material.id,
image: targetList.map(v => v.image),
});
Message.success(msg);
emits('success');
MaterialSourceRef.value.handleImage();
} }
</script> </script>
@@ -44,7 +75,7 @@ const success = async () => {
v-model:visible="visible" v-model:visible="visible"
title="添加素材"> title="添加素材">
<div class="target"> <div class="target">
<div class="text-[#1D2129] text-[14px]">已添加5个素材长摁图片可拖动排序</div> <div class="text-[#1D2129] text-[14px]">已添加{{ targetList.length }}个素材长摁图片可拖动排序</div>
<div class="grid grid-cols-6 mt-[10px] gap-[16px]"> <div class="grid grid-cols-6 mt-[10px] gap-[16px]">
<x-image <x-image
@delete="targetList.splice(index,1)" @delete="targetList.splice(index,1)"

View File

@@ -12,11 +12,14 @@ const {id} = defineProps({
} }
}); });
const getMaterialList = async () => {
const {data} = await Api.merchant.getMaterialList(id);
list.length = 0;
list.push(...data);
}
onMounted(() => { onMounted(() => {
Api.merchant.getMaterialList(id).then(({data}) => { getMaterialList();
list.length = 0;
list.push(...data);
});
}); });
const select = (v) => { const select = (v) => {
const flag = selectList.value.findIndex(k => k.id === v.id); const flag = selectList.value.findIndex(k => k.id === v.id);
@@ -27,11 +30,18 @@ const select = (v) => {
selectList.value.push(v); selectList.value.push(v);
} }
} }
const handle = async () => { const handleImage = async () => {
if (type === 0) { if (type.value === 0) {
selectList.value.filter(v => typeof v.id === 'number').forEach(v => {
Api.merchant.delMaterial(v.id);
});
} }
} }
defineExpose({
getMaterialList,
handleImage
});
</script> </script>
<template> <template>

View File

@@ -0,0 +1,54 @@
<script setup>
import XImage from "../../../components/XImage/Index.vue";
import {ref} from "vue";
const visible = ref(false);
const emits = defineEmits(['edit']);
const {id, material} = defineProps({
id: {
type: Number,
default: null,
},
material: {
type: Object,
default: null,
}
});
const edit = () => {
visible.value = false;
emits('edit');
}
</script>
<template>
<a-button v-if="!$slots.default" type="primary" @click="visible=true">
添加素材
</a-button>
<div @click="visible=true" v-else class="cursor-pointer">
<slot></slot>
</div>
<a-modal
v-model:visible="visible"
title-align="start"
title="素材">
<div class="grid grid-cols-4">
<div v-for="(v, index) in material.material_arr" :key="index"
class="flex flex-col justify-center items-center gap-[10px]">
<x-image :src="v" :hide-delete="true"></x-image>
素材{{ index }}
</div>
</div>
<template #footer>
<div class="flex gap-[12px] justify-end">
<a-button @click="edit">编辑素材</a-button>
<a-button @click="visible=false" type="primary">确定</a-button>
</div>
</template>
</a-modal>
</template>
<style scoped lang="scss">
</style>

View File

@@ -1,6 +1,6 @@
<script setup> <script setup>
import {reactive} from "vue"; import {reactive} from "vue";
import Alipay from "../../../components/Alipay.vue"; import Alipay from "../../../../../components/Pay/Alipay.vue";
const form = reactive({ const form = reactive({
money: null, money: null,

View File

@@ -1,13 +1,18 @@
<script setup> <script setup>
import {reactive} from "vue"; import Comment from "../../../../components/Comment/index.vue";
import {reactive, ref} from "vue";
import useTableQuery from "../../../../hooks/useTableQuery.js"; import useTableQuery from "../../../../hooks/useTableQuery.js";
import Api from "../../../../api/index.js"; import Api from "../../../../api/index.js";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import MaterialLibrary from "../../components/MaterialLibrary.vue"; import MaterialLibrary from "../../components/MaterialLibrary.vue";
import AddMaterial from "../../components/AddMaterial.vue"; import AddMaterial from "../../components/AddMaterial.vue";
import AddComment from "../../components/AddComment.vue"; import AddComment from "../../components/AddComment.vue";
import XImageSmallList from "../../../../components/XImage/XImageSmallList.vue";
import ViewMaterial from "../../components/ViewMaterial.vue";
import {Message} from "@arco-design/web-vue";
const route = useRoute(); const route = useRoute();
const AddMaterialRef = ref();
const columns = reactive([ const columns = reactive([
{ {
title: '子任务编号', title: '子任务编号',
@@ -49,13 +54,31 @@ const vo = reactive({
pageSize: 9, pageSize: 9,
}); });
const {loading, pagination} = useTableQuery({ const {loading, pagination, fetchData} = useTableQuery({
parameter: po, parameter: po,
api: Api.merchant.getTaskChildrenList, api: Api.merchant.getTaskChildrenList,
callback: (data) => { callback: (data) => {
Object.assign(vo, data); Object.assign(vo, data);
} }
}); });
const editMaterial = (index) => {
AddMaterialRef.value[index].open();
}
const success = async () => {
const {msg} = await Api.merchant.editChildrenMaterimal({
id: vo.rows[0]?.task_id,
data: vo.rows.map(v => v.childrenMaterial.map(k => ({
id: k.id,
title: k.title,
content: k.content,
tags: k.tags,
}))).flat()
});
Message.success(msg);
await fetchData();
}
</script> </script>
<template> <template>
@@ -63,16 +86,17 @@ const {loading, pagination} = useTableQuery({
<a-card class="flex-grow text-[14px]"> <a-card class="flex-grow text-[14px]">
<div class="flex justify-between"> <div class="flex justify-between">
<MaterialLibrary :id="po.id"></MaterialLibrary> <MaterialLibrary :id="po.id"></MaterialLibrary>
<a-button type="primary">确认修改</a-button> <a-button type="primary" @click="success">确认修改</a-button>
</div> </div>
<a-table <a-table
:data="vo.rows" :data="vo.rows"
:loading="loading" :loading="loading"
:columns="columns" :columns="columns"
:scroll="{x: 'auto'}"
class="w-full mt-[20px] flex-grow" class="w-full mt-[20px] flex-grow"
@page-change="(e) => pagination.current = e" @page-change="(e) => pagination.current = e"
:pagination="pagination"> :pagination="false">
<template v-slot:title="{record}"> <template v-slot:title="{record}">
<div class="flex flex-col gap-[12px]"> <div class="flex flex-col gap-[12px]">
<a-input <a-input
@@ -100,28 +124,46 @@ const {loading, pagination} = useTableQuery({
</a-input> </a-input>
</div> </div>
</template> </template>
<template v-slot:material="{record}"> <template v-slot:material="{record, rowIndex}">
<div class="flex flex-col gap-[12px]"> <div class="flex flex-col gap-[12px]">
<add-material <div class="flex gap-[12px]" v-for="v in record.childrenMaterial">
v-for="v in record.childrenMaterial" <add-material
:id="po.id"> ref="AddMaterialRef"
<div class="add-materials"> @success="fetchData"
<icon-plus/> :id="po.id"
<div>添加</div> :material="v">
</div> <div class="add-materials">
</add-material> <icon-plus/>
<div>添加</div>
</div>
</add-material>
<view-material
@edit="editMaterial(rowIndex)"
:material="v"
:id="po.id">
<x-image-small-list
:preview="false"
v-if="v.material_arr.length>0"
:list="v.material_arr">
</x-image-small-list>
</view-material>
</div>
</div> </div>
</template> </template>
<template v-slot:pl="{record}"> <template v-slot:pl="{record}">
<div class="flex flex-col gap-[12px]"> <div class="flex flex-col gap-[12px]">
<add-comment <div v-for="(v, index) in record.childrenMaterial" :key="index" class="flex gap-[12px]">
v-for="v in record.childrenMaterial" <add-comment
:id="v.id"> @success="fetchData"
<div class="add-materials"> :material="record"
<icon-plus/> :item="v">
<div>添加</div> <div class="add-materials">
</div> <icon-plus/>
</add-comment> <div>添加</div>
</div>
</add-comment>
<comment @success="fetchData" :data="v.comment"></comment>
</div>
</div> </div>
</template> </template>
</a-table> </a-table>

View File

@@ -6,6 +6,7 @@ import useTableQuery from "../../../../hooks/useTableQuery.js";
import Api from "../../../../api/index.js"; import Api from "../../../../api/index.js";
import {toPath} from "../../../../utils/index.js"; import {toPath} from "../../../../utils/index.js";
import {Message} from "@arco-design/web-vue"; import {Message} from "@arco-design/web-vue";
import {usePayTask} from "../../../../hooks/usePayTask.js";
const columns = [ const columns = [
{ {
@@ -100,13 +101,44 @@ const po = reactive({
wd: null, wd: null,
}); });
const {loading, pagination, initFetchData} = useTableQuery({ const {loading, pagination, initFetchData, fetchData} = useTableQuery({
parameter: po, parameter: po,
api: Api.merchant.getTaskList, api: Api.merchant.getTaskList,
callback: (data) => { callback: (data) => {
Object.assign(vo, data); Object.assign(vo, data);
} }
}); });
const {open: openPayTask} = usePayTask();
const startTask = async (_value, record) => {
const {status, id} = record;
const showMessageAndFetch = async (msg) => {
Message.success(msg);
await fetchData();
};
if (status === -2 || status === 4) {
const {msg} = await Api.merchant.endTask(id);
await showMessageAndFetch(msg);
} else {
const {data} = await Api.merchant.startTask({id});
openPayTask({
props: {payInfo: data},
success: async () => {
const {msg} = await Api.merchant.moneyToTask(id);
await showMessageAndFetch(msg);
},
});
}
};
const stopTask = async (id) => {
const {msg} = await Api.merchant.stopTask(id);
Message.success(msg);
await fetchData();
}
</script> </script>
<template> <template>
@@ -150,7 +182,8 @@ const {loading, pagination, initFetchData} = useTableQuery({
</template> </template>
<template v-slot:start="{record}"> <template v-slot:start="{record}">
<a-switch <a-switch
:disabled="record.status !== -2 || record.status !== 3" @change="startTask($event, record)"
:disabled="record.status !== -2 && record.status !== 3 && record.status !== 4"
:model-value="record.status === 4"> :model-value="record.status === 4">
</a-switch> </a-switch>
</template> </template>
@@ -170,7 +203,9 @@ const {loading, pagination, initFetchData} = useTableQuery({
@click="record.status >= 2 ? toPath('/home/task-center/look-min-task', {id: record.id}) : Message.warning('审核未通过')"> @click="record.status >= 2 ? toPath('/home/task-center/look-min-task', {id: record.id}) : Message.warning('审核未通过')">
查看子任务 查看子任务
</a-link> </a-link>
<a-link :hoverable="false" status="danger">终止</a-link> <a-popconfirm content="确认终止吗?" @ok="stopTask(record.id)">
<a-link :hoverable="false" status="danger">终止</a-link>
</a-popconfirm>
</div> </div>
</template> </template>
<template v-slot:exp> <template v-slot:exp>

View File

@@ -21,3 +21,18 @@ export const baseImage = (url) => {
if (!url) url = ''; if (!url) url = '';
return url.startsWith('http') ? url : import.meta.env.VITE_API_URL + url; return url.startsWith('http') ? url : import.meta.env.VITE_API_URL + url;
} }
export const determineMediaType = (url) => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.svg'];
const videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
const extension = url.substring(url.lastIndexOf('.')).toLowerCase();
if (imageExtensions.includes(extension)) {
return 'Image';
} else if (videoExtensions.includes(extension)) {
return 'Video';
} else {
return 'Unknown';
}
}