This commit is contained in:
2025-03-10 19:01:21 +08:00
parent 94644d0601
commit 31a2667f8b
28 changed files with 1370 additions and 57 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
VITE_API_URL=http://127.0.0.1:4523

View File

@@ -9,8 +9,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"axios": "^1.8.2",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"pinia-plugin-persistedstate": "^4.2.0", "pinia-plugin-persistedstate": "^4.2.0",
"vite-plugin-vue-devtools": "^7.7.2",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },

762
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
<script setup></script> <script setup></script>
<template> <template>
<a-config-provider size="large"> <a-config-provider>
<router-view></router-view> <router-view></router-view>
</a-config-provider> </a-config-provider>
</template> </template>

7
src/api/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import system from './system.js';
const Api = {
system: {...system}
}
export default Api;

13
src/api/system.js Normal file
View File

@@ -0,0 +1,13 @@
import request from "../utils/request.js";
const system = {
getData: async (params) => {
return request({
url: '/m1/5995958-5684445-default/getList',
method: "POST",
data: params
});
}
}
export default system;

View File

@@ -0,0 +1,104 @@
<script setup>
import {defineModel} from 'vue';
const FROM_TYPE = {
INPUT: 'input',
SELECT: 'select',
DATETIME: 'datetime',
CUSTOM: 'custom',
}
const emits = defineEmits(['search']);
const {config} = defineProps({
config: {
type: Array,
default: []
}
});
const from = defineModel('from');
const reset = () => {
Object.keys(from.value).forEach(key => {
from.value[key] = null;
});
emits('search');
}
</script>
<template>
<div class="title">
查询任务
</div>
<div class="flex">
<div class="flex-grow">
<a-form class="AFORM">
<div class="grid grid-cols-3">
<template v-for="(item, index) in config" :key="index">
<a-form-item :label="item.label">
<template v-if="item.type === FROM_TYPE.INPUT">
<a-input
class="w-full"
v-model:model-value="from[item.key]"
:placeholder="item.placeholder">
</a-input>
</template>
<template v-if="item.type === FROM_TYPE.SELECT">
<a-select
class="w-full"
v-model:model-value="from[item.key]"
:options="item.options"
:placeholder="item.placeholder">
</a-select>
</template>
<template v-if="item.type === FROM_TYPE.DATETIME">
<a-range-picker
class="w-full"
@change="(v) => from[item.key] = `${v[0]}~${v[1]}`">
</a-range-picker>
</template>
<template v-if="item.type === FROM_TYPE.CUSTOM">
<slot :name="item.slotName" :scope="item"></slot>
</template>
</a-form-item>
</template>
</div>
</a-form>
</div>
<a-divider direction="vertical" margin="20px"></a-divider>
<div class="flex flex-col gap-[16px]">
<a-button @click="emits('search')" type="primary">
<template #icon>
<icon-search/>
</template>
查询
</a-button>
<a-button @click="reset">
<template #icon>
<icon-refresh />
</template>
重置
</a-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.title {
color: rgb(29, 33, 41);
font-family: PingFang SC, serif;
font-size: 20px;
font-weight: 400;
line-height: 28px;
text-align: left;
}
.AFORM {
:deep(.arco-row) {
margin-bottom: 16px;
}
}
</style>

View File

@@ -0,0 +1,10 @@
const config = [
{
key: 'keywords',
type: 'input',
label: '任务编号',
placeholder: '请输入集合编号'
},
];
export default config;

View File

@@ -5,7 +5,7 @@ const SystemStore = useSystemStore();
</script> </script>
<template> <template>
<div class="w-full h-full flex items-center px-[24px] box-border"> <div class="w-full h-full flex items-center px-[24px] box-border bg-white">
<div class="title"> <div class="title">
代发平台-{{SystemStore.isRoot?'管理员':'商家'}} 代发平台-{{SystemStore.isRoot?'管理员':'商家'}}
</div> </div>

View File

