finish init again
This commit is contained in:
13
src/App.vue
Normal file
13
src/App.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<a-config-provider :locale="zhCN">
|
||||
<router-view/>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import zhCN from "ant-design-vue/es/locale/zh_CN";
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
</script>
|
||||
4
src/api/ImageUrl.ts
Normal file
4
src/api/ImageUrl.ts
Normal file
@ -0,0 +1,4 @@
|
||||
// export const downLoadImage = 'http://1.94.237.210:3457/file/download/'
|
||||
export const downLoadImage = 'http://27.30.77.229:9092/file/download/'
|
||||
|
||||
|
||||
32
src/api/myAxios.ts
Normal file
32
src/api/myAxios.ts
Normal file
@ -0,0 +1,32 @@
|
||||
// 创建实例时配置默认值
|
||||
import axios from "axios";
|
||||
import router from "../router";
|
||||
|
||||
// const viteEnv = import.meta.env;
|
||||
|
||||
const myAxios = axios.create({
|
||||
withCredentials: true,
|
||||
//baseURL:'http://localhost:9091'
|
||||
// baseURL:'http://1.94.237.210:3457'
|
||||
//baseURL:'http://1.94.237.210:8088'
|
||||
// baseURL:'http://27.30.77.229:9091/'
|
||||
baseURL:'http://27.30.77.229:9092/'
|
||||
|
||||
});
|
||||
|
||||
myAxios.interceptors.request.use(function (config) {
|
||||
return config;
|
||||
}, function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
myAxios.interceptors.response.use(function (response: any) {
|
||||
if (response.data.code === 40100) {
|
||||
router.replace('/')
|
||||
}
|
||||
return response.data;
|
||||
}, function (error) {
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
export default myAxios;
|
||||
BIN
src/assets/Login.png
Normal file
BIN
src/assets/Login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 594 KiB |
BIN
src/assets/login/hidePassword.png
Normal file
BIN
src/assets/login/hidePassword.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/login/showPassword.png
Normal file
BIN
src/assets/login/showPassword.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.6 KiB |
48
src/layout/ManageLayout.vue
Normal file
48
src/layout/ManageLayout.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div id="manage">
|
||||
<a-layout has-sider>
|
||||
<a-layout-sider :style="{ width: '200px' }">
|
||||
<ManageSidebar/>
|
||||
</a-layout-sider>
|
||||
<a-layout>
|
||||
<a-layout-header :style="{ background: '#fff', padding: 0}">
|
||||
<ManageHeader/>
|
||||
</a-layout-header>
|
||||
<a-layout-content class="main">
|
||||
<router-view :key="key" class="main-card"/>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ManageHeader from "./manage/ManageHeader.vue";
|
||||
import ManageSidebar from "./manage/ManageSidebar.vue";
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const key = () => {
|
||||
return route.path + Math.random();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
#manage {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
flex: 1;
|
||||
flex-basis: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
208
src/layout/manage/ManageHeader.vue
Normal file
208
src/layout/manage/ManageHeader.vue
Normal file
@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="manage-header">
|
||||
<div class="header-left">
|
||||
<a-breadcrumb class="breadcrumb">
|
||||
<a-breadcrumb-item>青橙校园后台管理系统</a-breadcrumb-item>
|
||||
<a-breadcrumb-item class="routeName">{{ route.name }}</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<!-- <div class="bell">-->
|
||||
<!-- <BellTwoTone/>-->
|
||||
<!-- </div>-->
|
||||
<div class="name">
|
||||
{{ store.loginUser.userRole === "notLogin" ? '未登录' : store.loginUser.userRole }}
|
||||
</div>
|
||||
<div class="user">
|
||||
<a-dropdown>
|
||||
<a-avatar :size="40" class="user-avatar" :src="downLoadImage+store.loginUser.avatarUrl"/>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item @click="logout">
|
||||
<LogoutOutlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="showDrawer">
|
||||
<UserOutlined />
|
||||
个人中心
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-drawer
|
||||
title="个人中心"
|
||||
:placement="placement"
|
||||
:closable="false"
|
||||
:open="open"
|
||||
@close="onClose"
|
||||
class="custom-class"
|
||||
>
|
||||
<a-avatar :size="64" :src="downLoadImage+store.loginUser.avatarUrl"></a-avatar>
|
||||
|
||||
<div class="message">
|
||||
<p class="firstmessage">昵称:{{ store.loginUser.nickName }}</p>
|
||||
<p>账号:{{ store.loginUser.username }}</p>
|
||||
<p>账号权限:{{store.loginUser.userRole}}</p>
|
||||
<p>联系方式:{{store.loginUser.phone}}</p>
|
||||
</div>
|
||||
|
||||
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import {onBeforeMount} from 'vue'
|
||||
import {userStore} from "../../store/userStore.ts";
|
||||
import {UserOutlined,LogoutOutlined} from '@ant-design/icons-vue';
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import {message} from "ant-design-vue";
|
||||
import { ref } from 'vue';
|
||||
import type { DrawerProps } from 'ant-design-vue';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const store = userStore()
|
||||
|
||||
|
||||
const placement = ref<DrawerProps['placement']>('right');
|
||||
const open = ref<boolean>(false);
|
||||
const checkLoginStatus = () => {
|
||||
// 检查store中的登录状态
|
||||
if (store.loginUser.userRole === "notLogin") {
|
||||
console.log("未检测到登录状态,跳转到登录页");
|
||||
// 使用replace替换当前路由,禁止返回
|
||||
router.replace({ path: '/' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 生命周期钩子:在组件挂载前检查登录状态
|
||||
onBeforeMount(() => {
|
||||
checkLoginStatus();
|
||||
});
|
||||
const showDrawer = () => {
|
||||
open.value = true;
|
||||
};
|
||||
|
||||
const onClose = () => {
|
||||
open.value = false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 登出
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
// 严格遵循接口文档路径大小写
|
||||
const res: any = await myAxios.get(
|
||||
"/userInfo/logout",
|
||||
{
|
||||
headers: {
|
||||
Authorization:token,
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('登出响应:', res);
|
||||
|
||||
if (res.code === 1) {
|
||||
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
store.$reset();
|
||||
|
||||
|
||||
await router.replace('/');
|
||||
message.success('登出成功');
|
||||
} else {
|
||||
|
||||
message.error(`登出失败:${res.message || '未知错误'}`);
|
||||
console.error('业务逻辑错误:', res);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求异常:', error);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.manage-header {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
background: #fff;
|
||||
border-bottom: 2px solid #ccced7;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.header-left .breadcrumb {
|
||||
color: black;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.routeName {
|
||||
font-size: 13px;
|
||||
padding-top: 10px;
|
||||
font-weight: 400;
|
||||
|
||||
}
|
||||
.header-right {
|
||||
display: flex;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.header-right .bell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.header-right .name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 14px;
|
||||
}
|
||||
|
||||
.header-right .user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.user-avatar:hover {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
|
||||
.custom-class img{
|
||||
float: left;
|
||||
}
|
||||
.message {
|
||||
float: right;
|
||||
margin-right: 100px;
|
||||
}
|
||||
.firstmessage {
|
||||
margin-top: 0;
|
||||
}
|
||||
</style>
|
||||
128
src/layout/manage/ManageSidebar.vue
Normal file
128
src/layout/manage/ManageSidebar.vue
Normal file
@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
style="max-height: 200vh; min-height: 100vh;background-color: #ffe7ba"
|
||||
@click="handleClick"
|
||||
>
|
||||
<a-menu-item key="/index">
|
||||
<PieChartOutlined />
|
||||
<span>首页</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/userList">
|
||||
<UserOutlined />
|
||||
<span>用户列表</span>
|
||||
</a-menu-item>
|
||||
<a-sub-menu>
|
||||
<template #title>
|
||||
<span>
|
||||
<UserOutlined />
|
||||
<span>项目管理</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-menu-item key="/project">接单管理</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<a-sub-menu>
|
||||
<template #title>
|
||||
<span>
|
||||
<CommentOutlined />
|
||||
<span>结算管理</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-menu-item key="/applicationRecord">推广码申请记录</a-menu-item>
|
||||
<a-menu-item key="/withdrawalApplicationRecord">提现申请记录</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu>
|
||||
<template #title>
|
||||
<span>
|
||||
<ReadOutlined />
|
||||
<span>课程管理</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-menu-item key="/localCurriculum">本地课程</a-menu-item>
|
||||
<a-menu-item key="/linkedCourse">链接课程</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-sub-menu>
|
||||
<template #title>
|
||||
<span>
|
||||
<FieldTimeOutlined />
|
||||
<span>勤工俭学</span>
|
||||
</span>
|
||||
</template>
|
||||
<a-menu-item key="/workList">工作列表</a-menu-item>
|
||||
<a-menu-item key="/workDetail">工作详情</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<a-menu-item key="/community">
|
||||
<CommentOutlined />
|
||||
<span>社群</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {onMounted, ref} from "vue";
|
||||
import {UserOutlined, FieldTimeOutlined,ReadOutlined,CommentOutlined,PieChartOutlined} from '@ant-design/icons-vue';
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
// 选中侧边栏
|
||||
const selectedKeys = ref<string[]>(['/userList']);
|
||||
|
||||
onMounted(() => {
|
||||
setSelectedKey()
|
||||
})
|
||||
|
||||
// 根据路由设置当前选中侧边栏
|
||||
const setSelectedKey = () => {
|
||||
selectedKeys.value = [route.path];
|
||||
}
|
||||
|
||||
// 路由跳转
|
||||
const handleClick = (item: any) => {
|
||||
router.push(item.key)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全局菜单项样式 */
|
||||
:deep(.ant-menu-item),
|
||||
:deep(.ant-menu-submenu-title) {
|
||||
color: rgba(0, 0, 0, 0.85) !important; /* 未选中黑色字体 */
|
||||
font-weight: normal !important;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 选中项样式 */
|
||||
:deep(.ant-menu-item-selected) {
|
||||
background-color: #ffa940 !important;
|
||||
color: white !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
/* 鼠标悬停样式 */
|
||||
:deep(.ant-menu-item:hover),
|
||||
:deep(.ant-menu-submenu-title:hover) {
|
||||
background-color: rgba(255, 169, 64, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 子菜单箭头颜色 */
|
||||
:deep(.ant-menu-submenu-arrow::before),
|
||||
:deep(.ant-menu-submenu-arrow::after) {
|
||||
background: rgba(0, 0, 0, 0.65) !important;
|
||||
}
|
||||
|
||||
/* 子菜单展开时标题样式 */
|
||||
:deep(.ant-menu-submenu-selected .ant-menu-submenu-title) {
|
||||
color: rgba(0, 0, 0, 0.95) !important;
|
||||
}
|
||||
|
||||
/* 折叠状态下选中样式 */
|
||||
:deep(.ant-menu-inline-collapsed .ant-menu-item-selected) {
|
||||
background-color: #ffa940 !important;
|
||||
}
|
||||
</style>
|
||||
16
src/main.ts
Normal file
16
src/main.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import {createApp} from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
import pinia from "./store";
|
||||
import router from "./router";
|
||||
import 'quill/dist/quill.snow.css';
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||
|
||||
const app = createApp(App)
|
||||
// 配置路由
|
||||
app.use(router)
|
||||
// 配置pinia
|
||||
app.use(pinia)
|
||||
// 挂在实例
|
||||
app.mount('#app')
|
||||
|
||||
11
src/router/index.ts
Normal file
11
src/router/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import {createRouter, createWebHashHistory} from "vue-router";
|
||||
import {routes} from "./routes";
|
||||
|
||||
// 创建路由实例并传递 `routes` 配置
|
||||
const router = createRouter({
|
||||
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
|
||||
history: createWebHashHistory(),
|
||||
routes, // `routes: routes` 的缩写
|
||||
})
|
||||
|
||||
export default router
|
||||
112
src/router/routes.ts
Normal file
112
src/router/routes.ts
Normal file
@ -0,0 +1,112 @@
|
||||
// 将路由规则 routes 导出
|
||||
export const routes = [
|
||||
// 全局路由(无需嵌套上左右整体布局)
|
||||
{
|
||||
path: '/',
|
||||
name: 'Login',
|
||||
component: () => import("../view/Login.vue")
|
||||
},
|
||||
{
|
||||
path: '/test',
|
||||
name: '全局测试页面',
|
||||
component: () => import("../view/Test.vue"),
|
||||
},
|
||||
// 管理端
|
||||
{
|
||||
path: '/manage',
|
||||
component: () => import("../layout/ManageLayout.vue"),
|
||||
children: [
|
||||
// 首页
|
||||
{
|
||||
path: '/index',
|
||||
name: '首页',
|
||||
component: () => import("../view/Index.vue"),
|
||||
},
|
||||
{
|
||||
path: '/localCurriculum',
|
||||
name: '本地课程',
|
||||
component: () => import("../view/course/localCurriculum.vue"),
|
||||
},
|
||||
{
|
||||
path: '/linkedCourse',
|
||||
name: '链接课程',
|
||||
component: () => import("../view/course/linkedCourse.vue"),
|
||||
},
|
||||
{
|
||||
path: '/workList',
|
||||
name: '工作列表',
|
||||
component: () => import("../view/work/workList.vue"),
|
||||
},
|
||||
{
|
||||
path: '/workDetail',
|
||||
name: '工作详情',
|
||||
component: () => import("../view/work/workDetail.vue"),
|
||||
},
|
||||
{
|
||||
path: '/community',
|
||||
name: '社群',
|
||||
component: () => import("../view/community/community.vue"),
|
||||
},
|
||||
{
|
||||
path: '/userList',
|
||||
name: '用户列表',
|
||||
component: () => import("../view/userList/userList.vue"),
|
||||
},
|
||||
{
|
||||
path: '/project',
|
||||
name: '项目管理',
|
||||
component: () => import("../view/project/project.vue"),
|
||||
},
|
||||
{
|
||||
path: '/projectDetail',
|
||||
name: '项目详情',
|
||||
component: () => import("../view/project/projectDetail.vue"),
|
||||
},
|
||||
{
|
||||
path: '/addProject',
|
||||
name: '新增项目',
|
||||
component: () => import("../view/project/addProject.vue"),
|
||||
},
|
||||
{
|
||||
path: '/moneyDetail',
|
||||
name: '项目明细',
|
||||
component: () => import("../view/project/moneyDetail.vue"),
|
||||
},
|
||||
{
|
||||
path: '/projectNotice',
|
||||
name: '项目通知',
|
||||
component: () => import("../view/project/projectNotice.vue"),
|
||||
},
|
||||
{
|
||||
path: '/promotionCode',
|
||||
name: '推广码',
|
||||
component: () => import("../view/project/promotionCode.vue"),
|
||||
},
|
||||
{
|
||||
path: '/applicationRecord',
|
||||
name: '推广码记录',
|
||||
component: () => import("../view/settlement/applicationRecord.vue"),
|
||||
},
|
||||
{
|
||||
path: '/addprojectNotice',
|
||||
name: '新增项目通知',
|
||||
component: () => import("../view/project/addprojectNotice.vue"),
|
||||
},
|
||||
{
|
||||
path: '/moneyRecord',
|
||||
name: '项目结算记录',
|
||||
component: () => import("../view/settlement/moneyRecord.vue"),
|
||||
},
|
||||
{
|
||||
path: '/noticeDetail',
|
||||
name: '项目通知详情',
|
||||
component: () => import("../view/project/noticeDetail.vue"),
|
||||
},
|
||||
{
|
||||
path: '/withdrawalApplicationRecord',
|
||||
name: '提现申请记录',
|
||||
component: () => import("../view/settlement/withdrawalApplicationRecord.vue"),
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
10
src/store/index.ts
Normal file
10
src/store/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import {createPinia} from 'pinia'
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate)
|
||||
|
||||
|
||||
|
||||
|
||||
export default pinia
|
||||
72
src/store/userStore.ts
Normal file
72
src/store/userStore.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import myAxios from "../api/myAxios";
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
export const userStore = defineStore('user', {
|
||||
state: () => ({
|
||||
loginUser: {
|
||||
username: '未登录',
|
||||
avatarUrl: '',
|
||||
userRole: 'notLogin',
|
||||
phone: '',
|
||||
nickName:''
|
||||
}
|
||||
}),
|
||||
persist: true,
|
||||
|
||||
actions: {
|
||||
// 获取登录用户信息
|
||||
async getLoginUser() {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (!storedToken) {
|
||||
message.warning('请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
const res:any = await myAxios.get('/userInfo/get/jwt/web', {
|
||||
headers: {
|
||||
Authorization: storedToken
|
||||
}
|
||||
});
|
||||
console.log(res)
|
||||
if (res.code === 1) {
|
||||
console.log(res)
|
||||
this.updateUser({
|
||||
username: res.data.userAccount,
|
||||
avatarUrl: res.data.userAvatar,
|
||||
userRole: res.data.userRole,
|
||||
phone: res.data.phoneNumber,
|
||||
nickName:res.data.nickName
|
||||
});
|
||||
} else {
|
||||
message.error(res.message || '获取用户信息失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
message.error('获取用户信息失败,请检查网络连接');
|
||||
this.clearUser();
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户信息
|
||||
updateUser(payload: any) {
|
||||
this.loginUser = {
|
||||
...this.loginUser,
|
||||
...payload
|
||||
};
|
||||
},
|
||||
|
||||
// 清除用户信息
|
||||
clearUser() {
|
||||
this.loginUser = {
|
||||
username: '未登录',
|
||||
avatarUrl: '',
|
||||
userRole: 'notLogin',
|
||||
phone: '',
|
||||
nickName: ''
|
||||
};
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
}
|
||||
})
|
||||
48
src/style.css
Normal file
48
src/style.css
Normal file
@ -0,0 +1,48 @@
|
||||
html {
|
||||
/* 滚动时采用平滑过渡 */
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 管理页面全局样式 */
|
||||
|
||||
.main-card {
|
||||
min-height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.search-box {
|
||||
display: flex;
|
||||
background-color: white;
|
||||
height: auto;
|
||||
box-shadow: 0 0 1px 1px #dedede;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.middle-button {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.data-box {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 1px 1px #dedede;
|
||||
border-radius: 5px;
|
||||
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin: 20px 0 20px 0;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.txt {
|
||||
font-weight: 900;
|
||||
font-size: 500px;
|
||||
}
|
||||
7
src/types/wangeditor.d.ts
vendored
Normal file
7
src/types/wangeditor.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
// src/types/wangeditor.d.ts
|
||||
declare module '@wangeditor/editor-for-vue' {
|
||||
import { Component } from 'vue'
|
||||
export const Editor: Component
|
||||
export const Toolbar: Component
|
||||
// 如果库有其他导出,需一并声明
|
||||
}
|
||||
3
src/view/Index.vue
Normal file
3
src/view/Index.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
测试页面
|
||||
</template>
|
||||
369
src/view/Login.vue
Normal file
369
src/view/Login.vue
Normal file
@ -0,0 +1,369 @@
|
||||
<template>
|
||||
<div id="login">
|
||||
<form>
|
||||
<div class="box" @submit.prevent>
|
||||
<h2>欢迎登录青橙校园管理端</h2>
|
||||
<div class="input-box">
|
||||
|
||||
<input type="text" placeholder="账号" v-model="userAccount"/>
|
||||
</div>
|
||||
<div class="input-box">
|
||||
|
||||
<input class="password" :type="type" placeholder="密码" v-model="userPassword"/>
|
||||
<!-- <div @click="showPwd">-->
|
||||
<!-- <img src="../assets/login/hidePassword.png" v-show="!showPassword" alt=""/>-->
|
||||
<!-- <img src="../assets/login/showPassword.png" v-show="showPassword" alt=""/>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
<div class="error-messages" v-if="userPassword && passwordErrors.length">
|
||||
<!-- <div class="error-messages" v-if="false">-->
|
||||
<div v-for="error in passwordErrors" :key="error" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-box">
|
||||
<div>
|
||||
<button @click="onLogin" type="button" :disabled="!isFormValid">登录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useRouter} from 'vue-router'
|
||||
import {ref, computed, watch} from 'vue'
|
||||
import {userStore} from "../store/userStore.ts";
|
||||
import myAxios from "../api/myAxios";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
const store = userStore()
|
||||
const router = useRouter()
|
||||
|
||||
//默认闭眼图标
|
||||
// let showPassword = ref(false)
|
||||
//登录密码隐藏
|
||||
let type = ref('password')
|
||||
const userAccount = ref('');
|
||||
const userPassword = ref('');
|
||||
|
||||
/*
|
||||
* 验证
|
||||
* */
|
||||
|
||||
const passwordErrors = ref<string[]>([]);
|
||||
|
||||
const validatePassword = () => {
|
||||
const errors: string[] = [];
|
||||
const password = userPassword.value;
|
||||
|
||||
if (password.length < 6 || password.length > 11) {
|
||||
errors.push('密码长度需为6到11位');
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
errors.push('必须包含大写字母');
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
errors.push('必须包含小写字母');
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
errors.push('必须至少包含一个数字');
|
||||
}
|
||||
|
||||
passwordErrors.value = errors;
|
||||
};
|
||||
// const validatePassword = () => {
|
||||
// passwordErrors.value = []; // 直接设置为空数组
|
||||
// // 注释原有验证逻辑
|
||||
// };
|
||||
|
||||
const isFormValid = computed(() => {
|
||||
return userAccount.value.trim() !== '' &&
|
||||
userPassword.value !== '' &&
|
||||
passwordErrors.value.length === 0;
|
||||
});
|
||||
|
||||
// const isFormValid = computed(() => {
|
||||
// return userAccount.value.trim() !== '' &&
|
||||
// userPassword.value !== ''
|
||||
// // && passwordErrors.value.length === 0
|
||||
// });
|
||||
|
||||
watch(userPassword, validatePassword);
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*/
|
||||
const onLogin = async () => {
|
||||
try {
|
||||
|
||||
const res: any = await myAxios.post(
|
||||
"/userInfo/login",
|
||||
{
|
||||
userAccount: userAccount.value,
|
||||
userPassword: userPassword.value // 发送加密后的密码
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'AfterScript': 'required-script'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(res);
|
||||
if (res.code === 1 && res?.data) {
|
||||
localStorage.setItem('token', res.data);
|
||||
myAxios.defaults.headers.common['Authorization'] = res.data;
|
||||
await store.getLoginUser();
|
||||
if (store.loginUser) {
|
||||
router.push('/index');
|
||||
message.success('登录成功');
|
||||
} else {
|
||||
message.error('获取用户信息失败');
|
||||
}
|
||||
} else {
|
||||
message.error(`登录失败:${res.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
message.error('登录失败,请稍后再试');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示密码
|
||||
*/
|
||||
// const showPwd = () => {
|
||||
// showPassword.value = !showPassword.value
|
||||
// if (showPassword.value == false) {
|
||||
// type.value = 'password'
|
||||
// } else {
|
||||
// type.value = 'text'
|
||||
// }
|
||||
// }
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#login {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#login::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: url('../assets/Login.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
opacity: 0.3;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.box {
|
||||
z-index:1;
|
||||
width: 450px;
|
||||
height: 350px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.5);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
background: rgba(50, 50, 50, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background-image: url('../assets/Login.png');
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.box > h2 {
|
||||
color: #fff;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.box .input-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.box .input-box input {
|
||||
letter-spacing: 1px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
width: 250px;
|
||||
height: 35px;
|
||||
border-radius: 15px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
outline: none;
|
||||
padding: 0 12px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.box .input-box .eye {
|
||||
float: right;
|
||||
position: relative;
|
||||
bottom: 32px;
|
||||
right: -210px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box .input-box img {
|
||||
float: right;
|
||||
position: relative;
|
||||
bottom: 32px;
|
||||
right: 6px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.box .input-box input:focus {
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.box .btn-box {
|
||||
width: 250px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.box .btn-box > a {
|
||||
outline: none;
|
||||
display: block;
|
||||
width: 250px;
|
||||
text-align: end;
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.box .btn-box > a:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.box .btn-box > div {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.box .btn-box > div > button {
|
||||
outline: none;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.box .btn-box > div > button:nth-of-type(1) {
|
||||
width: 250px;
|
||||
height: 35px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
background: rgba(217, 224, 231, 0.5);
|
||||
}
|
||||
|
||||
.box .btn-box > div > button:nth-of-type(2) {
|
||||
width: 120px;
|
||||
height: 35px;
|
||||
margin-left: 10px;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
background: rgba(64, 149, 229, 0.5);
|
||||
}
|
||||
|
||||
.box .btn-box > div > button:hover {
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
background: rgb(249, 249, 208);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
.container {
|
||||
width: 28%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1366px) {
|
||||
.container {
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.container {
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.agileheader h1 {
|
||||
font-size: 41px;
|
||||
}
|
||||
|
||||
.container h2 {
|
||||
font-size: 27px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.container {
|
||||
width: 49%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.error-messages {
|
||||
width: 250px;
|
||||
margin: -8px 0 10px 0;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #ff0000;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 禁用按钮样式 */
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
background: rgba(217, 224, 231, 0.3) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||
}
|
||||
|
||||
button:disabled:hover {
|
||||
background: rgba(217, 224, 231, 0.3) !important;
|
||||
color: rgba(255, 255, 255, 0.9) !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
48
src/view/Test.vue
Normal file
48
src/view/Test.vue
Normal file
@ -0,0 +1,48 @@
|
||||
<script setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
padding: 1rem;
|
||||
background: #ffe3e3;
|
||||
color: #ff4444;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
display: inline-block;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
border-radius: 50%;
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background-color: #fafafa !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-row:hover) {
|
||||
background-color: #fafafa !important;
|
||||
}
|
||||
</style>
|
||||
4
src/view/community/community.vue
Normal file
4
src/view/community/community.vue
Normal file
@ -0,0 +1,4 @@
|
||||
<template>
|
||||
<div>465</div>
|
||||
</template>
|
||||
|
||||
133
src/view/components/RichTextEditor.vue
Normal file
133
src/view/components/RichTextEditor.vue
Normal file
@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div class="editor-container">
|
||||
<Toolbar
|
||||
:editor="editorRef"
|
||||
:defaultConfig="toolbarConfig"
|
||||
class="toolbar"
|
||||
/>
|
||||
<Editor
|
||||
v-model="valueHtml"
|
||||
:defaultConfig="editorConfig"
|
||||
:mode="mode"
|
||||
class="editor"
|
||||
@onCreated="handleCreated"
|
||||
@onFocus="handleFocus"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { shallowRef, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
import type { IDomEditor } from '@wangeditor/editor'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
disable: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'content-change'])
|
||||
|
||||
const editorRef = shallowRef<IDomEditor>()
|
||||
const valueHtml = ref(props.modelValue)
|
||||
const mode = 'default'
|
||||
|
||||
const toolbarConfig = {
|
||||
excludeKeys: [
|
||||
'insertVideo',
|
||||
'uploadVideo',
|
||||
'codeBlock',
|
||||
'|',
|
||||
'group-more-style'
|
||||
]
|
||||
}
|
||||
|
||||
const editorConfig = {
|
||||
placeholder: '请输入内容...',
|
||||
readOnly: props.disable,
|
||||
MENU_CONF: {
|
||||
uploadImage: {
|
||||
allowedFileTypes: ['image/*'],
|
||||
async customUpload(file: File, insertFn: (url: string) => void) {
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('biz', 'richText')
|
||||
formData.append('file', file)
|
||||
formData.append('new', '1')
|
||||
formData.append('Ctrl', 'upload')
|
||||
|
||||
const res: any = await myAxios.post('/file/upload', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
})
|
||||
|
||||
if (res.code === 1) {
|
||||
// 拼接完整 URL 地址再插入到富文本中
|
||||
const imageUrl = 'http://27.30.77.229:9092/file/download/' + res.data
|
||||
insertFn(imageUrl)
|
||||
} else {
|
||||
console.error('上传失败:', res.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('图片上传失败', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
editorRef.value?.restoreSelection()
|
||||
}
|
||||
|
||||
const handleCreated = (editor: IDomEditor) => {
|
||||
editorRef.value = editor
|
||||
|
||||
editor.on('menuClick', (menu: { key: string }) => { // ✅ 显式类型声明
|
||||
if (menu.key === 'headerSelect') {
|
||||
nextTick(() => {
|
||||
editor.restoreSelection()
|
||||
editor.focus()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal !== valueHtml.value && editorRef.value?.isEmpty()) {
|
||||
valueHtml.value = newVal
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(valueHtml, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
emit('content-change', newVal)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (editorRef.value) {
|
||||
editorRef.value.destroy()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.editor-container {
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
.toolbar {
|
||||
border-bottom: 1px solid #e2e8f0 !important;
|
||||
}
|
||||
</style>
|
||||
11
src/view/course/linkedCourse.vue
Normal file
11
src/view/course/linkedCourse.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>链接课程</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
11
src/view/course/localCurriculum.vue
Normal file
11
src/view/course/localCurriculum.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>本地课程</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
790
src/view/project/addProject.vue
Normal file
790
src/view/project/addProject.vue
Normal file
@ -0,0 +1,790 @@
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit" class="modern-form">
|
||||
<h2 class="form-title">新建推广项目</h2>
|
||||
|
||||
<div class="form-grid">
|
||||
<!-- 左列 - 手机预览区域 -->
|
||||
<div class="form-column phone-preview-container">
|
||||
<div class="phone-frame">
|
||||
<div class="phone-header">
|
||||
<div class="phone-camera"></div>
|
||||
<div class="phone-speaker"></div>
|
||||
</div>
|
||||
|
||||
<div class="phone-screen">
|
||||
<div class="phone-content">
|
||||
<!-- 项目信息 -->
|
||||
<div class="phone-project-info">
|
||||
<h2 class="phone-project-title">{{ formData.projectName }}</h2>
|
||||
<div class="phone-project-image" :style="{ backgroundImage: `url(${formData.projectImage})` }"></div>
|
||||
<p class="phone-project-desc">{{ formData.projectDescription }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 富文本内容区域 -->
|
||||
<div class="phone-sections">
|
||||
<!-- 结算说明 -->
|
||||
<div class="phone-section">
|
||||
<h3 class="phone-section-title">结算说明</h3>
|
||||
<div class="phone-section-content" v-html="formData.settlementDesc"></div>
|
||||
</div>
|
||||
|
||||
<!-- 项目说明 -->
|
||||
<div class="phone-section">
|
||||
<h3 class="phone-section-title">项目说明</h3>
|
||||
<div class="phone-section-content" v-html="formData.projectDesc"></div>
|
||||
</div>
|
||||
|
||||
<!-- 项目流程 -->
|
||||
<div class="phone-section">
|
||||
<h3 class="phone-section-title">项目流程</h3>
|
||||
<div class="phone-section-content" v-html="formData.projectFlow"></div>
|
||||
</div>
|
||||
|
||||
<!-- 申请推广码说明 -->
|
||||
<div class="phone-section">
|
||||
<h3 class="phone-section-title">申请推广码说明</h3>
|
||||
<div class="phone-section-content" v-html="formData.applyPromoCodeDesc"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-home-button"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右列 - 富文本编辑区域 -->
|
||||
<div class="form-column">
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">项目名称</span>
|
||||
<input
|
||||
v-model="formData.projectName"
|
||||
type="text"
|
||||
class="input-field"
|
||||
required
|
||||
placeholder="请输入项目名称"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">项目图片</span>
|
||||
<div class="file-upload">
|
||||
<input
|
||||
type="file"
|
||||
class="file-input"
|
||||
accept="image/*"
|
||||
@change="handleFileUpload"
|
||||
ref="fileInput"
|
||||
>
|
||||
<div class="upload-button">
|
||||
<span v-if="!formData.projectImage">点击上传图片</span>
|
||||
<span v-else class="file-name">已选择:{{ fileName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">结算周期(天)</span>
|
||||
<input
|
||||
v-model.number="formData.projectSettlementCycle"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input-field"
|
||||
required
|
||||
placeholder="请输入结算周期"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">最大推广人数</span>
|
||||
<input
|
||||
v-model.number="formData.maxPromoterCount"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input-field"
|
||||
required
|
||||
placeholder="请输入最大人数"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">项目状态</span>
|
||||
<div class="select-wrapper">
|
||||
<select
|
||||
v-model="formData.projectStatus"
|
||||
class="select-field"
|
||||
>
|
||||
<option value="running">运行中</option>
|
||||
<option value="full">人数已满</option>
|
||||
<option value="paused">已暂停</option>
|
||||
</select>
|
||||
<div class="select-arrow">▼</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">项目简介</span>
|
||||
<div class="relative">
|
||||
<textarea
|
||||
v-model="formData.projectDescription"
|
||||
class="textarea-field"
|
||||
placeholder="请输入项目简介..."
|
||||
maxlength="30"
|
||||
></textarea>
|
||||
<div class="absolute right-2 bottom-2 text-xs text-gray-500">
|
||||
{{ formData.projectDescription.length }}/30
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rich-text-container">
|
||||
<div class="rich-text-columns">
|
||||
|
||||
<div class="rich-text-group">
|
||||
<span class="label-text">结算说明</span>
|
||||
<RichTextEditor
|
||||
v-model="formData.settlementDesc"
|
||||
:disable="false"
|
||||
@content-change="(html:any) => formData.settlementDesc = html"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rich-text-group">
|
||||
<span class="label-text">项目说明</span>
|
||||
<RichTextEditor
|
||||
v-model="formData.projectDesc"
|
||||
:disable="false"
|
||||
@content-change="(html:any) => formData.projectDesc = html"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rich-text-group">
|
||||
<span class="label-text">项目流程</span>
|
||||
<RichTextEditor
|
||||
v-model="formData.projectFlow"
|
||||
:disable="false"
|
||||
@content-change="(html:any) => formData.projectFlow = html"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rich-text-group">
|
||||
<span class="label-text">申请推广码说明</span>
|
||||
<RichTextEditor
|
||||
v-model="formData.applyPromoCodeDesc"
|
||||
:disable="false"
|
||||
@content-change="(html:any) => formData.applyPromoCodeDesc = html"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-button">
|
||||
<span>立即创建</span>
|
||||
<div class="button-sparkles"></div>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted, nextTick,ref } from 'vue';
|
||||
import RichTextEditor from '../components/RichTextEditor.vue';
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import router from "../../router";
|
||||
|
||||
|
||||
interface ProjectForm {
|
||||
projectName: string;
|
||||
projectImage: string;
|
||||
projectSettlementCycle: number;
|
||||
maxPromoterCount: number;
|
||||
projectStatus: 'running' | 'full' | 'paused';
|
||||
projectDescription: string;
|
||||
settlementDesc: string;
|
||||
projectDesc: string;
|
||||
projectFlow: string;
|
||||
applyPromoCodeDesc: string;
|
||||
}
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
const handleFileUpload = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
// 重命名局部变量避免冲突(修复错误❷)
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('biz', 'project');
|
||||
uploadFormData.append('file', file);
|
||||
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res: any = await myAxios.post('/file/upload', uploadFormData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': storedToken
|
||||
}
|
||||
});
|
||||
|
||||
if (res.code === 1) {
|
||||
// 正确访问响应式对象
|
||||
formData.projectImage = res.data;
|
||||
fileName.value = file.name;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
alert('文件上传失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// 添加文件名响应式变量
|
||||
const fileName = ref('');
|
||||
|
||||
// 响应式表单数据
|
||||
const formData = reactive<ProjectForm>({
|
||||
projectName: '',
|
||||
projectSettlementCycle: 2,
|
||||
maxPromoterCount: 200,
|
||||
projectStatus: 'running',
|
||||
projectDescription: '',
|
||||
settlementDesc: '',
|
||||
projectDesc: '',
|
||||
projectFlow: '',
|
||||
applyPromoCodeDesc: '',
|
||||
projectImage: ''
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
// 初始处理
|
||||
nextTick(() => {
|
||||
// 为手机预览添加初始样式
|
||||
const phoneScreen = document.querySelector('.phone-screen');
|
||||
if (phoneScreen) {
|
||||
// 添加阴影和动画效果
|
||||
phoneScreen.classList.add('animate-fade-in');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
|
||||
const res:any = await myAxios.post(`/project/add`,
|
||||
JSON.stringify(formData),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': storedToken
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
alert('项目创建成功!');
|
||||
// router.back();
|
||||
router.push('/project')
|
||||
Object.assign(formData, {
|
||||
projectName: '',
|
||||
projectSettlementCycle: 2,
|
||||
maxPromoterCount: 200,
|
||||
projectStatus: 'running',
|
||||
projectDescription: '',
|
||||
settlementDesc: '',
|
||||
projectDesc: '',
|
||||
projectFlow: '',
|
||||
applyPromoCodeDesc: '',
|
||||
projectImage: ''
|
||||
});
|
||||
} else {
|
||||
alert(`创建失败:${res.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modern-form {
|
||||
max-width: 90%;
|
||||
margin: 2rem auto;
|
||||
padding: 2.5rem;
|
||||
background: white;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
display: block;
|
||||
margin-bottom: 0.6rem;
|
||||
color: #4a5568;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-field,
|
||||
.select-wrapper,
|
||||
.textarea-field {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1.2rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-field:focus,
|
||||
.select-wrapper:focus-within,
|
||||
.textarea-field:focus {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
z-index: 2; /* 确保文件输入在按钮上方 */
|
||||
}
|
||||
|
||||
|
||||
.upload-button {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-button:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.select-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.select-field {
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #94a3b8;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.textarea-field {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.button-sparkles {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255,255,255,0.4);
|
||||
border-radius: 50%;
|
||||
animation: sparkle 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% { transform: scale(0) translate(0,0); }
|
||||
50% { transform: scale(1) translate(100px, -50px); }
|
||||
100% { transform: scale(0) translate(200px, -100px); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modern-form {
|
||||
padding: 1.5rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.rich-text-container {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.rich-text-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr)); /* 修正列宽分配 */
|
||||
gap: 1rem; /* 减小间距 */
|
||||
align-items: start; /* 顶部对齐 */
|
||||
}
|
||||
|
||||
.rich-text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem; /* 减小标签与编辑器间距 */
|
||||
height: 100%; /* 保持等高布局 */
|
||||
|
||||
}
|
||||
.rich-text-group:focus-within {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 1440px) {
|
||||
.rich-text-columns {
|
||||
gap: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.rich-text-columns {
|
||||
grid-template-columns: repeat(2, 1fr); /* 更早切换为2列 */
|
||||
gap: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.rich-text-columns {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.rich-text-group {
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 320px;
|
||||
height: auto;
|
||||
flex-grow: 1;
|
||||
/* 新增焦点状态 */
|
||||
&:focus-within {
|
||||
border-color: #6366f1 !important;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 调整标签与编辑器间距 */
|
||||
.rich-text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
/* 新增标签焦点状态联动 */
|
||||
&:focus-within .label-text {
|
||||
color: #6366f1;
|
||||
transition: color 0.3s ease;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/* 保持与其他输入组件的一致性 */
|
||||
.rich-text-group .label-text {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #3b4151;
|
||||
padding-left: 0.2rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
|
||||
/* 手机预览样式 */
|
||||
.phone-preview-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.phone-frame {
|
||||
position: relative;
|
||||
width: 320px;
|
||||
height: 640px;
|
||||
background: #000;
|
||||
border-radius: 40px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 0 12px #1f1f1f, 0 0 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.phone-header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 180px;
|
||||
height: 25px;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.phone-camera {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 20px;
|
||||
transform: translateY(-50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #15294c;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.phone-speaker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
background: #1f1f1f;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.phone-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f5f5f5;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.phone-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.phone-home-button {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #1f1f1f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.phone-project-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.phone-project-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.phone-project-image {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
border-radius: 12px;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.phone-project-desc {
|
||||
font-size: 0.9rem;
|
||||
color: #4a5568;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.phone-sections {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.phone-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.phone-section-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: #2c3e50;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
.phone-section-content {
|
||||
font-size: 0.9rem;
|
||||
color: #4a5568;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.phone-section-content * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.phone-section-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.phone-section-content h1,
|
||||
.phone-section-content h2,
|
||||
.phone-section-content h3 {
|
||||
color: #2c3e50;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.phone-section-content p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.phone-section-content ul,
|
||||
.phone-section-content ol {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.file-upload {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-button:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
431
src/view/project/addprojectNotice.vue
Normal file
431
src/view/project/addprojectNotice.vue
Normal file
@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<form @submit.prevent="handleSubmit" class="modern-form">
|
||||
<h2 class="form-title">新建项目通知</h2>
|
||||
|
||||
<div class="form-grid">
|
||||
<!-- 左列 - 手机预览区域 -->
|
||||
<div class="form-column phone-preview-container">
|
||||
<div class="phone-frame">
|
||||
<div class="phone-header">
|
||||
<div class="phone-camera"></div>
|
||||
<div class="phone-speaker"></div>
|
||||
</div>
|
||||
|
||||
<div class="phone-screen">
|
||||
<div class="phone-content">
|
||||
<!-- 通知预览 -->
|
||||
<div class="phone-notification">
|
||||
<h2 class="notification-title">{{ formData.notificationTitle }}</h2>
|
||||
<div class="notification-content" v-html="formData.notificationContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="phone-home-button"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右列 - 编辑区域 -->
|
||||
<div class="form-column">
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">通知标题</span>
|
||||
<input
|
||||
v-model="formData.notificationTitle"
|
||||
type="text"
|
||||
class="input-field"
|
||||
required
|
||||
placeholder="请输入通知标题"
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="label-text">关联项目ID</span>
|
||||
|
||||
<input
|
||||
v-model.number="formData.projectId"
|
||||
type="number"
|
||||
min="1"
|
||||
class="input-field"
|
||||
required
|
||||
placeholder="请输入项目ID"
|
||||
readonly
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- 修改后的富文本编辑器 -->
|
||||
<div class="rich-text-container">
|
||||
<div class="rich-text-columns">
|
||||
<div class="rich-text-group">
|
||||
<span class="label-text">通知内容</span>
|
||||
<RichTextEditor
|
||||
v-model="formData.notificationContent"
|
||||
:disable="false"
|
||||
@content-change="(html:any) => formData.notificationContent = html"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="submit-button">
|
||||
<span>发布通知</span>
|
||||
<div class="button-sparkles"></div>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, onMounted } from 'vue';
|
||||
import router from "../../router";
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import RichTextEditor from '../components/RichTextEditor.vue';
|
||||
import {useRoute} from "vue-router";
|
||||
|
||||
interface NotificationForm {
|
||||
notificationTitle: string;
|
||||
notificationContent: string;
|
||||
projectId: string | number; // 关联项目ID,接收数值或字符串
|
||||
}
|
||||
|
||||
const formData = reactive<NotificationForm>({
|
||||
notificationTitle: '',
|
||||
notificationContent: '',
|
||||
projectId: 0 // 初始化值
|
||||
});
|
||||
|
||||
const route = useRoute(); // 获取路由参数
|
||||
|
||||
onMounted(() => {
|
||||
// 从路由参数中获取projectId并填充到表单
|
||||
if (typeof route.query.projectId === 'string') {
|
||||
formData.projectId = Number(route.query.projectId); // 转换为数字
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res: any = await myAxios.post(
|
||||
'/projectNotification/add',
|
||||
JSON.stringify(formData),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': storedToken
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
alert('通知发布成功!');
|
||||
router.back();
|
||||
Object.assign(formData, {
|
||||
notificationTitle: '',
|
||||
notificationContent: '',
|
||||
projectId: ''
|
||||
});
|
||||
} else {
|
||||
alert(`发布失败:${res.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
alert('通知发布失败,请检查网络或数据格式');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 保留原有样式,添加富文本编辑器样式 */
|
||||
.phone-notification {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
font-size: 1.2rem;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.notification-content {
|
||||
font-size: 0.95rem;
|
||||
color: #4a5568;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.notification-content >>> img {
|
||||
max-width: 100%;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.modern-form {
|
||||
max-width: 90%;
|
||||
margin: 2rem auto;
|
||||
padding: 2.5rem;
|
||||
background: white;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
text-align: center;
|
||||
font-size: 1.8rem;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
display: block;
|
||||
margin-bottom: 0.6rem;
|
||||
color: #4a5568;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.input-field{
|
||||
width: 100%;
|
||||
padding: 0.8rem 1.2rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input-field:focus{
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-top: 1.5rem;
|
||||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.75rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.button-sparkles {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(255,255,255,0.4);
|
||||
border-radius: 50%;
|
||||
animation: sparkle 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% { transform: scale(0) translate(0,0); }
|
||||
50% { transform: scale(1) translate(100px, -50px); }
|
||||
100% { transform: scale(0) translate(200px, -100px); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modern-form {
|
||||
padding: 1.5rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 新增富文本编辑器样式 */
|
||||
.rich-text-container {
|
||||
margin-top: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rich-text-columns {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.rich-text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 320px;
|
||||
height: auto;
|
||||
padding: 0.8rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.rich-text-group:focus-within {
|
||||
border-color: #6366f1 !important;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.rich-text-group .label-text {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #3b4151;
|
||||
padding-left: 0.2rem;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.rich-text-group:focus-within .label-text {
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
/* 手机预览样式 */
|
||||
.phone-preview-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.phone-frame {
|
||||
position: relative;
|
||||
width: 320px;
|
||||
height: 640px;
|
||||
background: #000;
|
||||
border-radius: 40px;
|
||||
padding: 15px;
|
||||
box-shadow: 0 0 0 12px #1f1f1f, 0 0 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.phone-header {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 180px;
|
||||
height: 25px;
|
||||
background: #000;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.phone-camera {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 20px;
|
||||
transform: translateY(-50%);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #15294c;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.phone-speaker {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
background: #1f1f1f;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.phone-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f5f5f5;
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.phone-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
.phone-home-button {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
background: #1f1f1f;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.phone-section-content * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.phone-section-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.phone-section-content h1,
|
||||
.phone-section-content h2,
|
||||
.phone-section-content h3 {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.phone-section-content p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.phone-section-content ul,
|
||||
.phone-section-content ol {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
692
src/view/project/moneyDetail.vue
Normal file
692
src/view/project/moneyDetail.vue
Normal file
@ -0,0 +1,692 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import myAxios from "../../api/myAxios";
|
||||
import {message} from "ant-design-vue";
|
||||
import { reactive } from 'vue';
|
||||
import { SmileOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '项目明细ID',
|
||||
dataIndex: 'id',
|
||||
width: 50,
|
||||
key: 'id',
|
||||
fixed: 'left',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目明细名称',
|
||||
dataIndex: 'projectDetailName',
|
||||
key: 'projectDetailName',
|
||||
width: 80,
|
||||
fixed: 'left',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目结算价',
|
||||
dataIndex: 'projectSettlementPrice',
|
||||
width: 30,
|
||||
key: 'projectSettlementPrice',
|
||||
fixed: 'left',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目最小结算价',
|
||||
dataIndex: 'projectMinSettlementPrice',
|
||||
width: 30,
|
||||
key: 'projectMinSettlementPrice',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '最大抽成比例',
|
||||
dataIndex: 'maxCommissionRate',
|
||||
width: 40,
|
||||
key: 'maxCommissionRate',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目ID',
|
||||
dataIndex: 'projectId',
|
||||
key: 'projectId',
|
||||
width: 50,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 50,
|
||||
align: 'center'
|
||||
}
|
||||
];
|
||||
|
||||
interface ProjectDetail {
|
||||
id: number;
|
||||
projectDetailName: string;
|
||||
projectSettlementPrice: number;
|
||||
projectMinSettlementPrice: number; // 修正接口字段
|
||||
maxCommissionRate: number;
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = ref<string | number>("");
|
||||
const tableData = ref<ProjectDetail[]>([]); // 改为数组存储
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const searchId = ref(""); // 新增搜索ID绑定
|
||||
|
||||
// 新增变量保存原始数据
|
||||
const originalTableData = ref<ProjectDetail[]>([]);
|
||||
|
||||
// 修改 getMoneyDetail 方法
|
||||
const getMoneyDetail = async (id: string | number) => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
try {
|
||||
loading.value = true;
|
||||
const response:any = await myAxios.post(
|
||||
"/projectDetail/query/pid",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(response)
|
||||
if (response.code === 1) {
|
||||
tableData.value = response.data;
|
||||
// 保存原始数据
|
||||
originalTableData.value = response.data;
|
||||
} else {
|
||||
error.value = "获取项目详情失败";
|
||||
tableData.value = [];
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = "数据加载失败,请重试";
|
||||
tableData.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 修改搜索处理方法 - 前端过滤
|
||||
const handleIdSearch = (value: string) => {
|
||||
if (!value.trim()) {
|
||||
// 如果搜索值为空,显示所有数据
|
||||
tableData.value = [...originalTableData.value];
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Number(value);
|
||||
if (isNaN(id)) {
|
||||
message.warning("ID必须为数字");
|
||||
return;
|
||||
}
|
||||
|
||||
// 前端过滤逻辑
|
||||
const filtered = originalTableData.value.filter(item => item.id === id);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
message.warning("未找到匹配的项目明细");
|
||||
tableData.value = [];
|
||||
} else {
|
||||
tableData.value = filtered;
|
||||
}
|
||||
};
|
||||
|
||||
// 修改重置方法
|
||||
const reset = () => {
|
||||
searchId.value = "";
|
||||
// 重置时显示所有原始数据
|
||||
tableData.value = [...originalTableData.value];
|
||||
};
|
||||
|
||||
// 新增根据明细ID查询的方法
|
||||
const queryDetailById = async (id: string | number) => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
try {
|
||||
loading.value = true;
|
||||
const response:any = await myAxios.post(
|
||||
"/projectDetail/queryById", // 使用文档中的接口路径
|
||||
{ id }, // 请求参数
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(response.data)
|
||||
if (response.code === 1) {
|
||||
tableData.value = [response.data];
|
||||
} else {
|
||||
message.error(response.message || "查询失败");
|
||||
tableData.value = [];
|
||||
}
|
||||
} catch (err) {
|
||||
message.error("查询请求失败");
|
||||
tableData.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化逻辑
|
||||
if (typeof route.query.id === "string") {
|
||||
projectId.value = route.query.id;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (projectId.value) {
|
||||
getMoneyDetail(projectId.value);
|
||||
}
|
||||
});
|
||||
|
||||
const deleteMoneyDetail = async (id: number) => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
// 修正接口路径为项目明细的删除接口
|
||||
const res: any = await myAxios.post(
|
||||
"/projectDetail/delete", // 修改这里
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
'AfterScript': 'required-script'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('删除成功');
|
||||
if (projectId.value) {
|
||||
await getMoneyDetail(projectId.value);
|
||||
}
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
message.error('删除操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
const drawerVisible = ref(false);
|
||||
const formState = reactive({
|
||||
id: 0,
|
||||
projectDetailName: '',
|
||||
projectSettlementPrice: 0,
|
||||
projectMinSettlementPrice: 0,
|
||||
maxCommissionRate: 0,
|
||||
projectId: 0
|
||||
});
|
||||
|
||||
// 修改updateMoneyDetail方法
|
||||
// const updateMoneyDetail = async (id: number) => {
|
||||
// try {
|
||||
// // 先获取明细数据
|
||||
// const storedToken = localStorage.getItem('token');
|
||||
// const res:any = await myAxios.post(
|
||||
// "/projectDetail/queryById",
|
||||
// { id },
|
||||
// {
|
||||
// headers: {
|
||||
// Authorization: storedToken,
|
||||
// "Content-Type": "application/json"
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
// console.log(res)
|
||||
// if (res.code === 1) {
|
||||
// // 映射字段到表单
|
||||
// const detail = res.data;
|
||||
// Object.assign(formState, {
|
||||
// id: detail.id,
|
||||
// projectDetailName: detail.projectDetailName,
|
||||
// projectSettlementPrice: detail.projectSettlementPrice,
|
||||
// projectMinSettlementPrice: detail.projectMinSettlementPrice,
|
||||
// maxCommissionRate: detail.maxCommissionRate,
|
||||
// projectId: detail.projectId
|
||||
// });
|
||||
// drawerVisible.value = true;
|
||||
// }
|
||||
// } catch (error) {
|
||||
// message.error('获取明细数据失败');
|
||||
// }
|
||||
// };
|
||||
//
|
||||
|
||||
// 新增表单相关状态
|
||||
const addDrawerVisible = ref(false);
|
||||
const addFormState = reactive({
|
||||
projectDetailName: '',
|
||||
projectSettlementPrice: 0,
|
||||
projectMinSettlementPrice: 0, // 注意字段名称需要与接口一致
|
||||
maxCommissionRate: 0,
|
||||
projectId: 0
|
||||
});
|
||||
|
||||
// 打开新增表单
|
||||
const openAddDrawer = () => {
|
||||
addFormState.projectId = Number(projectId.value); // 关联当前项目
|
||||
addDrawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 提交新增请求
|
||||
const handleAddSubmit = async () => {
|
||||
if (addFormState.projectMinSettlementPrice >= addFormState.projectSettlementPrice) {
|
||||
message.error("最小结算价必须小于结算价");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
addFormState.projectSettlementPrice === 0 ||
|
||||
addFormState.projectMinSettlementPrice === 0 ||
|
||||
addFormState.maxCommissionRate === 0
|
||||
) {
|
||||
message.error("结算价、最小结算价和抽成比例不能为0");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/projectDetail/add",
|
||||
{
|
||||
projectDetailName: addFormState.projectDetailName,
|
||||
projectSettlementPrice: addFormState.projectSettlementPrice,
|
||||
projectMinSettlementPrice: addFormState.projectMinSettlementPrice, // 字段映射
|
||||
maxCommissionRate: addFormState.maxCommissionRate,
|
||||
projectId: addFormState.projectId
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('新增成功');
|
||||
addDrawerVisible.value = false;
|
||||
await getMoneyDetail(projectId.value); // 刷新列表
|
||||
// 重置表单
|
||||
Object.assign(addFormState, {
|
||||
projectDetailName: '',
|
||||
projectSettlementPrice: 0,
|
||||
projectMinSettlementPrice: 0,
|
||||
maxCommissionRate: 0,
|
||||
projectId: Number(projectId.value)
|
||||
});
|
||||
} else {
|
||||
message.error(res.message || '新增失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('新增请求失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 修改模板按钮绑定
|
||||
const goAddProject = () => {
|
||||
openAddDrawer();
|
||||
};
|
||||
|
||||
// 修改handleSubmit方法
|
||||
const handleSubmit = async () => {
|
||||
if (formState.projectMinSettlementPrice >= formState.projectSettlementPrice) {
|
||||
message.error("最小结算价必须小于结算价");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
formState.projectSettlementPrice === 0 ||
|
||||
formState.projectMinSettlementPrice === 0 ||
|
||||
formState.maxCommissionRate === 0
|
||||
) {
|
||||
message.error("结算价、最小结算价和抽成比例不能为0");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/projectDetail/update",
|
||||
{
|
||||
// 根据接口文档调整字段映射
|
||||
id: formState.id,
|
||||
projectDetailName: formState.projectDetailName,
|
||||
projectSettlementPrice: formState.projectSettlementPrice,
|
||||
projectMinSettlementPrice: formState.projectMinSettlementPrice,
|
||||
maxCommissionRate: formState.maxCommissionRate,
|
||||
projectId: formState.projectId
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log('接口响应:', res); // 添加详细日志
|
||||
|
||||
if (res.code === 1) { // 注意响应结构层级
|
||||
message.success('更新成功');
|
||||
drawerVisible.value = false;
|
||||
// 根据当前查看模式刷新
|
||||
if (searchId.value) {
|
||||
await queryDetailById(formState.id);
|
||||
} else {
|
||||
await getMoneyDetail(projectId.value);
|
||||
}
|
||||
} else {
|
||||
message.error(res.message || `更新失败,错误码:${res.code}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('完整错误信息:', error); // 输出完整错误对象
|
||||
message.error(`更新失败'}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
//返回上一级
|
||||
const router = useRouter();
|
||||
|
||||
// 返回上一级方法
|
||||
const goBack = () => {
|
||||
// router.go(-1); // 返回上一页
|
||||
router.push('/project')
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<a-form layout="inline">
|
||||
<a-space>
|
||||
<a-form-item label="ID">
|
||||
<a-input-search
|
||||
style="width: 300px"
|
||||
placeholder="请输入项目明细ID"
|
||||
enter-button
|
||||
@search="handleIdSearch"
|
||||
v-model:value="searchId"
|
||||
type="number"
|
||||
class="custom-search"
|
||||
min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-button class="custom-button" @click="goAddProject">新增项目明细</a-button>
|
||||
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||
<a-button @click="goBack" class="custom-button">返回</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:scroll="{ x: 1500, y: 450 }"
|
||||
:loading="loading"
|
||||
bordered
|
||||
rowKey="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 格式化金额显示 -->
|
||||
<template v-if="column.dataIndex === 'projectSettlementPrice'">
|
||||
¥{{ record.projectSettlementPrice.toFixed(2) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'projectMinSettlementPrice'">
|
||||
¥{{ record.projectMinSettlementPrice.toFixed(2) }}
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'maxCommissionRate'">
|
||||
{{ record.maxCommissionRate}}%
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space :size="8">
|
||||
<a-button
|
||||
size="small"
|
||||
danger
|
||||
@click="deleteMoneyDetail(record.id)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
<!-- <a-button-->
|
||||
<!-- size="small"-->
|
||||
<!-- @click="updateMoneyDetail(record.id)"-->
|
||||
<!-- >-->
|
||||
<!-- 编辑-->
|
||||
<!-- </a-button>-->
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="error" class="error-alert">
|
||||
<span class="error-icon">!</span>
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<!-- 修改编辑抽屉的表单项 -->
|
||||
<a-drawer
|
||||
title="编辑项目明细"
|
||||
placement="right"
|
||||
:visible="drawerVisible"
|
||||
@close="drawerVisible = false"
|
||||
width="600"
|
||||
>
|
||||
|
||||
<a-form
|
||||
:model="formState"
|
||||
layout="vertical"
|
||||
@finish="handleSubmit"
|
||||
>
|
||||
<a-form-item label="项目明细名称" required>
|
||||
<a-input v-model:value="formState.projectDetailName" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目结算价" required>
|
||||
<a-input-number
|
||||
v-model:value="formState.projectSettlementPrice"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目最小结算价" required>
|
||||
<a-input-number
|
||||
v-model:value="formState.projectMinSettlementPrice"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="最大抽成比例" required>
|
||||
<a-input-number
|
||||
v-model:value="formState.maxCommissionRate"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button class="custom-button" html-type="submit">提交</a-button>
|
||||
<a-button style="margin-left: 10px" @click="drawerVisible = false">取消</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
<!-- 修改新增抽屉的表单项 -->
|
||||
<a-drawer
|
||||
title="新增项目明细"
|
||||
placement="right"
|
||||
:visible="addDrawerVisible"
|
||||
@close="addDrawerVisible = false"
|
||||
width="600"
|
||||
>
|
||||
<a-alert message="项目结算价必须大于最小结算价" type="warning" show-icon class="addAlert">
|
||||
<template #icon><smile-outlined /></template>
|
||||
</a-alert>
|
||||
<a-form
|
||||
:model="addFormState"
|
||||
layout="vertical"
|
||||
@finish="handleAddSubmit"
|
||||
>
|
||||
<a-form-item label="明细名称" required>
|
||||
<a-input v-model:value="addFormState.projectDetailName" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="结算价 (¥)" required>
|
||||
<a-input-number
|
||||
v-model:value="addFormState.projectSettlementPrice"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="最小结算价 (¥)" required>
|
||||
<a-input-number
|
||||
v-model:value="addFormState.projectMinSettlementPrice"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="抽成比例 (%)" required>
|
||||
<a-input-number
|
||||
v-model:value="addFormState.maxCommissionRate"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
:max="100"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button class="custom-button" html-type="submit">提交</a-button>
|
||||
<a-button style="margin-left: 10px" @click="addDrawerVisible = false">
|
||||
取消
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
padding: 1rem;
|
||||
background: #ffe3e3;
|
||||
color: #ff4444;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
display: inline-block;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
border-radius: 50%;
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead) {
|
||||
background-color: #fafafa !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-row:hover) {
|
||||
background-color: #fafafa !important;
|
||||
}
|
||||
|
||||
|
||||
/*橙色按钮*/
|
||||
|
||||
.custom-button {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button:hover,
|
||||
.custom-button:focus {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button[disabled] {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
opacity: 0.6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 危险按钮样式 */
|
||||
.custom-button.ant-btn-dangerous {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-button.ant-btn-dangerous:hover,
|
||||
.custom-button.ant-btn-dangerous:focus {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
/* 保持原有的其他样式不变 */
|
||||
.search-box {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button) {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button:hover),
|
||||
.custom-search :deep(.ant-input-search-button:focus) {
|
||||
background-color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 保持输入框原有样式不变 */
|
||||
.custom-search :deep(.ant-input) {
|
||||
border-right-color: #ffa940;
|
||||
}
|
||||
|
||||
|
||||
/* 调整分页器位置 */
|
||||
:deep(.ant-table-pagination.ant-pagination) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.addAlert {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
376
src/view/project/noticeDetail.vue
Normal file
376
src/view/project/noticeDetail.vue
Normal file
@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div class="notification-detail-container">
|
||||
<a-spin :spinning="loading" tip="加载中...">
|
||||
<a-card v-if="projectNotice.id" class="notification-card">
|
||||
<a-button class="custom-button" @click="goBack">返回</a-button>
|
||||
<a-button v-if="!isEditing" class="custom-button" @click="enterEditMode" style="margin-left: 10px">编辑</a-button>
|
||||
<template v-else>
|
||||
<a-button class="custom-button" @click="saveNotice" style="margin-left: 10px">保存</a-button>
|
||||
<a-button class="custom-button" @click="cancelEdit" style="margin-left: 10px">取消</a-button>
|
||||
</template>
|
||||
|
||||
<div class="header">
|
||||
<h1 v-if="!isEditing" class="title">{{ projectNotice.notificationTitle }}</h1>
|
||||
<a-input v-else v-model:value="editableNotice.notificationTitle" placeholder="请输入通知标题" />
|
||||
|
||||
<div class="meta-info">
|
||||
<span class="project-id">项目ID: {{ projectNotice.projectId }}</span>
|
||||
<span class="create-time">创建时间: {{ projectNotice.createTime }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="content-header">
|
||||
<notification-outlined class="icon" />
|
||||
<h2>通知内容</h2>
|
||||
</div>
|
||||
|
||||
<!-- 富文本显示和编辑区域 -->
|
||||
<div v-if="!isEditing" class="content-html" v-html="projectNotice.notificationContent"></div>
|
||||
<div v-else class="rich-text-group">
|
||||
<RichTextEditor
|
||||
v-model="editableNotice.notificationContent"
|
||||
:disable="false"
|
||||
@content-change="(html:any) => editableNotice.notificationContent = html"
|
||||
class="rich-text-editor"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider"></div>
|
||||
</a-card>
|
||||
|
||||
<a-empty v-else-if="!loading" :description="errorMessage || '未找到通知详情'" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { message } from "ant-design-vue";
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import { NotificationOutlined } from '@ant-design/icons-vue';
|
||||
import RichTextEditor from '../components/RichTextEditor.vue'; // 引入富文本编辑器
|
||||
import '@vueup/vue-quill/dist/vue-quill.snow.css'; // 引入富文本编辑器样式
|
||||
|
||||
interface NotificationDetail {
|
||||
id: string; // 改为 string 类型
|
||||
notificationTitle: string;
|
||||
projectId: string; // 改为 string 类型
|
||||
createTime: string;
|
||||
notificationContent: string;
|
||||
}
|
||||
|
||||
const projectNotice = ref<Partial<NotificationDetail>>({
|
||||
id: '',
|
||||
notificationTitle: '',
|
||||
projectId: '',
|
||||
createTime: '',
|
||||
notificationContent: ''
|
||||
});
|
||||
|
||||
|
||||
const editableNotice = ref<Partial<NotificationDetail>>({});
|
||||
const isEditing = ref(false);
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const loading = ref(false);
|
||||
const errorMessage = ref("");
|
||||
|
||||
// 进入编辑模式
|
||||
const enterEditMode = () => {
|
||||
// 创建可编辑数据的副本
|
||||
editableNotice.value = {
|
||||
...projectNotice.value
|
||||
};
|
||||
isEditing.value = true;
|
||||
};
|
||||
|
||||
// 取消编辑
|
||||
const cancelEdit = () => {
|
||||
isEditing.value = false;
|
||||
};
|
||||
|
||||
// 保存编辑 - 根据接口要求修改
|
||||
// 修改保存函数
|
||||
const saveNotice = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const storedToken = localStorage.getItem('token');
|
||||
|
||||
// 创建符合接口要求的JSON对象
|
||||
const requestData = {
|
||||
id: editableNotice.value.id,
|
||||
notificationTitle: editableNotice.value.notificationTitle,
|
||||
notificationContent: editableNotice.value.notificationContent,
|
||||
projectId: editableNotice.value.projectId
|
||||
};
|
||||
|
||||
// 发送JSON格式的数据
|
||||
const res:any = await myAxios.post(
|
||||
"/projectNotification/update",
|
||||
requestData, // 直接发送JSON对象
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
'Content-Type': 'application/json' // 关键修改:使用JSON格式
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('更新成功');
|
||||
// 更新本地数据并退出编辑模式
|
||||
projectNotice.value = { ...editableNotice.value };
|
||||
isEditing.value = false;
|
||||
} else {
|
||||
message.error(res.message || '更新失败');
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('更新通知失败:', error);
|
||||
// 改进错误处理
|
||||
const errMsg = error.response?.data?.message ||
|
||||
error.message ||
|
||||
'更新通知失败,请检查网络';
|
||||
message.error(errMsg);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const fetchNotificationDetail = async (id: number) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
errorMessage.value = "";
|
||||
const storedToken = localStorage.getItem('token');
|
||||
|
||||
const res:any = await myAxios.post(
|
||||
"/projectNotification/queryById",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1 && res.data) {
|
||||
projectNotice.value = {
|
||||
id: res.data.id,
|
||||
notificationTitle: res.data.notificationTitle,
|
||||
notificationContent: res.data.notificationContent,
|
||||
projectId: res.data.projectId,
|
||||
createTime: res.data.createTime
|
||||
};
|
||||
} else {
|
||||
errorMessage.value = res.message || '获取通知详情失败';
|
||||
message.error(errorMessage.value);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('获取通知详情失败:', error);
|
||||
errorMessage.value = error.response?.message ||
|
||||
error.message ||
|
||||
'获取通知详情失败,请检查网络';
|
||||
message.error(errorMessage.value);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const id = Number(route.params.id || route.query.id);
|
||||
if (id && !isNaN(id)) {
|
||||
fetchNotificationDetail(id);
|
||||
} else {
|
||||
errorMessage.value = '无效的通知ID';
|
||||
message.error('无效的通知ID');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 基本样式保持不变 */
|
||||
.notification-detail-container {
|
||||
width: 100%;
|
||||
padding: 24px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.notification-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 24px 24px 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #86909c;
|
||||
font-size: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #f0f0f0;
|
||||
margin: 0 24px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content-header h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
margin: 0 0 0 8px;
|
||||
color: #1d2129;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 20px;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 富文本样式 */
|
||||
.rich-text-group {
|
||||
border: 2px solid #e2e8f0;
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
min-height: 320px;
|
||||
height: auto;
|
||||
flex-grow: 1;
|
||||
&:focus-within {
|
||||
border-color: #6366f1 !important;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.rich-text-editor {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
/* 富文本内容显示样式 */
|
||||
.content-html {
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
color: #4e5969;
|
||||
}
|
||||
|
||||
.content-html * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.content-html img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.content-html h1,
|
||||
.content-html h2,
|
||||
.content-html h3 {
|
||||
color: #2c3e50;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content-html p {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.content-html ul,
|
||||
.content-html ol {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notification-detail-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.rich-text-group {
|
||||
min-height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
/*橙色按钮*/
|
||||
|
||||
.custom-button {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button:hover,
|
||||
.custom-button:focus {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button[disabled] {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
opacity: 0.6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 危险按钮样式 */
|
||||
.custom-button.ant-btn-dangerous {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-button.ant-btn-dangerous:hover,
|
||||
.custom-button.ant-btn-dangerous:focus {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
</style>
|
||||
564
src/view/project/project.vue
Normal file
564
src/view/project/project.vue
Normal file
@ -0,0 +1,564 @@
|
||||
<template>
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<a-form layout="inline">
|
||||
<a-space>
|
||||
<a-form-item label="项目名称">
|
||||
<a-input-search
|
||||
style="width: 300px"
|
||||
placeholder="请输入项目名称"
|
||||
enter-button
|
||||
@search="handleProjectSearch"
|
||||
v-model:value="searchProjectName"
|
||||
class="custom-search"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-button class="custom-button" @click="goAddProject">新增项目</a-button>
|
||||
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据-->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:scroll="{ x: 2000, y: 550 }"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
bordered
|
||||
rowKey="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 项目图片 -->
|
||||
<template v-if="column.key === 'projectImage'">
|
||||
<a-avatar :src="downLoadImage+record.projectImage" shape="square" :size="64"/>
|
||||
</template>
|
||||
<template v-if="column.key === 'currentPromotionCount'">
|
||||
{{record.currentPromotionCount }}人
|
||||
</template>
|
||||
<template v-if="column.key === 'projectSettlementCycle'">
|
||||
T+{{record.projectSettlementCycle}}
|
||||
</template>
|
||||
<template v-if="column.key === 'maxPromoterCount'">
|
||||
{{record.maxPromoterCount}}人
|
||||
</template>
|
||||
<template v-if="column.key === 'projectPrice'">
|
||||
¥{{record.projectPrice }}
|
||||
</template>
|
||||
<!-- 项目状态 -->
|
||||
<template v-if="column.key === 'projectStatus'">
|
||||
<a-tag :color="getStatusColor(record.projectStatus)">
|
||||
{{ getStatusText(record.projectStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 是否上架 -->
|
||||
<template v-if="column.key === 'isShelves'">
|
||||
<a-tag :color="record.isShelves ? 'green' : 'red'">
|
||||
{{ record.isShelves ? '已上架' : '已下架' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space :size="8">
|
||||
<a-tag
|
||||
size="small"
|
||||
:color="record.isShelves ? 'orange' : 'red'"
|
||||
@click="toggleShelves(record)"
|
||||
>
|
||||
{{ record.isShelves ? '去下架' : '去上架' }}
|
||||
</a-tag>
|
||||
<a-button
|
||||
size="small"
|
||||
danger
|
||||
@click="deleteProject(record.id)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="showDetails(record.id)"
|
||||
>
|
||||
详情
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="moneyDetails(record.id)"
|
||||
>
|
||||
项目明细
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="projectNotice(record.id)"
|
||||
>
|
||||
项目通知
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="link"
|
||||
@click="promotionCode(record.id)"
|
||||
>
|
||||
推广码
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||
import router from "../../router";
|
||||
|
||||
|
||||
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
|
||||
const searchProjectName = ref(""); // 改为项目名称搜索参数
|
||||
const searchParams = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
sortField: "id",
|
||||
sortOrder: "ascend",
|
||||
userRole: null,
|
||||
projectName: "" // 新增项目名称查询参数
|
||||
});
|
||||
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: { [key: string]: string } = {
|
||||
running: '项目运行',
|
||||
full: '人数已满',
|
||||
paused: '项目暂停'
|
||||
};
|
||||
return statusMap[status] || '未知状态';
|
||||
};
|
||||
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: { [key: string]: string } = {
|
||||
running: 'blue',
|
||||
full: 'orange',
|
||||
paused: 'red'
|
||||
};
|
||||
return colorMap[status] || 'gray';
|
||||
};
|
||||
|
||||
//用户表
|
||||
const columns = [
|
||||
{
|
||||
title: '项目ID',
|
||||
dataIndex: 'id',
|
||||
width: 10,
|
||||
key: 'id',
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
|
||||
{
|
||||
title: '项目图片',
|
||||
dataIndex: 'projectImage',
|
||||
key: 'projectImage',
|
||||
width: 10,
|
||||
fixed: 'left',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'projectName',
|
||||
width: 15,
|
||||
key: 'projectName',
|
||||
fixed: 'left',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目价格',
|
||||
dataIndex: 'projectPrice',
|
||||
width: 20,
|
||||
key: 'projectPrice',
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '当前推广人数',
|
||||
dataIndex: 'currentPromotionCount',
|
||||
width: 20,
|
||||
key: 'currentPromotionCount',
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '结算周期',
|
||||
dataIndex: 'projectSettlementCycle',
|
||||
key: 'projectSettlementCycle',
|
||||
width: 10,
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '最大推广人数',
|
||||
dataIndex: 'maxPromoterCount',
|
||||
key: 'maxPromoterCount',
|
||||
width: 20,
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '项目状态',
|
||||
dataIndex: 'projectStatus',
|
||||
key: 'projectStatus',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '是否上架',
|
||||
dataIndex: 'isShelves',
|
||||
key: 'isShelves',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 40,
|
||||
align: 'center'
|
||||
}
|
||||
];
|
||||
// 新增上架/下架操作
|
||||
const toggleShelves = async (record: any) => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res: any = await myAxios.post(
|
||||
"/project/shelves",
|
||||
{
|
||||
id: record.id,
|
||||
},
|
||||
{ headers: { Authorization: storedToken } }
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('状态更新成功');
|
||||
// 使用 Object.assign 触发响应式更新
|
||||
Object.assign(record, { isShelves: !record.isShelves });
|
||||
// 或重新获取数据(推荐)
|
||||
await getProjectList();
|
||||
}else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
message.error('状态更新失败');
|
||||
}
|
||||
};
|
||||
|
||||
interface ProjectRecord {
|
||||
// 这里根据实际数据结构定义属性
|
||||
superHostList?: string[];
|
||||
// 其他属性...
|
||||
}
|
||||
// 项目名称搜索方法
|
||||
const handleProjectSearch = async () => {
|
||||
// 将搜索参数同步到分页查询参数
|
||||
searchParams.value.projectName = searchProjectName.value;
|
||||
searchParams.value.current = 1; // 重置到第一页
|
||||
await getProjectList();
|
||||
};
|
||||
//用户分页查询
|
||||
const getProjectList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (!storedToken) throw new Error('未找到登录信息');
|
||||
|
||||
const res:any = await myAxios.post("/project/page",
|
||||
{
|
||||
...searchParams.value,
|
||||
projectName: searchParams.value.projectName
|
||||
},
|
||||
{ headers: { Authorization: storedToken } }
|
||||
);
|
||||
console.log(res)
|
||||
if (res.code === 1 && res.data && Array.isArray(res.data.records)) {
|
||||
tableData.value = res.data.records.map((item: ProjectRecord) => ({
|
||||
...item,
|
||||
superUserList: item.superHostList? item.superHostList.join(', ') : '无'
|
||||
}));
|
||||
// 同步总条数到分页组件
|
||||
total.value = res.data.total;
|
||||
pagination.value.total = res.data.total; // 新增此行
|
||||
pagination.value.current = searchParams.value.current; // 同步当前页
|
||||
} else {
|
||||
message.error(res.message || '请求失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("请求失败:", error);
|
||||
message.error('获取数据失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(getProjectList);
|
||||
//分页
|
||||
|
||||
// 分页配置
|
||||
const pagination = ref({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
pageSizeOptions: ['10', '20', '50', '100']
|
||||
});
|
||||
const handleTableChange = (pag: any, _: any, sorter: any) => {
|
||||
// 处理排序参数
|
||||
let sortField = "id"; // 默认排序字段
|
||||
let sortOrder = "ascend"; // 默认排序方式
|
||||
if (sorter.field) {
|
||||
sortField = sorter.field;
|
||||
sortOrder = sorter.order;
|
||||
}
|
||||
searchParams.value = {
|
||||
...searchParams.value,
|
||||
current: pag.current,
|
||||
pageSize: pag.pageSize,
|
||||
sortField: sortField, // 设置排序字段
|
||||
sortOrder: sortOrder
|
||||
};
|
||||
|
||||
// 同步到分页组件
|
||||
pagination.value = {
|
||||
...pagination.value,
|
||||
current: pag.current,
|
||||
pageSize: pag.pageSize
|
||||
};
|
||||
|
||||
getProjectList();
|
||||
};
|
||||
|
||||
// ID查询方法
|
||||
interface Project {
|
||||
id: number;
|
||||
projectName: string;
|
||||
projectImage: string;
|
||||
projectSettlementCycle: number;
|
||||
maxPromoterCount: number;
|
||||
projectStatus: string;
|
||||
projectDescription: string;
|
||||
settlementDesc: string;
|
||||
// 其他可能存在的属性根据实际情况补充
|
||||
}
|
||||
|
||||
const tableData = ref<Project[]>([]);
|
||||
|
||||
|
||||
// 删除项目 - 添加确认弹窗
|
||||
const deleteProject = (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该项目吗?删除后数据将无法恢复!',
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/project/delete",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
'AfterScript': 'required-script'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('删除成功');
|
||||
await getProjectList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
message.error('删除操作失败');
|
||||
}
|
||||
},
|
||||
onCancel() {
|
||||
// 用户点击取消,不做操作
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// 重置按钮
|
||||
const reset = () => {
|
||||
searchProjectName.value = "";
|
||||
searchParams.value = {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
sortField: "id",
|
||||
sortOrder: "ascend",
|
||||
userRole: null,
|
||||
projectName: ""
|
||||
};
|
||||
getProjectList();
|
||||
};
|
||||
|
||||
//去新增项目
|
||||
const goAddProject=()=>{
|
||||
router.push('/addproject')
|
||||
}
|
||||
|
||||
const showDetails=(id:string)=>{
|
||||
router.push({
|
||||
path:'/projectDetail',
|
||||
query:{
|
||||
id:String(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
//去项目明细
|
||||
const moneyDetails=(id:string)=>{
|
||||
router.push({
|
||||
path:'/moneyDetail',
|
||||
query:{
|
||||
id:String(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//去项目通知
|
||||
const projectNotice=(id:number)=>{
|
||||
router.push({
|
||||
path:'/projectNotice',
|
||||
query:{
|
||||
id:String(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//去推广码
|
||||
const promotionCode=(id:number)=>{
|
||||
router.push({
|
||||
path:'/promotionCode',
|
||||
query:{
|
||||
id:String(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.action-btn {
|
||||
padding: 0 8px;
|
||||
}
|
||||
/* 分割线样式 */
|
||||
:deep(.ant-divider-vertical) {
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
height: 1.2em;
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
|
||||
:deep(.ant-descriptions-item-label) {
|
||||
font-weight: 600;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
|
||||
.info-card :deep(.ant-card-head) {
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 角色颜色映射 */
|
||||
:root {
|
||||
--role-user: #87d068;
|
||||
--role-admin: #f50;
|
||||
--role-boss: #722ed1;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
/*橙色按钮*/
|
||||
|
||||
.custom-button {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button:hover,
|
||||
.custom-button:focus {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button[disabled] {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
opacity: 0.6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 危险按钮样式 */
|
||||
.custom-button.ant-btn-dangerous {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-button.ant-btn-dangerous:hover,
|
||||
.custom-button.ant-btn-dangerous:focus {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
/* 保持原有的其他样式不变 */
|
||||
.search-box {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button) {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button:hover),
|
||||
.custom-search :deep(.ant-input-search-button:focus) {
|
||||
background-color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 保持输入框原有样式不变 */
|
||||
.custom-search :deep(.ant-input) {
|
||||
border-right-color: #ffa940;
|
||||
}
|
||||
</style>
|
||||
692
src/view/project/projectDetail.vue
Normal file
692
src/view/project/projectDetail.vue
Normal file
@ -0,0 +1,692 @@
|
||||
<script setup lang="ts">
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import { onMounted, ref } from "vue";
|
||||
import myAxios from "../../api/myAxios";
|
||||
import { message } from "ant-design-vue";
|
||||
import { QuillEditor } from '@vueup/vue-quill';
|
||||
import {Form} from 'ant-design-vue'
|
||||
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||
|
||||
interface ProjectDetail {
|
||||
id: number;
|
||||
projectName: string;
|
||||
projectImage: string;
|
||||
projectSettlementCycle: number;
|
||||
maxPromoterCount: number;
|
||||
projectStatus: string;
|
||||
projectDescription: string;
|
||||
settlementDesc: string;
|
||||
projectDesc: string;
|
||||
projectFlow: string;
|
||||
applyPromoCodeDesc: string;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const projectData = ref<ProjectDetail>({
|
||||
id: 0,
|
||||
projectName: '',
|
||||
projectImage: '',
|
||||
projectSettlementCycle: 0,
|
||||
maxPromoterCount: 0,
|
||||
projectStatus: '',
|
||||
projectDescription: '',
|
||||
settlementDesc: '',
|
||||
projectDesc: '',
|
||||
projectFlow: '',
|
||||
applyPromoCodeDesc: ''
|
||||
});
|
||||
const projectId = ref<string | null>(null);
|
||||
const isEditing = ref(false);
|
||||
const fileInput = ref<HTMLInputElement | null>(null);
|
||||
|
||||
|
||||
// 图片上传处理逻辑
|
||||
const handleFileUpload = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
// 本地预览
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
// 仅设置临时预览,不覆盖实际值
|
||||
previewImage.value = e.target?.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
// 上传服务器
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('biz', 'project');
|
||||
uploadFormData.append('file', file);
|
||||
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res: any = await myAxios.post('/file/upload', uploadFormData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
'Authorization': storedToken
|
||||
}
|
||||
});
|
||||
|
||||
if (res.code === 1 && res.data) {
|
||||
// 核心修改:将返回的文件标识符赋值给projectImage
|
||||
projectData.value.projectImage = res.data;
|
||||
message.success('图片上传成功');
|
||||
} else {
|
||||
throw new Error(res.message || '上传失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
message.error('文件上传失败');
|
||||
previewImage.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const previewImage = ref('');
|
||||
|
||||
const formRef = ref<typeof Form>(); // 添加表单引用
|
||||
|
||||
// 添加数字输入校验方法
|
||||
const handleNumberInput = (e: KeyboardEvent) => {
|
||||
const allowedKeys = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'];
|
||||
if (!allowedKeys.includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
// 添加粘贴校验方法
|
||||
const handlePaste = (e: ClipboardEvent) => {
|
||||
const pasteData = e.clipboardData?.getData('text/plain');
|
||||
if (pasteData && !/^\d+$/.test(pasteData)) {
|
||||
e.preventDefault();
|
||||
message.error('只能粘贴数字');
|
||||
}
|
||||
};
|
||||
|
||||
// 更新项目时提交处理
|
||||
const finishEditing = async () => {
|
||||
//isEditing.value = false;
|
||||
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (!storedToken) throw new Error('未找到登录信息');
|
||||
|
||||
// 转换富文本内容为字符串
|
||||
const payload = {
|
||||
...projectData.value,
|
||||
settlementDesc: String(projectData.value.settlementDesc),
|
||||
projectDesc: String(projectData.value.projectDesc),
|
||||
projectFlow: String(projectData.value.projectFlow),
|
||||
applyPromoCodeDesc: String(projectData.value.applyPromoCodeDesc)
|
||||
};
|
||||
|
||||
const res:any = await myAxios.post("/project/update",
|
||||
JSON.stringify(payload),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': storedToken
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(res)
|
||||
if (res.code === 1) {
|
||||
message.success('项目更新成功');
|
||||
router.push('/project')
|
||||
|
||||
}else {
|
||||
message.error(res.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("更新失败:", error);
|
||||
|
||||
message.error('项目更新失败');
|
||||
return;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (typeof route.query.id === 'string') {
|
||||
projectId.value = route.query.id;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if(projectId.value !== null) {
|
||||
handleIdSearch(projectId.value);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const handleIdSearch = async (id: string) => {
|
||||
if (!id) return message.warning("请输入项目ID");
|
||||
if (!/^\d+$/.test(id)) return message.warning("ID必须为数字");
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (!storedToken) throw new Error('未找到登录信息');
|
||||
|
||||
const res:any = await myAxios.post("/project/queryById",
|
||||
{ id: parseInt(id) },
|
||||
{ headers: { Authorization: storedToken } }
|
||||
);
|
||||
|
||||
if (res.code === 1 && res.data) {
|
||||
console.log('Received project data:', res.data);
|
||||
projectData.value = res.data;
|
||||
} else {
|
||||
message.error(res.message || '查询失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("查询失败:", error);
|
||||
message.error('项目查询失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateProject = () => {
|
||||
if (!projectData.value) {
|
||||
message.warning('项目数据尚未加载完成,请稍后再试');
|
||||
return;
|
||||
}
|
||||
isEditing.value = true;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const goBack = () => {
|
||||
router.push('/project')
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<div class="project-detail-container">
|
||||
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
|
||||
<!-- 详情页 -->
|
||||
<div v-if="projectData && !isEditing" class="detail-card">
|
||||
<!-- 头部区域 -->
|
||||
<div class="header-section">
|
||||
<h1 class="project-title">{{ projectData.projectName }}</h1>
|
||||
<a-button class="custom-button" @click="updateProject">编辑项目</a-button>
|
||||
<a-button class="custom-button" @click="goBack">返回</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<div class="basic-info">
|
||||
<div class="project-image">
|
||||
<img :src="downLoadImage+projectData.projectImage" alt="项目封面图">
|
||||
</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<label>结算周期</label>
|
||||
<div class="value">T+{{ projectData.projectSettlementCycle }} 天</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<label>最大推广人数</label>
|
||||
<div class="value">{{ projectData.maxPromoterCount }}人</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目简介 -->
|
||||
<div class="section">
|
||||
<h2 class="section-title">项目简介</h2>
|
||||
<div class="section-content">{{ projectData.projectDescription }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 富文本内容区块 -->
|
||||
<div class="rich-section" v-if="projectData.settlementDesc">
|
||||
<h2 class="section-title">结算说明</h2>
|
||||
<div class="rich-content" v-html="projectData.settlementDesc"></div>
|
||||
</div>
|
||||
|
||||
<div class="rich-section" v-if="projectData.projectDesc">
|
||||
<h2 class="section-title">项目说明</h2>
|
||||
<div class="rich-content" v-html="projectData.projectDesc"></div>
|
||||
</div>
|
||||
|
||||
<div class="rich-section" v-if="projectData.projectFlow">
|
||||
<h2 class="section-title">项目流程</h2>
|
||||
<div class="rich-content" v-html="projectData.projectFlow"></div>
|
||||
</div>
|
||||
|
||||
<div class="rich-section" v-if="projectData.applyPromoCodeDesc">
|
||||
<h2 class="section-title">申请推广码说明</h2>
|
||||
<div class="rich-content" v-html="projectData.applyPromoCodeDesc"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ✅ 编辑页 - 使用 addProject.vue 结构 -->
|
||||
<div v-if="isEditing && projectData" class="add-project-container">
|
||||
<a-form ref="formRef" :model="projectData" layout="vertical" @submit.prevent="finishEditing">
|
||||
<div class="form-section">
|
||||
<div class="form-header">
|
||||
<h2>项目基本信息</h2>
|
||||
<a-button class="custom-button" @click="isEditing = false">返回</a-button>
|
||||
</div>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="项目名称">
|
||||
<a-input v-model:value="projectData.projectName" placeholder="请输入项目名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="项目状态">
|
||||
<a-select v-model:value="projectData.projectStatus" placeholder="请选择状态">
|
||||
<a-select-option value="running">运行中</a-select-option>
|
||||
<a-select-option value="full">人数已满</a-select-option>
|
||||
<a-select-option value="paused">已暂停</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="结算周期(天)"
|
||||
name="projectSettlementCycle"
|
||||
:rules="[{
|
||||
required: true,
|
||||
type: 'number',
|
||||
min: 0,
|
||||
message: '请输入有效的正整数'
|
||||
}]"
|
||||
>
|
||||
<a-input-number
|
||||
v-model:value="projectData.projectSettlementCycle"
|
||||
style="width: 100%"
|
||||
:min="0"
|
||||
@keypress="handleNumberInput"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
label="最大推广人数"
|
||||
name="maxPromoterCount"
|
||||
:rules="[{
|
||||
required: true,
|
||||
type: 'number',
|
||||
min: 1,
|
||||
message: '请输入有效的正整数'
|
||||
}]"
|
||||
>
|
||||
<a-input-number
|
||||
v-model:value="projectData.maxPromoterCount"
|
||||
style="width: 100%"
|
||||
:min="1"
|
||||
@keypress="handleNumberInput"
|
||||
@paste="handlePaste"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="项目图片">
|
||||
<div class="file-upload">
|
||||
<input
|
||||
type="file"
|
||||
class="file-input"
|
||||
accept="image/*"
|
||||
@change="handleFileUpload"
|
||||
ref="fileInput"
|
||||
>
|
||||
<div class="upload-button" @click="fileInput?.click()">
|
||||
<span v-if="!projectData.projectImage && !previewImage">点击上传图片</span>
|
||||
<span v-else class="file-name">已选择图片</span>
|
||||
</div>
|
||||
<!-- 修改:优先显示预览图,没有预览图时显示服务器图片 -->
|
||||
<div v-if="previewImage || projectData.projectImage" class="preview-image">
|
||||
<img
|
||||
v-if="previewImage"
|
||||
:src="previewImage"
|
||||
alt="上传预览"
|
||||
>
|
||||
<img
|
||||
v-else
|
||||
:src="downLoadImage + projectData.projectImage"
|
||||
alt="项目封面预览"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目描述">
|
||||
<a-textarea v-model:value="projectData.projectDescription" :rows="4" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<h2>富文本配置</h2>
|
||||
|
||||
<a-form-item label="结算说明">
|
||||
<QuillEditor v-model:content="projectData.settlementDesc" content-type="html" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目说明">
|
||||
<QuillEditor v-model:content="projectData.projectDesc" content-type="html" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目流程">
|
||||
<QuillEditor v-model:content="projectData.projectFlow" content-type="html" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="申请推广码说明">
|
||||
<QuillEditor v-model:content="projectData.applyPromoCodeDesc" content-type="html" />
|
||||
</a-form-item>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<a-button class="custom-button" htmlType="submit">完成编辑</a-button>
|
||||
</div>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.form-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
||||
/* 原有样式保持不变 */
|
||||
.project-detail-container {
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
color: #666;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 2rem;
|
||||
color: #1a1a1a;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
padding: 4px 12px;
|
||||
border-radius: 16px;
|
||||
color: white;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.basic-info {
|
||||
display: grid;
|
||||
grid-template-columns: 100px 1fr;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.project-image img {
|
||||
width: 100%;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.info-item label {
|
||||
display: block;
|
||||
color: #666;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-size: 1rem;
|
||||
color: #1a1a1a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.4rem;
|
||||
color: #1a1a1a;
|
||||
border-left: 4px solid #ffa940;
|
||||
padding-left: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
line-height: 1.6;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.rich-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.rich-content {
|
||||
background: #f8f9fa;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.rich-content :deep(p) {
|
||||
margin: 0.8em 0;
|
||||
}
|
||||
|
||||
.rich-content :deep(h2) {
|
||||
color: #1a1a1a;
|
||||
margin: 1.2em 0 0.8em;
|
||||
}
|
||||
|
||||
.rich-content :deep(ul) {
|
||||
padding-left: 1.5em;
|
||||
}
|
||||
|
||||
.rich-content :deep(li) {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.basic-info {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.project-title {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.6rem;
|
||||
font-size: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
height: 100px;
|
||||
}
|
||||
|
||||
.add-project-container {
|
||||
max-width: 960px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.form-section h2 {
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #1a1a1a;
|
||||
border-left: 4px solid #ffa940;
|
||||
padding-left: 0.8rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.ql-container {
|
||||
min-height: 120px;
|
||||
}
|
||||
.preview-image {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.preview-image img {
|
||||
max-width: 200px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* 保持原有上传样式 */
|
||||
.file-upload {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.upload-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.upload-button:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/*橙色按钮*/
|
||||
|
||||
.custom-button {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button:hover,
|
||||
.custom-button:focus {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button[disabled] {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
opacity: 0.6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 危险按钮样式 */
|
||||
.custom-button.ant-btn-dangerous {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-button.ant-btn-dangerous:hover,
|
||||
.custom-button.ant-btn-dangerous:focus {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.custom-search :deep(.ant-input-search-button) {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button:hover),
|
||||
.custom-search :deep(.ant-input-search-button:focus) {
|
||||
background-color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 保持输入框原有样式不变 */
|
||||
.custom-search :deep(.ant-input) {
|
||||
border-right-color: #ffa940;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
382
src/view/project/projectNotice.vue
Normal file
382
src/view/project/projectNotice.vue
Normal file
@ -0,0 +1,382 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import myAxios from "../../api/myAxios";
|
||||
import {message} from "ant-design-vue";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '项目通知ID',
|
||||
dataIndex: 'id',
|
||||
width: 20,
|
||||
key: 'id',
|
||||
fixed: 'left',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '通知标题',
|
||||
dataIndex: 'notificationTitle',
|
||||
key: 'notificationTitle',
|
||||
width: 30,
|
||||
fixed: 'left',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '通知内容',
|
||||
dataIndex: 'notificationContent',
|
||||
key: 'notificationContent',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目ID',
|
||||
dataIndex: 'projectId',
|
||||
key: 'projectId',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 70,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
}
|
||||
];
|
||||
|
||||
// 修改接口数据类型
|
||||
interface ProjectNotification {
|
||||
id: number;
|
||||
notificationTitle: string;
|
||||
notificationContent: string;
|
||||
projectId: number;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = ref<string | number>("");
|
||||
const originalTableData = ref<ProjectNotification[]>([]);
|
||||
const searchedData = ref<ProjectNotification[]>([]); // 存储搜索结果
|
||||
const displayData = ref<ProjectNotification[]>([]); // 实际显示的数据
|
||||
|
||||
const loading = ref(false);
|
||||
const error = ref("");
|
||||
const searchId = ref("");
|
||||
|
||||
// 修改搜索处理方法,不再调用后端接口
|
||||
const handleIdSearch = () => {
|
||||
const value = searchId.value.trim();
|
||||
|
||||
if (!value) {
|
||||
message.warning("请输入有效的项目通知ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Number(value);
|
||||
if (isNaN(id)) {
|
||||
message.warning("ID必须为数字");
|
||||
return;
|
||||
}
|
||||
|
||||
// 在原始数据中过滤
|
||||
const result = originalTableData.value.filter(item => item.id === id);
|
||||
|
||||
if (result.length === 0) {
|
||||
message.warning("未找到匹配的项目通知ID");
|
||||
}
|
||||
|
||||
// 更新显示数据为搜索结果
|
||||
searchedData.value = result;
|
||||
displayData.value = result;
|
||||
};
|
||||
|
||||
|
||||
const reset = () => {
|
||||
searchId.value = "";
|
||||
displayData.value = originalTableData.value;
|
||||
searchedData.value = [];
|
||||
};
|
||||
|
||||
if (typeof route.query.id === "string") {
|
||||
projectId.value = route.query.id;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (projectId.value) {
|
||||
getNotifications(projectId.value);
|
||||
}
|
||||
});
|
||||
const getNotifications = async (id: string | number) => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
try {
|
||||
loading.value = true;
|
||||
const response:any = await myAxios.post(
|
||||
"/projectNotification/query/pid",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(response)
|
||||
if (response.code === 1) {
|
||||
originalTableData.value = response.data;
|
||||
displayData.value = response.data; // 初始显示所有数据
|
||||
} else {
|
||||
error.value = "获取通知列表失败";
|
||||
originalTableData.value = [];
|
||||
displayData.value = [];
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = "数据加载失败,请重试";
|
||||
originalTableData.value = [];
|
||||
displayData.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (projectId.value) {
|
||||
getNotifications(projectId.value);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const goToNotificationDetail = (id: number) => {
|
||||
router.push({
|
||||
path: '/noticeDetail',
|
||||
query: { id }
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(getNotifications)
|
||||
|
||||
//删除操作
|
||||
const deleteNotification = async (id: number) => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res: any = await myAxios.post(
|
||||
"/projectNotification/delete",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
'AfterScript': 'required-script'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('删除成功');
|
||||
if (projectId.value) {
|
||||
await getNotifications (projectId.value);
|
||||
}
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
message.error('删除操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 从路由参数中获取项目ID
|
||||
if (typeof route.query.id === "string") {
|
||||
projectId.value = route.query.id;
|
||||
}
|
||||
|
||||
// 修改新增按钮的路由跳转,传递projectId到新增页面
|
||||
const goAddProjectNotice = () => {
|
||||
router.push({
|
||||
path: '/addprojectNotice',
|
||||
query: { projectId: projectId.value } // 传递项目ID到新增页面
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const goBack = () => {
|
||||
router.push('/project')
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-box">
|
||||
<a-form layout="inline">
|
||||
<a-space>
|
||||
<a-form-item label="ID">
|
||||
<a-input-search
|
||||
style="width: 300px"
|
||||
placeholder="请输入项目通知ID"
|
||||
enter-button
|
||||
@search="handleIdSearch"
|
||||
v-model:value="searchId"
|
||||
type="number"
|
||||
class="custom-search"
|
||||
min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-button class="custom-button" @click="goAddProjectNotice">新增项目通知</a-button>
|
||||
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</div>
|
||||
<!-- 修改表格模板 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="displayData"
|
||||
:scroll="{ x: 1200, y: 450 }"
|
||||
:loading="loading"
|
||||
bordered
|
||||
rowKey="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" danger @click="deleteNotification(record.id)">
|
||||
删除
|
||||
</a-button>
|
||||
<a-button size="small" @click="goToNotificationDetail(record.id)">
|
||||
编辑
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
<template #footer>
|
||||
<div class="table-footer">
|
||||
<a-button @click="goBack" class="back-button">返回</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.error-alert {
|
||||
padding: 1rem;
|
||||
background: #ffe3e3;
|
||||
color: #ff4444;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
display: inline-block;
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
border-radius: 50%;
|
||||
background: #ff4444;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead) {
|
||||
background-color: #fafafa !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-row:hover) {
|
||||
background-color: #fafafa !important;
|
||||
}
|
||||
|
||||
|
||||
/*橙色按钮*/
|
||||
|
||||
.custom-button {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button:hover,
|
||||
.custom-button:focus {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button[disabled] {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
opacity: 0.6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 危险按钮样式 */
|
||||
.custom-button.ant-btn-dangerous {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-button.ant-btn-dangerous:hover,
|
||||
.custom-button.ant-btn-dangerous:focus {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
/* 保持原有的其他样式不变 */
|
||||
.search-box {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button) {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button:hover),
|
||||
.custom-search :deep(.ant-input-search-button:focus) {
|
||||
background-color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 保持输入框原有样式不变 */
|
||||
.custom-search :deep(.ant-input) {
|
||||
border-right-color: #ffa940;
|
||||
}
|
||||
|
||||
/* 新增表格页脚样式 */
|
||||
.table-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
/* 调整分页器位置 */
|
||||
:deep(.ant-table-pagination.ant-pagination) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
856
src/view/project/promotionCode.vue
Normal file
856
src/view/project/promotionCode.vue
Normal file
@ -0,0 +1,856 @@
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted,reactive} from "vue";
|
||||
import {useRoute, useRouter} from "vue-router";
|
||||
import myAxios from "../../api/myAxios";
|
||||
import {message} from "ant-design-vue";
|
||||
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '推广码ID',
|
||||
dataIndex: 'id',
|
||||
width: 80,
|
||||
key: 'id',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '信息Key',
|
||||
dataIndex: 'promoCodeInfoKey',
|
||||
key: 'promoCodeInfoKey',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '推广链接',
|
||||
dataIndex: 'promoCodeLink',
|
||||
key: 'promoCodeLink',
|
||||
width: 200,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '推广码图片',
|
||||
dataIndex: 'promoCodeImage',
|
||||
key: 'promoCodeImage',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目ID',
|
||||
dataIndex: 'projectId',
|
||||
key: 'projectId',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'promoCodeStatus',
|
||||
key: 'promoCodeStatus',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
}
|
||||
];
|
||||
|
||||
// 接口数据类型
|
||||
interface PromoCode {
|
||||
id: number;
|
||||
promoCodeInfoKey: string;
|
||||
promoCodeLink: string;
|
||||
promoCodeImage: string;
|
||||
projectId: number;
|
||||
promoCodeStatus: boolean;
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const projectId = ref<string | number>("");
|
||||
const originalTableData = ref<PromoCode[]>([]); // 存储所有原始数据
|
||||
const displayData = ref<PromoCode[]>([]); // 实际显示的数据
|
||||
const loading = ref(false);
|
||||
const searchId = ref("");
|
||||
const previewVisible = ref(false);
|
||||
const previewImage = ref("");
|
||||
|
||||
// 主查询方法 - 获取项目所有推广码
|
||||
const getPromoCodes = async (id: string | number) => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
try {
|
||||
loading.value = true;
|
||||
const response: any = await myAxios.post(
|
||||
"/promoCode/queryByPid",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
console.log(response);
|
||||
|
||||
if (response.code === 1) {
|
||||
// 确保data是数组,即使为null也转为空数组
|
||||
const data = Array.isArray(response.data) ? response.data : [];
|
||||
originalTableData.value = data;
|
||||
displayData.value = data; // 初始显示所有数据
|
||||
} else {
|
||||
message.error(response.message || "获取数据失败");
|
||||
originalTableData.value = [];
|
||||
displayData.value = [];
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("查询错误:", err);
|
||||
message.error("数据加载失败,请重试");
|
||||
originalTableData.value = [];
|
||||
displayData.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化逻辑
|
||||
if (typeof route.query.id === "string") {
|
||||
projectId.value = route.query.id;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (projectId.value) {
|
||||
getPromoCodes(projectId.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 修改搜索处理 - 前端过滤
|
||||
const handleIdSearch = () => {
|
||||
const value = searchId.value.trim();
|
||||
|
||||
if (!value) {
|
||||
message.warning("请输入有效的推广码ID");
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Number(value);
|
||||
if (isNaN(id)) {
|
||||
message.warning("ID必须为数字");
|
||||
return;
|
||||
}
|
||||
|
||||
// 在前端原始数据中过滤
|
||||
const result = originalTableData.value.filter(item => item.id === id);
|
||||
|
||||
if (result.length === 0) {
|
||||
message.warning("未找到匹配的推广码ID");
|
||||
}
|
||||
|
||||
// 更新显示数据
|
||||
displayData.value = result;
|
||||
};
|
||||
|
||||
// 重置搜索 - 显示所有数据
|
||||
const reset = () => {
|
||||
searchId.value = "";
|
||||
displayData.value = originalTableData.value;
|
||||
};
|
||||
|
||||
// 预览图片
|
||||
const handlePreview = (imageUrl: string) => {
|
||||
previewImage.value = imageUrl;
|
||||
previewVisible.value = true;
|
||||
};
|
||||
|
||||
// 新增推广码表单相关状态
|
||||
const addDrawerVisible = ref(false);
|
||||
const addFormState = reactive({
|
||||
promoCodeInfoKey: '',
|
||||
promoCodeLink: '',
|
||||
promoCodeImage: '',
|
||||
projectId: 0,
|
||||
promoCodeStatus: false
|
||||
});
|
||||
|
||||
// 打开新增表单
|
||||
const openAddDrawer = () => {
|
||||
addFormState.projectId = Number(projectId.value); // 自动关联当前项目
|
||||
addDrawerVisible.value = true;
|
||||
};
|
||||
|
||||
// 提交新增请求
|
||||
const handleAddSubmit = async () => {
|
||||
// 如果有文件需要上传
|
||||
if (fileList.value.length > 0) {
|
||||
const imageUrl = await handleUpload();
|
||||
if (!imageUrl) {
|
||||
message.error('图片上传失败,无法提交');
|
||||
return;
|
||||
}
|
||||
// 更新表单中的图片URL
|
||||
addFormState.promoCodeImage = imageUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/promoCode/add",
|
||||
{
|
||||
promoCodeInfoKey: addFormState.promoCodeInfoKey,
|
||||
promoCodeLink: addFormState.promoCodeLink,
|
||||
promoCodeImage: addFormState.promoCodeImage,
|
||||
projectId: addFormState.projectId,
|
||||
promoCodeStatus: addFormState.promoCodeStatus
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('新增成功');
|
||||
addDrawerVisible.value = false;
|
||||
await getPromoCodes(projectId.value);
|
||||
// 重置表单和文件列表
|
||||
Object.assign(addFormState, {
|
||||
promoCodeInfoKey: '',
|
||||
promoCodeLink: '',
|
||||
promoCodeImage: '',
|
||||
promoCodeStatus: false,
|
||||
projectId: Number(projectId.value)
|
||||
});
|
||||
fileList.value = [];
|
||||
} else {
|
||||
message.error(res.message || '新增失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('新增请求失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 绑定新增按钮点击事件
|
||||
const goAddCode = () => {
|
||||
openAddDrawer();
|
||||
};
|
||||
|
||||
//删除操作
|
||||
const deletePromoCode = async (id: number) => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/promoCode/delete",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('删除成功');
|
||||
// 刷新数据
|
||||
await getPromoCodes(projectId.value);
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除操作失败');
|
||||
}
|
||||
};
|
||||
|
||||
//编辑
|
||||
// 编辑相关状态
|
||||
const editDrawerVisible = ref(false);
|
||||
const editFormState = reactive({
|
||||
id: 0,
|
||||
promoCodeInfoKey: '',
|
||||
promoCodeLink: '',
|
||||
promoCodeImage: '',
|
||||
projectId: 0,
|
||||
promoCodeStatus: false
|
||||
});
|
||||
const getPromoCodeDetail = async (id: number) => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
try {
|
||||
const response:any = await myAxios.post(
|
||||
"/promoCode/queryById",
|
||||
{ id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
if (response.code === 1) {
|
||||
editFormState.id = response.data.id;
|
||||
editFormState.promoCodeInfoKey = response.data.promoCodeInfoKey;
|
||||
editFormState.promoCodeLink = response.data.promoCodeLink;
|
||||
editFormState.promoCodeImage = response.data.promoCodeImage;
|
||||
editFormState.projectId = response.data.projectId;
|
||||
editFormState.promoCodeStatus = response.data.promoCodeStatus;
|
||||
} else {
|
||||
message.error(response.message || "获取编辑数据失败");
|
||||
}
|
||||
} catch (err) {
|
||||
message.error("获取编辑数据失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = async (id: number) => {
|
||||
await getPromoCodeDetail(id);
|
||||
editDrawerVisible.value = true;
|
||||
};
|
||||
|
||||
const handleEditSubmit = async () => {
|
||||
// 如果有文件需要上传
|
||||
if (fileList.value.length > 0) {
|
||||
const imageUrl = await handleUpload();
|
||||
if (!imageUrl) {
|
||||
message.error('图片上传失败,无法提交');
|
||||
return;
|
||||
}
|
||||
// 更新表单中的图片URL
|
||||
editFormState.promoCodeImage = imageUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/promoCode/update",
|
||||
{
|
||||
id: editFormState.id,
|
||||
promoCodeInfoKey: editFormState.promoCodeInfoKey,
|
||||
promoCodeLink: editFormState.promoCodeLink,
|
||||
promoCodeImage: editFormState.promoCodeImage,
|
||||
projectId: editFormState.projectId,
|
||||
promoCodeStatus: editFormState.promoCodeStatus
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
if (res.code === 1) {
|
||||
message.success('编辑成功');
|
||||
editDrawerVisible.value = false;
|
||||
await getPromoCodes(projectId.value);
|
||||
// 重置文件列表
|
||||
fileList.value = [];
|
||||
} else {
|
||||
message.error(res.message || '编辑失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('编辑请求失败');
|
||||
}
|
||||
};
|
||||
|
||||
//返回上一级
|
||||
const router = useRouter();
|
||||
|
||||
// 返回上一级方法
|
||||
const goBack = () => {
|
||||
router.go(-1); // 返回上一页
|
||||
};
|
||||
|
||||
// 添加文件上传相关状态
|
||||
const fileList = ref<any[]>([]);
|
||||
const uploading = ref(false);
|
||||
|
||||
// 图片上传处理
|
||||
const handleUpload = async () => {
|
||||
const formData = new FormData();
|
||||
fileList.value.forEach(file => {
|
||||
formData.append('file', file);
|
||||
});
|
||||
formData.append('biz', 'project'); // 添加biz参数
|
||||
|
||||
try {
|
||||
uploading.value = true;
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/file/upload",
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': storedToken,
|
||||
'AflatScript': 'required', // 添加AflatScript头部
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success('上传成功');
|
||||
// 将返回的文件路径保存到表单中
|
||||
addFormState.promoCodeImage = res.data;
|
||||
return res.data;
|
||||
} else {
|
||||
message.error(res.message || '上传失败');
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
message.error('上传失败');
|
||||
return null;
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
};
|
||||
// 文件上传前的处理
|
||||
const beforeUpload = (file: any) => {
|
||||
// 检查文件类型
|
||||
const isImage = file.type.includes('image');
|
||||
if (!isImage) {
|
||||
message.error('只能上传图片文件!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查文件大小 (限制为5MB)
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
if (!isLt5M) {
|
||||
message.error('图片大小不能超过5MB!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 添加到文件列表
|
||||
fileList.value = [file];
|
||||
return false; // 手动上传
|
||||
};
|
||||
|
||||
// 清除文件
|
||||
const clearFile = () => {
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
// 添加批量选择相关状态
|
||||
const selectedRowKeys = ref<number[]>([]); // 存储选中的行ID
|
||||
const batchDeleteLoading = ref(false); // 批量删除加载状态
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys,
|
||||
onChange: (selectedKeys: number[]) => {
|
||||
selectedRowKeys.value = selectedKeys;
|
||||
},
|
||||
};
|
||||
|
||||
// 批量删除方法
|
||||
const batchDelete = async () => {
|
||||
if (selectedRowKeys.value.length === 0) {
|
||||
message.warning('请至少选择一条记录');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
batchDeleteLoading.value = true;
|
||||
const storedToken = localStorage.getItem('token');
|
||||
const res:any = await myAxios.post(
|
||||
"/promoCode/delBatch",
|
||||
{ ids: selectedRowKeys.value }, // 发送选中ID数组
|
||||
{
|
||||
headers: {
|
||||
Authorization: storedToken,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (res.code === 1) {
|
||||
message.success(`成功删除 ${selectedRowKeys.value.length} 条记录`);
|
||||
// 刷新数据
|
||||
await getPromoCodes(projectId.value);
|
||||
// 清空选择
|
||||
selectedRowKeys.value = [];
|
||||
} else {
|
||||
message.error(res.message || '批量删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('批量删除操作失败');
|
||||
} finally {
|
||||
batchDeleteLoading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-box">
|
||||
<a-form layout="inline">
|
||||
<a-space>
|
||||
<a-form-item label="推广码ID">
|
||||
<a-input-search
|
||||
style="width: 300px"
|
||||
placeholder="输入推广码ID进行查询"
|
||||
enter-button
|
||||
@search="handleIdSearch"
|
||||
v-model:value="searchId"
|
||||
type="number"
|
||||
class="custom-search"
|
||||
min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||
<a-button class="custom-button" @click="goAddCode">新增推广码</a-button>
|
||||
<a-button @click="goBack" class="custom-button">返回</a-button>
|
||||
|
||||
<!-- 添加批量删除按钮 -->
|
||||
<a-popconfirm
|
||||
title="确定要删除选中的推广码吗?"
|
||||
@confirm="batchDelete"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a-button
|
||||
class="custom-button"
|
||||
type="danger"
|
||||
:disabled="selectedRowKeys.length === 0"
|
||||
:loading="batchDeleteLoading"
|
||||
>
|
||||
批量删除 ({{ selectedRowKeys.length }})
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
|
||||
<a-form-item label="当前项目ID">
|
||||
<a-tag color="blue">{{ projectId }}</a-tag>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="displayData"
|
||||
:scroll="{ x: 1000, y: 550 }"
|
||||
:loading="loading"
|
||||
bordered
|
||||
rowKey="id"
|
||||
locale="{ emptyText: '暂无数据' }"
|
||||
:row-selection="rowSelection"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'promoCodeStatus'">
|
||||
<a-tag :color="record.promoCodeStatus ? 'red' : 'green'">
|
||||
{{ record.promoCodeStatus ? '占用' : '空闲' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'promoCodeImage'">
|
||||
<a-image
|
||||
v-if="record.promoCodeImage"
|
||||
:width="80"
|
||||
:src="downLoadImage+record.promoCodeImage"
|
||||
:preview="false"
|
||||
@click="handlePreview(record.promoCodeImage)"
|
||||
/>
|
||||
<span v-else>无图片</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space :size="8">
|
||||
<a-button
|
||||
size="small"
|
||||
danger
|
||||
@click="deletePromoCode(record.id)"
|
||||
:loading="loading"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
<a-button size="small" @click="handleEdit(record.id)">
|
||||
编辑
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 图片预览模态框 -->
|
||||
<a-modal :visible="previewVisible" :footer="null" @cancel="previewVisible = false">
|
||||
<img alt="预览图片" style="width: 100%" :src="downLoadImage+previewImage" />
|
||||
</a-modal>
|
||||
|
||||
<!-- 新增推广码抽屉 -->
|
||||
<a-drawer
|
||||
title="新增推广码"
|
||||
placement="right"
|
||||
:visible="addDrawerVisible"
|
||||
@close="addDrawerVisible = false"
|
||||
width="600"
|
||||
>
|
||||
<a-form
|
||||
:model="addFormState"
|
||||
layout="vertical"
|
||||
@finish="handleAddSubmit"
|
||||
>
|
||||
<a-form-item label="信息Key" required>
|
||||
<a-input v-model:value="addFormState.promoCodeInfoKey" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="推广链接" required>
|
||||
<a-input v-model:value="addFormState.promoCodeLink" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="推广码图片" required>
|
||||
<a-upload
|
||||
:file-list="fileList"
|
||||
:before-upload="beforeUpload"
|
||||
:remove="clearFile"
|
||||
list-type="picture-card"
|
||||
accept="image/*"
|
||||
:max-count="1"
|
||||
>
|
||||
<div v-if="fileList.length < 1">
|
||||
<plus-outlined />
|
||||
<div class="ant-upload-text">上传图片</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<div v-if="addFormState.promoCodeImage">
|
||||
已上传图片:
|
||||
<a :href="addFormState.promoCodeImage" target="_blank">查看</a>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态">
|
||||
<a-input-number
|
||||
value="空闲"
|
||||
:disabled="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="项目ID">
|
||||
<a-input-number
|
||||
v-model:value="addFormState.projectId"
|
||||
:disabled="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
class="custom-button"
|
||||
html-type="submit"
|
||||
:loading="uploading"
|
||||
>
|
||||
提交
|
||||
</a-button>
|
||||
<a-button style="margin-left: 10px" @click="addDrawerVisible = false">
|
||||
取消
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
<!-- 编辑推广码抽屉 -->
|
||||
<a-drawer
|
||||
title="编辑推广码"
|
||||
placement="right"
|
||||
:visible="editDrawerVisible"
|
||||
@close="editDrawerVisible = false"
|
||||
width="600"
|
||||
>
|
||||
<a-form
|
||||
:model="editFormState"
|
||||
layout="vertical"
|
||||
@finish="handleEditSubmit"
|
||||
>
|
||||
<a-form-item label="ID">
|
||||
<a-input v-model:value="editFormState.id" :disabled="true" />
|
||||
</a-form-item>
|
||||
<a-form-item label="信息Key" required>
|
||||
<a-input v-model:value="editFormState.promoCodeInfoKey" />
|
||||
</a-form-item>
|
||||
<a-form-item label="推广链接" required>
|
||||
<a-input v-model:value="editFormState.promoCodeLink" />
|
||||
</a-form-item>
|
||||
<a-form-item label="推广码图片" required>
|
||||
<a-upload
|
||||
:file-list="fileList"
|
||||
:before-upload="beforeUpload"
|
||||
:remove="clearFile"
|
||||
list-type="picture-card"
|
||||
accept="image/*"
|
||||
:max-count="1"
|
||||
>
|
||||
<div v-if="fileList.length < 1">
|
||||
<plus-outlined />
|
||||
<div class="ant-upload-text">上传图片</div>
|
||||
</div>
|
||||
</a-upload>
|
||||
<div v-if="editFormState.promoCodeImage">
|
||||
当前图片:
|
||||
<a :href="editFormState.promoCodeImage" target="_blank">查看</a>
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<a-tag>
|
||||
{{ editFormState.promoCodeStatus ? '占用' : '空闲' }}
|
||||
</a-tag>
|
||||
</a-form-item>
|
||||
<a-form-item label="项目ID">
|
||||
<a-input-number
|
||||
v-model:value="editFormState.projectId"
|
||||
:disabled="true"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button
|
||||
class="custom-button"
|
||||
html-type="submit"
|
||||
:loading="uploading"
|
||||
>
|
||||
提交
|
||||
</a-button>
|
||||
<a-button style="margin-left: 10px" @click="editDrawerVisible = false">
|
||||
取消
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-drawer>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead) {
|
||||
background-color: #fafafa !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-row:hover) {
|
||||
background-color: #fafafa !important;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead) {
|
||||
background-color: #fafafa !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-row:hover) {
|
||||
background-color: #fafafa !important;
|
||||
}
|
||||
|
||||
/* 新增表格页脚样式 */
|
||||
.table-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
/* 调整分页器位置 */
|
||||
:deep(.ant-table-pagination.ant-pagination) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*橙色按钮*/
|
||||
|
||||
.custom-button {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button:hover,
|
||||
.custom-button:focus {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button[disabled] {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
opacity: 0.6;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 危险按钮样式 */
|
||||
.custom-button.ant-btn-dangerous {
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
}
|
||||
|
||||
.custom-button.ant-btn-dangerous:hover,
|
||||
.custom-button.ant-btn-dangerous:focus {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
/* 保持原有的其他样式不变 */
|
||||
.search-box {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button) {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button:hover),
|
||||
.custom-search :deep(.ant-input-search-button:focus) {
|
||||
background-color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 保持输入框原有样式不变 */
|
||||
.custom-search :deep(.ant-input) {
|
||||
border-right-color: #ffa940;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-select-picture-card i) {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
:deep(.ant-upload-select-picture-card .ant-upload-text) {
|
||||
margin-top: 8px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/*批量删除按钮操作*/
|
||||
.batch-delete-button {
|
||||
margin-left: 10px;
|
||||
background-color: #ff4d4f;
|
||||
border-color: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.batch-delete-button:hover {
|
||||
background-color: #ff7875;
|
||||
border-color: #ff7875;
|
||||
}
|
||||
|
||||
/* 添加选择计数样式 */
|
||||
.selection-count {
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
color: #ff4d4f;
|
||||
}
|
||||
</style>
|
||||
257
src/view/settlement/applicationRecord.vue
Normal file
257
src/view/settlement/applicationRecord.vue
Normal file
@ -0,0 +1,257 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-table
|
||||
:scroll="{ x: 2000, y: 550 }"
|
||||
:dataSource="dataSource"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
@change="handleTableChange"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'projectImage'">
|
||||
<!-- 修复:添加 alt 属性 -->
|
||||
<img
|
||||
:src="downLoadImage+record.projectImage"
|
||||
alt="项目图片"
|
||||
style="width: 50px; height: 50px"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.dataIndex === 'projectCodeImage'">
|
||||
<!-- 修复:添加 alt 属性 -->
|
||||
<img
|
||||
:src="downLoadImage+record.projectCodeImage"
|
||||
alt="推广码"
|
||||
style="width: 50px; height: 50px"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space :size="8">
|
||||
<a-tag
|
||||
color="orange"
|
||||
@click="settlementRecord(record)"
|
||||
>
|
||||
结算记录
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import type { TableProps } from 'ant-design-vue';
|
||||
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import router from "../../router";
|
||||
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||
|
||||
interface PromoRecord {
|
||||
id: number;
|
||||
salespersonName: string;
|
||||
salespersonPhone: string;
|
||||
promoCodeInfoKey: string;
|
||||
promoCodeLink: string;
|
||||
projectName: string;
|
||||
projectImage: string;
|
||||
projectId: number;
|
||||
userId: number;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
// 列配置保持不变
|
||||
const columns = ref([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 10,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortDirections: ['ascend', 'descend']
|
||||
},
|
||||
{
|
||||
title: '项目名称',
|
||||
dataIndex: 'projectName',
|
||||
key: 'projectName',
|
||||
width: 20,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortDirections: ['ascend', 'descend']
|
||||
},
|
||||
{
|
||||
title: '推广人员',
|
||||
dataIndex: 'salespersonName',
|
||||
key: 'salespersonName',
|
||||
width: 20,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortDirections: ['ascend', 'descend']
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'salespersonPhone',
|
||||
key: 'salespersonPhone',
|
||||
width: 20,
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortDirections: ['ascend', 'descend']
|
||||
},
|
||||
{
|
||||
title: '推广信息key',
|
||||
dataIndex: 'promoCodeInfoKey',
|
||||
key: 'promoCodeInfoKey',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '推广链接',
|
||||
dataIndex: 'promoCodeLink',
|
||||
key: 'promoCodeLink',
|
||||
width: 60,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '推广码',
|
||||
dataIndex: 'projectCodeImage',
|
||||
key: 'projectCodeImage',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目图片',
|
||||
dataIndex: 'projectImage',
|
||||
key: 'projectImage',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '项目ID',
|
||||
dataIndex: 'projectId',
|
||||
key: 'projectId',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '用户ID',
|
||||
dataIndex: 'userId',
|
||||
key: 'userId',
|
||||
width: 20,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 40,
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
sortDirections: ['ascend', 'descend']
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 30,
|
||||
align: 'center'
|
||||
}
|
||||
]);
|
||||
|
||||
const dataSource = ref<PromoRecord[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 添加排序参数
|
||||
const sortParams = reactive({
|
||||
sortField: 'id',
|
||||
sortOrder: 'ascend'
|
||||
});
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
});
|
||||
|
||||
// fetchData 方法保持不变
|
||||
const fetchData = async () => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
try {
|
||||
loading.value = true;
|
||||
const response:any = await myAxios.post('/promoCodeApply/page', {
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
sortField: sortParams.sortField,
|
||||
sortOrder: sortParams.sortOrder
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': storedToken
|
||||
}
|
||||
});
|
||||
|
||||
if (response.code === 1) {
|
||||
dataSource.value = response.data.records;
|
||||
pagination.total = response.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 修复:简化排序处理逻辑
|
||||
const handleTableChange: TableProps['onChange'] = (pag, _, sorter) => {
|
||||
// 处理分页参数
|
||||
if (pag) {
|
||||
pagination.current = pag.current!;
|
||||
pagination.pageSize = pag.pageSize!;
|
||||
}
|
||||
|
||||
// 修复:简化排序处理
|
||||
if (sorter) {
|
||||
// 处理排序参数(只处理单列排序)
|
||||
const { field, order } = sorter as { field?: string; order?: string };
|
||||
|
||||
if (field && order) {
|
||||
sortParams.sortField = field;
|
||||
sortParams.sortOrder = order;
|
||||
} else {
|
||||
// 取消排序时重置
|
||||
sortParams.sortField = 'id';
|
||||
sortParams.sortOrder = 'ascend';
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
const settlementRecord = (record: PromoRecord) => {
|
||||
router.push({
|
||||
path: '/moneyRecord',
|
||||
query: {
|
||||
id: String(record.id),
|
||||
projectId: String(record.projectId),
|
||||
userId: String(record.userId)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
517
src/view/settlement/moneyRecord.vue
Normal file
517
src/view/settlement/moneyRecord.vue
Normal file
@ -0,0 +1,517 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<!-- 搜索和筛选区域 -->
|
||||
<div class="search-box">
|
||||
<a-form layout="inline">
|
||||
<a-space>
|
||||
<a-form-item label="ID">
|
||||
<a-input-search
|
||||
style="width: 300px"
|
||||
placeholder="请输入项目结算记录ID"
|
||||
enter-button
|
||||
@search="handleIdSearch"
|
||||
v-model:value="searchId"
|
||||
type="number"
|
||||
class="custom-search"
|
||||
min="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="收益来源">
|
||||
<a-select
|
||||
v-model:value="revenueSourceFilter"
|
||||
style="width: 120px"
|
||||
@change="applyFilters"
|
||||
>
|
||||
<a-select-option value="all">全部</a-select-option>
|
||||
<a-select-option :value="false">推广码</a-select-option>
|
||||
<a-select-option :value="true">抽成</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||
|
||||
<a-button class="custom-button" @click="showModal">新增项目明细</a-button>
|
||||
<a-button class="custom-button" @click="goBack">返回</a-button>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-table
|
||||
:dataSource="paginatedData"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
@change="handleTableChange"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 格式化金额显示 -->
|
||||
<template v-if="column.dataIndex === 'settlementRevenue'">
|
||||
¥{{ record.settlementRevenue.toFixed(2) }}
|
||||
</template>
|
||||
|
||||
<!-- 收益来源列 -->
|
||||
<template v-if="column.dataIndex === 'revenueSource'">
|
||||
<a-tag v-if="record.revenueSource" color="orange">抽成</a-tag>
|
||||
<a-tag v-else color="green">推广码</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space :size="8">
|
||||
<a-button size="small" danger @click="deleteRecord(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 新增项目明细弹窗 -->
|
||||
<a-modal
|
||||
v-model:visible="visible"
|
||||
title="新增项目明细"
|
||||
@ok="handleAdd"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<a-form :model="form">
|
||||
<a-form-item label="项目明细名称" required>
|
||||
<a-input v-model:value="form.projectDetailName" />
|
||||
</a-form-item>
|
||||
<a-form-item label="结算数量" :rules="[{ required: true, message: '请输入结算数量' }]" required>
|
||||
<a-input-number
|
||||
v-model:value="form.settlementQuantity"
|
||||
min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="请输入格式为1"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="结算收益" :rules="[{ required: true, message: '请输入结算收益' }]" required>
|
||||
<a-input-number
|
||||
v-model:value="form.settlementRevenue"
|
||||
min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
placeholder="请输入格式为0.01"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="作业时间" required>
|
||||
<a-date-picker v-model:value="form.workTime" format="YYYY-MM-DD" />
|
||||
</a-form-item>
|
||||
<a-form-item label="结算时间" required>
|
||||
<a-date-picker v-model:value="form.settlementTime" format="YYYY-MM-DD" />
|
||||
</a-form-item>
|
||||
<a-form-item label="推广码申请ID">
|
||||
<a-input-number v-model:value="form.promoCodeApplyId" disabled/>
|
||||
</a-form-item>
|
||||
<a-form-item label="项目ID">
|
||||
<a-input-number v-model:value="form.projectId" disabled/>
|
||||
</a-form-item>
|
||||
<a-form-item label="用户ID">
|
||||
<a-input-number v-model:value="form.userId" disabled/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import type { TableProps } from 'ant-design-vue';
|
||||
import myAxios from '../../api/myAxios.ts';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import router from '../../router';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
// 项目结算记录接口类型
|
||||
interface SettlementRecord {
|
||||
id: number;
|
||||
projectDetailName: string;
|
||||
settlementQuantity: number;
|
||||
settlementRevenue: number;
|
||||
settlementTime: string;
|
||||
workTime: string;
|
||||
revenueSource: boolean;
|
||||
}
|
||||
|
||||
|
||||
// 表格列定义
|
||||
const columns = ref([
|
||||
{
|
||||
title: '项目结算记录ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '项目明细名称',
|
||||
dataIndex: 'projectDetailName',
|
||||
key: 'projectDetailName',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '结算数量',
|
||||
dataIndex: 'settlementQuantity',
|
||||
key: 'settlementQuantity',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '结算收益',
|
||||
dataIndex: 'settlementRevenue',
|
||||
key: 'settlementRevenue',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '结算时间',
|
||||
dataIndex: 'settlementTime',
|
||||
key: 'settlementTime',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '作业时间',
|
||||
dataIndex: 'workTime',
|
||||
key: 'workTime',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '收益来源',
|
||||
dataIndex: 'revenueSource',
|
||||
key: 'revenueSource',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
]);
|
||||
const allData = ref<SettlementRecord[]>([]); // 存储所有数据
|
||||
const filteredData = ref<SettlementRecord[]>([]); // 存储过滤后的数据
|
||||
const loading = ref(false);
|
||||
const searchId = ref(''); // 新增搜索ID绑定
|
||||
const originalData = ref<SettlementRecord[]>([]); // 存储原始数据
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
});
|
||||
|
||||
// 获取路由参数
|
||||
const route = useRoute();
|
||||
const idFromRoute = route.query.id as string | undefined;
|
||||
const projectIdFromRoute = route.query.projectId as string | undefined;
|
||||
const userIdFromRoute = route.query.userId as string | undefined;
|
||||
|
||||
// 计算属性:根据分页参数生成表格渲染用的分页数据
|
||||
const paginatedData = computed(() => {
|
||||
const start = (pagination.current - 1) * pagination.pageSize;
|
||||
const end = start + pagination.pageSize;
|
||||
return filteredData.value.slice(start, end);
|
||||
});
|
||||
|
||||
// 获取项目结算记录数据
|
||||
const fetchData = async () => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const response: any = await myAxios.post(
|
||||
'/projectSettlement/queryByPId',
|
||||
{ id: idFromRoute }, // 使用从路由获取的id
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: storedToken,
|
||||
AfterScript: 'required-script',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log('接口响应:', response);
|
||||
|
||||
if (response.code === 1) {
|
||||
allData.value = response.data;
|
||||
originalData.value = [...response.data]; // 保存原始数据
|
||||
filteredData.value = [...response.data];
|
||||
pagination.total = response.data.length;
|
||||
} else {
|
||||
message.error(response.message || '获取数据失败');
|
||||
allData.value = [];
|
||||
originalData.value = [];
|
||||
filteredData.value = [];
|
||||
pagination.total = 0;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
message.error('请求数据失败,请稍后重试');
|
||||
allData.value = [];
|
||||
originalData.value = [];
|
||||
filteredData.value = [];
|
||||
pagination.total = 0;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ID搜索处理 - 前端过滤
|
||||
const handleIdSearch = (value: string) => {
|
||||
const idStr = value.trim();
|
||||
if (!idStr) {
|
||||
// 如果搜索值为空,显示所有数据
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const id = Number(idStr);
|
||||
if (isNaN(id)) {
|
||||
message.warning('ID必须为数字');
|
||||
return;
|
||||
}
|
||||
|
||||
// 前端过滤 originalData 数据
|
||||
const result = originalData.value.filter((record) => record.id === id);
|
||||
|
||||
if (result.length > 0) {
|
||||
filteredData.value = result;
|
||||
pagination.total = result.length;
|
||||
pagination.current = 1;
|
||||
} else {
|
||||
message.info('未找到匹配的记录');
|
||||
filteredData.value = [];
|
||||
pagination.total = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const reset = () => {
|
||||
searchId.value = '';
|
||||
filteredData.value = [...originalData.value];
|
||||
pagination.total = originalData.value.length;
|
||||
pagination.current = 1; // 重置页码
|
||||
revenueSourceFilter.value = 'all'; // 重置收益来源筛选
|
||||
applyFilters();
|
||||
};
|
||||
|
||||
// 表格分页变更处理
|
||||
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||
if (pag) {
|
||||
pagination.current = pag.current!;
|
||||
pagination.pageSize = pag.pageSize!;
|
||||
// 计算属性自动根据 pagination 变化更新,无需额外调用
|
||||
}
|
||||
};
|
||||
|
||||
// 返回按钮(修正拼写,保持语义清晰)
|
||||
const goBack = () => {
|
||||
router.push('/applicationRecord');
|
||||
};
|
||||
|
||||
// 删除记录
|
||||
const deleteRecord = async (id: number) => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
const response: any = await myAxios.post(
|
||||
'/projectSettlement/delete',
|
||||
{ id }, // 要删除的记录ID
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: storedToken,
|
||||
AfterScript: 'required-script',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
console.log('删除接口响应:', response);
|
||||
|
||||
if (response.code === 1) {
|
||||
message.success('删除成功');
|
||||
// 重新加载数据
|
||||
if (searchId.value) {
|
||||
// 如果当前在搜索状态,重新搜索
|
||||
handleIdSearch(searchId.value);
|
||||
} else {
|
||||
// 否则重新获取所有数据
|
||||
await fetchData(); // 异步函数调用加 await
|
||||
}
|
||||
} else {
|
||||
message.error(response.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除请求失败:', error);
|
||||
message.error('删除数据失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
const visible = ref(false);
|
||||
const form = reactive({
|
||||
projectDetailName: '',
|
||||
settlementQuantity: '',
|
||||
settlementRevenue: '',
|
||||
workTime: null,
|
||||
settlementTime: null,
|
||||
// 新增:自动填充路由参数
|
||||
promoCodeApplyId: idFromRoute || 0,
|
||||
projectId: projectIdFromRoute || 0,
|
||||
userId: userIdFromRoute || 0,
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const showModal = () => {
|
||||
visible.value = true;
|
||||
// 重置非路由传递的字段(避免重复填充)
|
||||
form.projectDetailName = '';
|
||||
form.settlementQuantity = '';
|
||||
form.settlementRevenue ='';
|
||||
form.workTime = null;
|
||||
form.settlementTime = null;
|
||||
};
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!form.projectDetailName || !form.settlementQuantity || !form.settlementRevenue || !form.workTime || !form.settlementTime || !form.promoCodeApplyId || !form.projectId || !form.userId) {
|
||||
message.error('请填写完整信息');
|
||||
return;
|
||||
}
|
||||
|
||||
const storedToken = localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
const response:any = await myAxios.post(
|
||||
'/projectSettlement/add',
|
||||
{
|
||||
projectDetailName: form.projectDetailName,
|
||||
settlementQuantity: form.settlementQuantity,
|
||||
settlementRevenue: form.settlementRevenue,
|
||||
workTime: dayjs(form.workTime).format('YYYY-MM-DD'),
|
||||
settlementTime: dayjs(form.settlementTime).format('YYYY-MM-DD'),
|
||||
promoCodeApplyId: form.promoCodeApplyId,
|
||||
projectId: form.projectId,
|
||||
userId: form.userId,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: storedToken,
|
||||
AfterScript: 'required-script',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (response.code === 1) {
|
||||
message.success('新增成功');
|
||||
visible.value = false;
|
||||
await fetchData(); // 刷新数据
|
||||
} else {
|
||||
message.error(response.message || '新增失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
message.error('新增数据失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
const revenueSourceFilter = ref<string | boolean>('all'); // 收益来源筛选值
|
||||
|
||||
// 应用筛选条件
|
||||
const applyFilters = () => {
|
||||
let result = [...originalData.value];
|
||||
|
||||
// 应用ID筛选
|
||||
if (searchId.value.trim() !== '') {
|
||||
const id = Number(searchId.value.trim());
|
||||
if (!isNaN(id)) {
|
||||
result = result.filter(record => record.id === id);
|
||||
}
|
||||
}
|
||||
|
||||
// 应用收益来源筛选
|
||||
if (revenueSourceFilter.value !== 'all') {
|
||||
result = result.filter(record => record.revenueSource === revenueSourceFilter.value);
|
||||
}
|
||||
|
||||
filteredData.value = result;
|
||||
pagination.total = result.length;
|
||||
pagination.current = 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 橙色按钮样式 */
|
||||
.custom-button {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-button:hover,
|
||||
.custom-button:focus {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
color: #fff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 表格样式调整 */
|
||||
:deep(.ant-table-thead) {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table-row:hover) {
|
||||
background-color: #f9f9f9 !important;
|
||||
}
|
||||
|
||||
/* 搜索按钮样式 */
|
||||
.custom-search :deep(.ant-input-search-button) {
|
||||
background-color: #ffa940;
|
||||
border-color: #ffa940;
|
||||
}
|
||||
|
||||
.custom-search :deep(.ant-input-search-button:hover),
|
||||
.custom-search :deep(.ant-input-search-button:focus) {
|
||||
background-color: #fa8c16;
|
||||
border-color: #fa8c16;
|
||||
}
|
||||
|
||||
/* 保持输入框原有样式不变 */
|
||||
.custom-search :deep(.ant-input) {
|
||||
border-right-color: #ffa940;
|
||||
}
|
||||
</style>
|
||||
237
src/view/settlement/withdrawalApplicationRecord.vue
Normal file
237
src/view/settlement/withdrawalApplicationRecord.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<a-table
|
||||
:scroll="{ x: 2000, y: 550 }"
|
||||
:dataSource="dataSource"
|
||||
:columns="columns"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
@change="handleTableChange"
|
||||
bordered
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'withdrawnAmount'">
|
||||
¥{{record.withdrawnAmount }}
|
||||
</template>
|
||||
<template v-if="column.key === 'withdrawalStatus'">
|
||||
<a-tag :color="getStatusColor(record.withdrawalStatus)">
|
||||
{{ getStatusText(record.withdrawalStatus) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space :size="8">
|
||||
<a-tag
|
||||
color="orange"
|
||||
@click="viewDetails(record)"
|
||||
>
|
||||
查看详情
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import type { TableProps } from 'ant-design-vue';
|
||||
import myAxios from "../../api/myAxios.ts";
|
||||
import router from "../../router";
|
||||
|
||||
interface WithdrawalRecord {
|
||||
id: number;
|
||||
cardHolder: string;
|
||||
idCardNumber: string;
|
||||
phoneNumber: string;
|
||||
bankCardNumber: string;
|
||||
openBank: string;
|
||||
withdrawAmount: number;
|
||||
withdrawalStatus: string;
|
||||
}
|
||||
|
||||
// 修改后的列配置 - 添加排序功能
|
||||
const columns = ref([
|
||||
{
|
||||
title: '提现申请记录ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
fixed: 'left',
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '持卡人',
|
||||
dataIndex: 'cardHolder',
|
||||
key: 'cardHolder',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '身份证号',
|
||||
dataIndex: 'idCardNumber',
|
||||
key: 'idCardNumber',
|
||||
width: 180,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '手机号',
|
||||
dataIndex: 'phoneNumber',
|
||||
key: 'phoneNumber',
|
||||
width: 140,
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '银行卡号',
|
||||
dataIndex: 'bankCardNumber',
|
||||
key: 'bankCardNumber',
|
||||
width: 180,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '开户银行',
|
||||
dataIndex: 'openBank',
|
||||
key: 'openBank',
|
||||
width: 150,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '提现金额',
|
||||
dataIndex: 'withdrawnAmount',
|
||||
key: 'withdrawnAmount',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
sorter: true, // 添加排序功能
|
||||
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||
},
|
||||
{
|
||||
title: '提取状态',
|
||||
dataIndex: 'withdrawalStatus',
|
||||
key: 'withdrawalStatus',
|
||||
width: 120,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
width: 100,
|
||||
align: 'center'
|
||||
}
|
||||
]);
|
||||
|
||||
const dataSource = ref<WithdrawalRecord[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 添加排序参数
|
||||
const sortParams = reactive({
|
||||
sortField: 'id',
|
||||
sortOrder: 'ascend'
|
||||
});
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
});
|
||||
|
||||
// 状态映射函数
|
||||
const getStatusText = (status: string) => {
|
||||
const statusMap: Record<string, string> = {
|
||||
'processing': '提现中',
|
||||
'success': '提现成功',
|
||||
'failed': '提现失败'
|
||||
};
|
||||
return statusMap[status] || status;
|
||||
};
|
||||
|
||||
// 状态颜色映射函数
|
||||
const getStatusColor = (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'processing': 'blue',
|
||||
'success': 'green',
|
||||
'failed': 'red'
|
||||
};
|
||||
return colorMap[status] || 'default';
|
||||
};
|
||||
|
||||
// 修改后的 fetchData 方法 - 添加排序参数
|
||||
const fetchData = async () => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
try {
|
||||
loading.value = true;
|
||||
const response: any = await myAxios.post('/withdrawalApply/page', {
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
sortField: sortParams.sortField, // 添加排序字段
|
||||
sortOrder: sortParams.sortOrder, // 添加排序方式
|
||||
withdrawalStatus: 'processing'
|
||||
}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': storedToken
|
||||
}
|
||||
});
|
||||
|
||||
if (response.code === 1) {
|
||||
dataSource.value = response.data.records;
|
||||
pagination.total = response.data.total;
|
||||
pagination.current = response.data.current;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('请求失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 修改后的 handleTableChange 方法 - 处理排序
|
||||
const handleTableChange: TableProps['onChange'] = (pag, _, sorter) => {
|
||||
// 处理分页参数
|
||||
if (pag) {
|
||||
pagination.current = pag.current!;
|
||||
pagination.pageSize = pag.pageSize!;
|
||||
}
|
||||
|
||||
// 处理排序参数
|
||||
if (sorter) {
|
||||
const { field, order } = sorter as { field?: string; order?: string };
|
||||
|
||||
if (field && order) {
|
||||
sortParams.sortField = field;
|
||||
sortParams.sortOrder = order;
|
||||
} else {
|
||||
// 取消排序时重置
|
||||
sortParams.sortField = 'id';
|
||||
sortParams.sortOrder = 'ascend';
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
const viewDetails = (record: WithdrawalRecord) => {
|
||||
router.push({
|
||||
path: '/withdrawalDetail',
|
||||
query: { id: record.id.toString() }
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
1518
src/view/userList/userList.vue
Normal file
1518
src/view/userList/userList.vue
Normal file
File diff suppressed because it is too large
Load Diff
3
src/view/work/workDetail.vue
Normal file
3
src/view/work/workDetail.vue
Normal file
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
123
|
||||
</template>
|
||||
4
src/view/work/workList.vue
Normal file
4
src/view/work/workList.vue
Normal file
@ -0,0 +1,4 @@
|
||||
<template>
|
||||
789
|
||||
</template>
|
||||
|
||||
16
src/vite-env.d.ts
vendored
Normal file
16
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare module '*.vue' {
|
||||
import type {DefineComponent} from 'vue'
|
||||
const vueComponent: DefineComponent<{}, {}, any>
|
||||
export default vueComponent
|
||||
}
|
||||
declare module '*.mjs'
|
||||
declare module 'dayjs'
|
||||
// interface ImportMetaEnv {
|
||||
// readonly VITE_RSA_PUBLIC_KEY: string
|
||||
// // 添加其他环境变量...
|
||||
// }
|
||||
//
|
||||
// interface ImportMeta {
|
||||
// readonly env: ImportMetaEnv
|
||||
// }
|
||||
Reference in New Issue
Block a user