finish init again

This commit is contained in:
2025-06-18 09:26:56 +08:00
commit 29d5b6927d
57 changed files with 21760 additions and 0 deletions

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# Vue 3 + TypeScript + Vite
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

10
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
}

50
components.d.ts vendored Normal file
View File

@ -0,0 +1,50 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
AAvatar: typeof import('ant-design-vue/es')['Avatar']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACol: typeof import('ant-design-vue/es')['Col']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
ALayout: typeof import('ant-design-vue/es')['Layout']
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ATable: typeof import('ant-design-vue/es')['Table']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
AUpload: typeof import('ant-design-vue/es')['Upload']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

BIN
dist616.zip Normal file

Binary file not shown.

BIN
dist6月9日.zip Normal file

Binary file not shown.

13
index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link rel="icon" type="image/svg+xml" href="/vite.svg"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>青橙校园</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

12839
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

42
package.json Normal file
View File

@ -0,0 +1,42 @@
{
"name": "campusexpressdelivery",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@surely-vue/table": "^5.0.2",
"@vueup/vue-quill": "^1.0.0",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-vue": "^5.1.12",
"ant-design-vue": "^4.2.6",
"axios": "^1.7.7",
"crypto-js": "^4.2.0",
"element-plus": "^2.8.8",
"jsencrypt": "^3.3.2",
"pinia": "^2.2.6",
"pinia-plugin-persistedstate": "^4.1.3",
"quill": "^1.3.7",
"quill-image-resize-module": "^3.0.0",
"quill-image-resize-module--fix-imports-error": "^3.0.0",
"vue": "^3.5.12",
"vue-quill-editor": "^3.0.6",
"vue-router": "^4.4.5"
},
"devDependencies": {
"@types/crypto-js": "^4.2.2",
"@types/quill": "^2.0.14",
"@vitejs/plugin-vue": "^5.1.4",
"sass-embedded": "^1.81.0",
"typescript": "~5.6.2",
"unplugin-auto-import": "^0.18.5",
"unplugin-vue-components": "^0.27.4",
"vite": "^5.4.10",
"vite-plugin-commonjs": "^0.10.4",
"vue-tsc": "^2.1.8"
}
}

1
public/vite.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
qingcheng-Web Submodule

Submodule qingcheng-Web added at 7351f8522b

13
src/App.vue Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
<template>
测试页面
</template>

369
src/view/Login.vue Normal file
View 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
View 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>

View File

@ -0,0 +1,4 @@
<template>
<div>465</div>
</template>

View 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>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>链接课程</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
</script>
<template>
<div>本地课程</div>
</template>
<style scoped>
</style>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
<template>
123
</template>

View File

@ -0,0 +1,4 @@
<template>
789
</template>

16
src/vite-env.d.ts vendored Normal file
View 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
// }

27
tsconfig.app.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"allowSyntheticDefaultImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

33
tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
//加密
// "module": "ES2020",
// "types": ["vite/client"],
// "moduleResolution": "node",
// "target": "ES2020",
// "strict": true,
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

11
tsconfig.node.json Normal file
View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

File diff suppressed because one or more lines are too long

1
tsconfig.tsbuildinfo Normal file
View File

@ -0,0 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/api/imageurl.ts","./src/api/myaxios.ts","./src/router/index.ts","./src/router/routes.ts","./src/store/index.ts","./src/store/userstore.ts","./src/types/wangeditor.d.ts","./src/app.vue","./src/layout/managelayout.vue","./src/layout/manage/manageheader.vue","./src/layout/manage/managesidebar.vue","./src/view/index.vue","./src/view/login.vue","./src/view/test.vue","./src/view/community/community.vue","./src/view/components/richtexteditor.vue","./src/view/course/linkedcourse.vue","./src/view/course/localcurriculum.vue","./src/view/project/addproject.vue","./src/view/project/addprojectnotice.vue","./src/view/project/moneydetail.vue","./src/view/project/noticedetail.vue","./src/view/project/project.vue","./src/view/project/projectdetail.vue","./src/view/project/projectnotice.vue","./src/view/project/promotioncode.vue","./src/view/settlement/applicationrecord.vue","./src/view/settlement/moneyrecord.vue","./src/view/userlist/userlist.vue","./src/view/work/workdetail.vue","./src/view/work/worklist.vue"],"version":"5.6.3"}

2
vite.config.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
declare const _default: import("vite").UserConfig;
export default _default;

38
vite.config.js Normal file
View File

@ -0,0 +1,38 @@
// import {defineConfig} from 'vite'
// import vue from '@vitejs/plugin-vue'
// import Components from 'unplugin-vue-components/vite';
// import {AntDesignVueResolver} from "unplugin-vue-components/resolvers";
//
//
// export default defineConfig({
// plugins: [vue(),
// Components({
// resolvers: [
// AntDesignVueResolver({
// importStyle: false, // css in js
// }),
// ],
// }),],
// })
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
],
// optimizeDeps: {
// include: [
// "@surely-vue/table" // 显式包含依赖进行预构建
// ]
// }
});

39
vite.config.ts Normal file
View File

@ -0,0 +1,39 @@
// import {defineConfig} from 'vite'
// import vue from '@vitejs/plugin-vue'
// import Components from 'unplugin-vue-components/vite';
// import {AntDesignVueResolver} from "unplugin-vue-components/resolvers";
//
//
// export default defineConfig({
// plugins: [vue(),
// Components({
// resolvers: [
// AntDesignVueResolver({
// importStyle: false, // css in js
// }),
// ],
// }),],
// })
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
],
// optimizeDeps: {
// include: [
// "@surely-vue/table" // 显式包含依赖进行预构建
// ]
// }
})