@@ -1,32 +1,31 @@
<script setup> <script setup>
import {onMounted} from "vue"; import {onMounted} from "vue";
import mockRoutes from "./mock.js";
import routesMap from "../../router/routes-map.js";
import {toPath} from "../../utils/index.js"; import {toPath} from "../../utils/index.js";
import {useSystemStore} from "../../pinia/SystemStore/index.js";
const SystemStore = useSystemStore();
onMounted(() => { onMounted(() => {
console.log(mockRoutes.map(v => ({
path: v.path,
name: v.name,
component: routesMap[v.component]
})))
}); });
const menuItemClick = (e) => { const menuItemClick = (e) => {
toPath(`/home${e}`); toPath(`/home${e}`);
} }
//--main-bg-color
</script> </script>
<template> <template>
<a-menu @menu-item-click="menuItemClick"> <div class="w-full h-full box-border">
<a-sub-menu v-for="item in mockRoutes" :key="item.name"> <a-menu @menu-item-click="menuItemClick">
<template #icon> <a-sub-menu v-for="item in SystemStore.RoutesTemp" :key="item.name">
<icon-apps></icon-apps> <template #icon>
</template> <icon-apps></icon-apps>
<template #title>{{ item.title }}</template> </template>
<a-menu-item v-for="k in item.children" :key="item.path + k.path">{{k.title}}</a-menu-item> <template #title>{{ item.title }}</template>
</a-sub-menu> <a-menu-item v-for="k in item.children" :key="`/${item.path}/${k.path}`">{{ k.title }}</a-menu-item>
</a-menu> </a-sub-menu>
</a-menu>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@@ -0,0 +1,31 @@
<script setup>
const {color, content} = defineProps({
color: {
type: String,
default: 'red',
},
content: {
type: String,
default: '内容',
}
});
</script>
<template>
<a-tooltip :content="content">
<a-badge>
<template v-slot:content>
<icon-question-circle :style="{ verticalAlign: 'middle', color: 'rgb(var(--primary-6))' }"/>
<slot v-if="$slots.icon" name="icon"></slot>
</template>
<a-tag :color="color" class="cursor-pointer">
<slot></slot>
</a-tag>
</a-badge>
</a-tooltip>
</template>
<style scoped>
</style>

View File

@@ -0,0 +1,72 @@
import {ref, reactive, watch} from 'vue';
/**
*
* @param parameter
* @param api
* @param callback
* @param immediate
* @param watchParameter
*/
function useTableQuery({
parameter,
api,
callback,
immediate = true,
watchParameter = false,
}) {
const loading = ref(false);
const pagination = reactive({
current: 1,
pageSize: 20,
total: 0
});
const fetchData = async () => {
try {
loading.value = true;
const params = {
...parameter,
current: pagination.current,
pageSize: pagination.pageSize
}
const {data} = await api(params);
pagination.pageSize = data.page;
pagination.total = data.total;
callback && callback(data);
} finally {
loading.value = false;
}
}
const initFetchData = async () => {
pagination.current = 1;
pagination.total = 0;
}
watch(
() => [pagination.current, pagination.pageSize],
() => fetchData(),
{deep: true, immediate: immediate}
)
if (watchParameter) watch(
() => parameter,
() => fetchData(),
{deep: true}
);
return {
loading,
pagination,
fetchData,
initFetchData,
}
}
export default useTableQuery;

View File

@@ -8,7 +8,7 @@ import LayoutSider from '../../components/LayoutSider/index.vue';
<a-layout-header id="layout-header" style="height: 60px"> <a-layout-header id="layout-header" style="height: 60px">
<LayoutHeader></LayoutHeader> <LayoutHeader></LayoutHeader>
</a-layout-header> </a-layout-header>
<a-layout> <a-layout class="mt-[4px]">
<a-layout-sider> <a-layout-sider>
<LayoutSider></LayoutSider> <LayoutSider></LayoutSider>
</a-layout-sider> </a-layout-sider>

View File

@@ -2,6 +2,9 @@
import {ref, reactive} from 'vue'; import {ref, reactive} from 'vue';
import {toPath} from "../../utils/index.js"; import {toPath} from "../../utils/index.js";
import VerificationCode from '../../components/VerificationCode/index.vue'; import VerificationCode from '../../components/VerificationCode/index.vue';
import {useUserStore} from "../../pinia/UserStore/index.js";
const {login} = useUserStore();
const MODE = { const MODE = {
PHONE: 'PHONE', PHONE: 'PHONE',
@@ -39,7 +42,7 @@ const mode = ref(MODE.PHONE);
</a-input> </a-input>
</div> </div>
<div class="flex flex-col mt-[50px] gap-[32px]"> <div class="flex flex-col mt-[50px] gap-[32px]">
<a-button type="primary">登陆</a-button> <a-button @click="login" type="primary">登陆</a-button>
<a-button <a-button
@click="toPath('/loginSYS/register')" @click="toPath('/loginSYS/register')"
type="text"> type="text">

View File

@@ -4,6 +4,7 @@
<template> <template>
<!-- 指派任务 --> <!-- 指派任务 -->
指派任务
</template> </template>
<style scoped> <style scoped>

View File

@@ -4,8 +4,11 @@
<template> <template>
<!-- 任务中心 --> <!-- 任务中心 -->
任务中心 <div id="Item-View" class="p-[20px]">
<router-view></router-view> <a-card>
<router-view></router-view>
</a-card>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@@ -1,10 +1,207 @@
<script setup> <script setup>
import {reactive, computed} from 'vue';
import Filter from "../../../../components/Filter/index.vue";
import TooltipTag from "../../../../components/TooltipTag/index.vue";
import useTableQuery from "../../../../hooks/useTableQuery.js";
import Api from "../../../../api/index.js";
const columns = [
{
title: '任务编号',
dataIndex: 'name',
},
{
title: '任务名称',
dataIndex: 'name',
},
{
title: '发布渠道',
dataIndex: 'name',
},
{
title: '创建时间',
dataIndex: 'name',
},
{
title: '当前状态',
dataIndex: 'status',
slotName: 'status',
},
{
title: '子任务进度',
dataIndex: 'name',
},
{
title: '消耗金额',
dataIndex: 'money',
slotName: 'money',
},
{
title: '是否开始',
dataIndex: 'start',
slotName: 'start',
},
{
title: '操作',
dataIndex: 'action',
slotName: 'action',
width: 200,
},
{
title: '',
dataIndex: 'exp',
slotName: 'exp'
},
];
const FilterConfig = computed(() => [
{
key: 'wd',
type: 'input',
label: '任务编号',
placeholder: '请输入集合编号'
},
{
key: 'wd',
type: 'input',
label: '任务名称',
placeholder: '请输入集合名称'
},
{
key: 'wd',
type: 'select',
label: '任务渠道',
placeholder: '全部',
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
{
label: '选项三',
value: 3,
},
]
},
{
key: 'wd',
type: 'select',
label: '任务状态',
placeholder: '全部',
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
{
label: '选项三',
value: 3,
},
]
},
{
key: 'wd',
type: 'datetime',
label: '创建时间',
placeholder: '全部',
},
]);
const vo = reactive({
page: '',
rows: [],
total: 0,
});
const po = reactive({
wd: null,
});
const {loading, pagination, initFetchData} = useTableQuery({
parameter: po,
api: Api.system.getData,
callback: (data) => {
Object.assign(vo, data);
console.log(vo);
}
});
</script> </script>
<template> <template>
<!-- 悬赏任务 --> <!-- 悬赏任务 -->
悬赏任务 <Filter v-model:from="po" :config="FilterConfig" @search="initFetchData"></Filter>
<div class="my-[20px]">
<div class="flex gap-[16px] mb-[20px]">
<a-button type="primary">
<template #icon>
<icon-plus/>
</template>
新建子任务
</a-button>
<a-button>
<template #icon>
<icon-plus/>
</template>
从模板快速创建
</a-button>
</div>
<a-table
:columns="columns"
:data="vo.rows"
:loading="loading"
:pagination="pagination">
<template v-slot:status="{record}">
<TooltipTag v-if="record.status === 0" color="cyan">待完善</TooltipTag>
<TooltipTag v-if="record.status === 1" color="red">未通过</TooltipTag>
<TooltipTag v-if="record.status === 2" color="magenta">请完善子任务</TooltipTag>
<TooltipTag v-if="record.status === 3" color="magenta">待付款</TooltipTag>
<TooltipTag v-if="record.status === 4" color="blue">投放中</TooltipTag>
<TooltipTag v-if="record.status === 5" color="orangered">暂停中</TooltipTag>
<TooltipTag v-if="record.status === 6" color="purple">终止</TooltipTag>
<TooltipTag v-if="record.status === 7" color="green">已完成</TooltipTag>
</template>
<template v-slot:start="{record}">
<a-switch></a-switch>
</template>
<template v-slot:money>
<div class="flex flex-col gap-[8px]">
<div>120.00 / 600.00</div>
<a-progress
:percent="200/600"
:show-text="false">
</a-progress>
</div>
</template>
<template v-slot:action>
<div class="flex gap-[16px]">
<a-link :hoverable="false">编辑</a-link>
<a-link :hoverable="false">查看子任务</a-link>
<a-link :hoverable="false" status="danger">终止</a-link>
</div>
</template>
<template v-slot:exp>
<a-trigger trigger="click" :unmount-on-close="false">
<a-link :hoverable="false">更多
<icon-down/>
</a-link>
<template #content>
<div class="demo-basic">
<a-button type="text">
存为模版
</a-button>
</div>
</template>
</a-trigger>
</template>
</a-table>
</div>
</template> </template>
<style scoped> <style scoped>

View File

@@ -1,9 +1,47 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {ref} from "vue"; import {ref} from "vue";
import mockRoutes from './mock.js';
import router from "../../router/index.js";
import generateRouter from "../../router/generateRouter.js";
export const useSystemStore = defineStore("SystemStore", () => { export const useSystemStore = defineStore("SystemStore", () => {
const isRoot = ref(false); const isRoot = ref(false);
const RoutesTemp = ref([]);
const installRoute = async () => {
const routes = generateRouter(RoutesTemp.value);
router.removeRoute('home');
router.addRoute({
path: '/home',
name: 'home',
component: () => import('../../pages/layout/index.vue'),
redirect: `/home/${routes[0].path}`,
children: routes
});
await router.replace(router.currentRoute.value.fullPath);
}
const setRouter = async () => {
RoutesTemp.value.length = 0;
// 请求资源 mockRoutes
const routes = generateRouter(mockRoutes);
RoutesTemp.value.push(...mockRoutes);
await installRoute();
}
return { return {
isRoot isRoot,
RoutesTemp,
setRouter,
installRoute,
}
}, {
persist: {
key: 'SystemStore',
storage: localStorage,
afterHydrate: (val) => {
val.store.installRoute && val.store.installRoute();
},
pick: ['RoutesTemp']
} }
}); });

View File

@@ -1,12 +1,25 @@
import {defineStore} from "pinia"; import {defineStore} from "pinia";
import {ref} from "vue"; import {ref} from "vue";
import {useSystemStore} from "../SystemStore/index.js";
import router from "../../router/index.js";
export const useUserStore = defineStore("UserStore", () => { export const useUserStore = defineStore("UserStore", () => {
const isLogin = ref(true); const isLogin = ref(false);
const userInfo = ref(null); const userInfo = ref(null);
const login = async (from) => {
// 请求
isLogin.value = true;
// 获取并安装路由
const { setRouter } = useSystemStore();
await setRouter();
// 跳转
await router.push('/home');
}
return { return {
isLogin, isLogin,
userInfo userInfo,
login,
} }
}, { }, {
persist: { persist: {

View File

@@ -0,0 +1,15 @@
import routesMap from "./routes-map.js";
const generateRouter = (routes) => {
return routes.map(v => ({
path: v.path,
name: v.name,
component: routesMap[v.component],
children: v.children && v.children.length > 0 && [
{path: '', redirect: `/home/${v.name}/${v.children[0].name}`},
...generateRouter(v.children)
],
}))
}
export default generateRouter;

View File

@@ -1,6 +1,7 @@
import {createRouter, createWebHashHistory} from 'vue-router'; import {createRouter, createWebHashHistory} from 'vue-router';
import routes from "./routes.js"; import routes from "./routes.js";
import {useUserStore} from "../pinia/UserStore/index.js"; import {useUserStore} from "../pinia/UserStore/index.js";
import {useSystemStore} from "../pinia/SystemStore/index.js";
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(), history: createWebHashHistory(),
@@ -9,6 +10,7 @@ const router = createRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const {isLogin} = useUserStore(); const {isLogin} = useUserStore();
const SystemStore = useSystemStore();
if (!isLogin && !to.path.includes('loginSYS')) { if (!isLogin && !to.path.includes('loginSYS')) {
next({ path: '/loginSYS' }); next({ path: '/loginSYS' });

View File

@@ -1,27 +0,0 @@
// const merchant = [
// {
// path: '/task-center',
// name: 'task-center',
// title: '任务中心',
// icon: '',
// component: () => import('../pages/merchant/pages/task-center/index.vue'),
// children: [
// {
// path: '/reward-mission',
// name: 'reward-mission',
// title: '悬赏任务',
// icon: '',
// component: () => import('../pages/merchant/pages/task-center/reward-mission.vue'),
// },
// {
// path: '/appointed-task',
// name: 'appointed-task',
// title: '任务指派',
// icon: '',
// component: () => import('../pages/merchant/pages/task-center/appointed-task.vue'),
// }
// ]
// },
// ]
//
// export default merchant;

View File

@@ -1,7 +1,7 @@
const routesMap = { const routesMap = {
'task-center': import('../pages/merchant/pages/task-center/index.vue'), 'task-center': () => import('../pages/merchant/pages/task-center/index.vue'),
'reward-mission': import('../pages/merchant/pages/task-center/reward-mission.vue'), 'reward-mission': () => import('../pages/merchant/pages/task-center/reward-mission.vue'),
'appointed-task': import('../pages/merchant/pages/task-center/appointed-task.vue'), 'appointed-task': () => import('../pages/merchant/pages/task-center/appointed-task.vue'),
}; };
export default routesMap; export default routesMap;

View File

@@ -1,3 +1,5 @@
import routesMap from "./routes-map.js";
const routes = [ const routes = [
{ {
path: '/', path: '/',
@@ -7,6 +9,7 @@ const routes = [
path: '/home', path: '/home',
name: 'home', name: 'home',
component: () => import('../pages/layout/index.vue'), component: () => import('../pages/layout/index.vue'),
children: [],
}, },
{ {
path: '/loginSYS', path: '/loginSYS',

View File

@@ -1,5 +1,26 @@
:root {
--main-bg-color: rgb(247, 248, 250);
}
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
font-family: "PingFang SC", serif; font-family: "PingFang SC", serif;
} }
body {
background-color: var(--main-bg-color);
}
#Item-View {
.arco-card-body {
@apply p-[20px];
}
}
.demo-basic { // 默认下拉框样式
padding: 5px;
width: auto;
background-color: var(--color-bg-popup);
border-radius: 4px;
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.15);
}
.arco-btn-text {
color: var(--color-text-2) !important;
}

41
src/utils/request.js Normal file
View File

@@ -0,0 +1,41 @@
import axios from 'axios';
// import {useUserStore} from "../pinia/UserStore";
// 创建 Axios 实例
const request = axios.create({
baseURL: import.meta.env.VITE_API_URL, // 替换为你的基础 URL
timeout: 10000, // 请求超时设置
});
// 请求拦截器
request.interceptors.request.use(
(config) => {
// const {userInfo} = useUserStore();
// 如果 token 存在,则将其添加到请求头中
// if (userInfo?.token) {
// config.headers['token'] = `${userInfo?.token}`;
// }
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
request.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
if (error.response) {
return Promise.reject(error.response.data); // 返回错误信息
} else { // 网络错误
return Promise.reject(error.message);
}
}
);
export default request; // 导出 Axios 实例

View File

@@ -2,6 +2,7 @@ import {defineConfig} from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import tailwindcss from 'tailwindcss'; import tailwindcss from 'tailwindcss';
import AutoImport from 'unplugin-auto-import/vite'; import AutoImport from 'unplugin-auto-import/vite';
import vueDevTools from 'vite-plugin-vue-devtools';
import Components from 'unplugin-vue-components/vite'; import Components from 'unplugin-vue-components/vite';
import {vitePluginForArco} from '@arco-plugins/vite-vue'; import {vitePluginForArco} from '@arco-plugins/vite-vue';
import {ArcoResolver} from 'unplugin-vue-components/resolvers'; import {ArcoResolver} from 'unplugin-vue-components/resolvers';
@@ -11,6 +12,7 @@ export default defineConfig({
base: './', base: './',
plugins: [ plugins: [
vue(), vue(),
vueDevTools(),
AutoImport({ AutoImport({
resolvers: [ArcoResolver()], resolvers: [ArcoResolver()],
}), }),
@@ -38,6 +40,6 @@ export default defineConfig({
} }
}, },
server: { server: {
port: 9000 port: 9050
} }
}) })