finish init again
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal 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
5
README.md
Normal 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
10
auto-imports.d.ts
vendored
Normal 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
50
components.d.ts
vendored
Normal 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
BIN
dist616.zip
Normal file
Binary file not shown.
BIN
dist6月9日.zip
Normal file
BIN
dist6月9日.zip
Normal file
Binary file not shown.
13
index.html
Normal file
13
index.html
Normal 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
12839
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
package.json
Normal file
42
package.json
Normal 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
1
public/vite.svg
Normal 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
1
qingcheng-Web
Submodule
Submodule qingcheng-Web added at 7351f8522b
13
src/App.vue
Normal file
13
src/App.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<template>
|
||||||
|
<a-config-provider :locale="zhCN">
|
||||||
|
<router-view/>
|
||||||
|
</a-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import zhCN from "ant-design-vue/es/locale/zh_CN";
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import 'dayjs/locale/zh-cn';
|
||||||
|
|
||||||
|
dayjs.locale('zh-cn');
|
||||||
|
</script>
|
4
src/api/ImageUrl.ts
Normal file
4
src/api/ImageUrl.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// export const downLoadImage = 'http://1.94.237.210:3457/file/download/'
|
||||||
|
export const downLoadImage = 'http://27.30.77.229:9092/file/download/'
|
||||||
|
|
||||||
|
|
32
src/api/myAxios.ts
Normal file
32
src/api/myAxios.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// 创建实例时配置默认值
|
||||||
|
import axios from "axios";
|
||||||
|
import router from "../router";
|
||||||
|
|
||||||
|
// const viteEnv = import.meta.env;
|
||||||
|
|
||||||
|
const myAxios = axios.create({
|
||||||
|
withCredentials: true,
|
||||||
|
//baseURL:'http://localhost:9091'
|
||||||
|
// baseURL:'http://1.94.237.210:3457'
|
||||||
|
//baseURL:'http://1.94.237.210:8088'
|
||||||
|
// baseURL:'http://27.30.77.229:9091/'
|
||||||
|
baseURL:'http://27.30.77.229:9092/'
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
myAxios.interceptors.request.use(function (config) {
|
||||||
|
return config;
|
||||||
|
}, function (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
myAxios.interceptors.response.use(function (response: any) {
|
||||||
|
if (response.data.code === 40100) {
|
||||||
|
router.replace('/')
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
}, function (error) {
|
||||||
|
return Promise.reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default myAxios;
|
BIN
src/assets/Login.png
Normal file
BIN
src/assets/Login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 594 KiB |
BIN
src/assets/login/hidePassword.png
Normal file
BIN
src/assets/login/hidePassword.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
src/assets/login/showPassword.png
Normal file
BIN
src/assets/login/showPassword.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
48
src/layout/ManageLayout.vue
Normal file
48
src/layout/ManageLayout.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<div id="manage">
|
||||||
|
<a-layout has-sider>
|
||||||
|
<a-layout-sider :style="{ width: '200px' }">
|
||||||
|
<ManageSidebar/>
|
||||||
|
</a-layout-sider>
|
||||||
|
<a-layout>
|
||||||
|
<a-layout-header :style="{ background: '#fff', padding: 0}">
|
||||||
|
<ManageHeader/>
|
||||||
|
</a-layout-header>
|
||||||
|
<a-layout-content class="main">
|
||||||
|
<router-view :key="key" class="main-card"/>
|
||||||
|
</a-layout-content>
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ManageHeader from "./manage/ManageHeader.vue";
|
||||||
|
import ManageSidebar from "./manage/ManageSidebar.vue";
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
|
||||||
|
const key = () => {
|
||||||
|
return route.path + Math.random();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
#manage {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
padding: 5px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
|
flex: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
208
src/layout/manage/ManageHeader.vue
Normal file
208
src/layout/manage/ManageHeader.vue
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="manage-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<a-breadcrumb class="breadcrumb">
|
||||||
|
<a-breadcrumb-item>青橙校园后台管理系统</a-breadcrumb-item>
|
||||||
|
<a-breadcrumb-item class="routeName">{{ route.name }}</a-breadcrumb-item>
|
||||||
|
</a-breadcrumb>
|
||||||
|
</div>
|
||||||
|
<div class="header-right">
|
||||||
|
<!-- <div class="bell">-->
|
||||||
|
<!-- <BellTwoTone/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<div class="name">
|
||||||
|
{{ store.loginUser.userRole === "notLogin" ? '未登录' : store.loginUser.userRole }}
|
||||||
|
</div>
|
||||||
|
<div class="user">
|
||||||
|
<a-dropdown>
|
||||||
|
<a-avatar :size="40" class="user-avatar" :src="downLoadImage+store.loginUser.avatarUrl"/>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu>
|
||||||
|
<a-menu-item @click="logout">
|
||||||
|
<LogoutOutlined />
|
||||||
|
退出登录
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item @click="showDrawer">
|
||||||
|
<UserOutlined />
|
||||||
|
个人中心
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-drawer
|
||||||
|
title="个人中心"
|
||||||
|
:placement="placement"
|
||||||
|
:closable="false"
|
||||||
|
:open="open"
|
||||||
|
@close="onClose"
|
||||||
|
class="custom-class"
|
||||||
|
>
|
||||||
|
<a-avatar :size="64" :src="downLoadImage+store.loginUser.avatarUrl"></a-avatar>
|
||||||
|
|
||||||
|
<div class="message">
|
||||||
|
<p class="firstmessage">昵称:{{ store.loginUser.nickName }}</p>
|
||||||
|
<p>账号:{{ store.loginUser.username }}</p>
|
||||||
|
<p>账号权限:{{store.loginUser.userRole}}</p>
|
||||||
|
<p>联系方式:{{store.loginUser.phone}}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import {onBeforeMount} from 'vue'
|
||||||
|
import {userStore} from "../../store/userStore.ts";
|
||||||
|
import {UserOutlined,LogoutOutlined} from '@ant-design/icons-vue';
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import type { DrawerProps } from 'ant-design-vue';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const store = userStore()
|
||||||
|
|
||||||
|
|
||||||
|
const placement = ref<DrawerProps['placement']>('right');
|
||||||
|
const open = ref<boolean>(false);
|
||||||
|
const checkLoginStatus = () => {
|
||||||
|
// 检查store中的登录状态
|
||||||
|
if (store.loginUser.userRole === "notLogin") {
|
||||||
|
console.log("未检测到登录状态,跳转到登录页");
|
||||||
|
// 使用replace替换当前路由,禁止返回
|
||||||
|
router.replace({ path: '/' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 生命周期钩子:在组件挂载前检查登录状态
|
||||||
|
onBeforeMount(() => {
|
||||||
|
checkLoginStatus();
|
||||||
|
});
|
||||||
|
const showDrawer = () => {
|
||||||
|
open.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
open.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登出
|
||||||
|
*/
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// 严格遵循接口文档路径大小写
|
||||||
|
const res: any = await myAxios.get(
|
||||||
|
"/userInfo/logout",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization:token,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('登出响应:', res);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
sessionStorage.clear();
|
||||||
|
store.$reset();
|
||||||
|
|
||||||
|
|
||||||
|
await router.replace('/');
|
||||||
|
message.success('登出成功');
|
||||||
|
} else {
|
||||||
|
|
||||||
|
message.error(`登出失败:${res.message || '未知错误'}`);
|
||||||
|
console.error('业务逻辑错误:', res);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求异常:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.manage-header {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 2px solid #ccced7;
|
||||||
|
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left .breadcrumb {
|
||||||
|
color: black;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.routeName {
|
||||||
|
font-size: 13px;
|
||||||
|
padding-top: 10px;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
}
|
||||||
|
.header-right {
|
||||||
|
display: flex;
|
||||||
|
margin-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right .bell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right .name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right .user {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-avatar:hover {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-class img{
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
float: right;
|
||||||
|
margin-right: 100px;
|
||||||
|
}
|
||||||
|
.firstmessage {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
128
src/layout/manage/ManageSidebar.vue
Normal file
128
src/layout/manage/ManageSidebar.vue
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<a-menu
|
||||||
|
v-model:selectedKeys="selectedKeys"
|
||||||
|
mode="inline"
|
||||||
|
style="max-height: 200vh; min-height: 100vh;background-color: #ffe7ba"
|
||||||
|
@click="handleClick"
|
||||||
|
>
|
||||||
|
<a-menu-item key="/index">
|
||||||
|
<PieChartOutlined />
|
||||||
|
<span>首页</span>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-menu-item key="/userList">
|
||||||
|
<UserOutlined />
|
||||||
|
<span>用户列表</span>
|
||||||
|
</a-menu-item>
|
||||||
|
<a-sub-menu>
|
||||||
|
<template #title>
|
||||||
|
<span>
|
||||||
|
<UserOutlined />
|
||||||
|
<span>项目管理</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<a-menu-item key="/project">接单管理</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
|
||||||
|
<a-sub-menu>
|
||||||
|
<template #title>
|
||||||
|
<span>
|
||||||
|
<CommentOutlined />
|
||||||
|
<span>结算管理</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<a-menu-item key="/applicationRecord">推广码申请记录</a-menu-item>
|
||||||
|
<a-menu-item key="/withdrawalApplicationRecord">提现申请记录</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
<a-sub-menu>
|
||||||
|
<template #title>
|
||||||
|
<span>
|
||||||
|
<ReadOutlined />
|
||||||
|
<span>课程管理</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<a-menu-item key="/localCurriculum">本地课程</a-menu-item>
|
||||||
|
<a-menu-item key="/linkedCourse">链接课程</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
<a-sub-menu>
|
||||||
|
<template #title>
|
||||||
|
<span>
|
||||||
|
<FieldTimeOutlined />
|
||||||
|
<span>勤工俭学</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<a-menu-item key="/workList">工作列表</a-menu-item>
|
||||||
|
<a-menu-item key="/workDetail">工作详情</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
|
||||||
|
<a-menu-item key="/community">
|
||||||
|
<CommentOutlined />
|
||||||
|
<span>社群</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {onMounted, ref} from "vue";
|
||||||
|
import {UserOutlined, FieldTimeOutlined,ReadOutlined,CommentOutlined,PieChartOutlined} from '@ant-design/icons-vue';
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
// 选中侧边栏
|
||||||
|
const selectedKeys = ref<string[]>(['/userList']);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setSelectedKey()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 根据路由设置当前选中侧边栏
|
||||||
|
const setSelectedKey = () => {
|
||||||
|
selectedKeys.value = [route.path];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由跳转
|
||||||
|
const handleClick = (item: any) => {
|
||||||
|
router.push(item.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 全局菜单项样式 */
|
||||||
|
:deep(.ant-menu-item),
|
||||||
|
:deep(.ant-menu-submenu-title) {
|
||||||
|
color: rgba(0, 0, 0, 0.85) !important; /* 未选中黑色字体 */
|
||||||
|
font-weight: normal !important;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 选中项样式 */
|
||||||
|
:deep(.ant-menu-item-selected) {
|
||||||
|
background-color: #ffa940 !important;
|
||||||
|
color: white !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 鼠标悬停样式 */
|
||||||
|
:deep(.ant-menu-item:hover),
|
||||||
|
:deep(.ant-menu-submenu-title:hover) {
|
||||||
|
background-color: rgba(255, 169, 64, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子菜单箭头颜色 */
|
||||||
|
:deep(.ant-menu-submenu-arrow::before),
|
||||||
|
:deep(.ant-menu-submenu-arrow::after) {
|
||||||
|
background: rgba(0, 0, 0, 0.65) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 子菜单展开时标题样式 */
|
||||||
|
:deep(.ant-menu-submenu-selected .ant-menu-submenu-title) {
|
||||||
|
color: rgba(0, 0, 0, 0.95) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠状态下选中样式 */
|
||||||
|
:deep(.ant-menu-inline-collapsed .ant-menu-item-selected) {
|
||||||
|
background-color: #ffa940 !important;
|
||||||
|
}
|
||||||
|
</style>
|
16
src/main.ts
Normal file
16
src/main.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import {createApp} from 'vue'
|
||||||
|
import './style.css'
|
||||||
|
import App from './App.vue'
|
||||||
|
import pinia from "./store";
|
||||||
|
import router from "./router";
|
||||||
|
import 'quill/dist/quill.snow.css';
|
||||||
|
import '@vueup/vue-quill/dist/vue-quill.snow.css'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
// 配置路由
|
||||||
|
app.use(router)
|
||||||
|
// 配置pinia
|
||||||
|
app.use(pinia)
|
||||||
|
// 挂在实例
|
||||||
|
app.mount('#app')
|
||||||
|
|
11
src/router/index.ts
Normal file
11
src/router/index.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import {createRouter, createWebHashHistory} from "vue-router";
|
||||||
|
import {routes} from "./routes";
|
||||||
|
|
||||||
|
// 创建路由实例并传递 `routes` 配置
|
||||||
|
const router = createRouter({
|
||||||
|
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes, // `routes: routes` 的缩写
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
112
src/router/routes.ts
Normal file
112
src/router/routes.ts
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// 将路由规则 routes 导出
|
||||||
|
export const routes = [
|
||||||
|
// 全局路由(无需嵌套上左右整体布局)
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import("../view/Login.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/test',
|
||||||
|
name: '全局测试页面',
|
||||||
|
component: () => import("../view/Test.vue"),
|
||||||
|
},
|
||||||
|
// 管理端
|
||||||
|
{
|
||||||
|
path: '/manage',
|
||||||
|
component: () => import("../layout/ManageLayout.vue"),
|
||||||
|
children: [
|
||||||
|
// 首页
|
||||||
|
{
|
||||||
|
path: '/index',
|
||||||
|
name: '首页',
|
||||||
|
component: () => import("../view/Index.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/localCurriculum',
|
||||||
|
name: '本地课程',
|
||||||
|
component: () => import("../view/course/localCurriculum.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/linkedCourse',
|
||||||
|
name: '链接课程',
|
||||||
|
component: () => import("../view/course/linkedCourse.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/workList',
|
||||||
|
name: '工作列表',
|
||||||
|
component: () => import("../view/work/workList.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/workDetail',
|
||||||
|
name: '工作详情',
|
||||||
|
component: () => import("../view/work/workDetail.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/community',
|
||||||
|
name: '社群',
|
||||||
|
component: () => import("../view/community/community.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/userList',
|
||||||
|
name: '用户列表',
|
||||||
|
component: () => import("../view/userList/userList.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/project',
|
||||||
|
name: '项目管理',
|
||||||
|
component: () => import("../view/project/project.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/projectDetail',
|
||||||
|
name: '项目详情',
|
||||||
|
component: () => import("../view/project/projectDetail.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/addProject',
|
||||||
|
name: '新增项目',
|
||||||
|
component: () => import("../view/project/addProject.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/moneyDetail',
|
||||||
|
name: '项目明细',
|
||||||
|
component: () => import("../view/project/moneyDetail.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/projectNotice',
|
||||||
|
name: '项目通知',
|
||||||
|
component: () => import("../view/project/projectNotice.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/promotionCode',
|
||||||
|
name: '推广码',
|
||||||
|
component: () => import("../view/project/promotionCode.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/applicationRecord',
|
||||||
|
name: '推广码记录',
|
||||||
|
component: () => import("../view/settlement/applicationRecord.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/addprojectNotice',
|
||||||
|
name: '新增项目通知',
|
||||||
|
component: () => import("../view/project/addprojectNotice.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/moneyRecord',
|
||||||
|
name: '项目结算记录',
|
||||||
|
component: () => import("../view/settlement/moneyRecord.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/noticeDetail',
|
||||||
|
name: '项目通知详情',
|
||||||
|
component: () => import("../view/project/noticeDetail.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/withdrawalApplicationRecord',
|
||||||
|
name: '提现申请记录',
|
||||||
|
component: () => import("../view/settlement/withdrawalApplicationRecord.vue"),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
10
src/store/index.ts
Normal file
10
src/store/index.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import {createPinia} from 'pinia'
|
||||||
|
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
pinia.use(piniaPluginPersistedstate)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default pinia
|
72
src/store/userStore.ts
Normal file
72
src/store/userStore.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import myAxios from "../api/myAxios";
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
export const userStore = defineStore('user', {
|
||||||
|
state: () => ({
|
||||||
|
loginUser: {
|
||||||
|
username: '未登录',
|
||||||
|
avatarUrl: '',
|
||||||
|
userRole: 'notLogin',
|
||||||
|
phone: '',
|
||||||
|
nickName:''
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
persist: true,
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 获取登录用户信息
|
||||||
|
async getLoginUser() {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) {
|
||||||
|
message.warning('请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res:any = await myAxios.get('/userInfo/get/jwt/web', {
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(res)
|
||||||
|
if (res.code === 1) {
|
||||||
|
console.log(res)
|
||||||
|
this.updateUser({
|
||||||
|
username: res.data.userAccount,
|
||||||
|
avatarUrl: res.data.userAvatar,
|
||||||
|
userRole: res.data.userRole,
|
||||||
|
phone: res.data.phoneNumber,
|
||||||
|
nickName:res.data.nickName
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '获取用户信息失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
message.error('获取用户信息失败,请检查网络连接');
|
||||||
|
this.clearUser();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
updateUser(payload: any) {
|
||||||
|
this.loginUser = {
|
||||||
|
...this.loginUser,
|
||||||
|
...payload
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除用户信息
|
||||||
|
clearUser() {
|
||||||
|
this.loginUser = {
|
||||||
|
username: '未登录',
|
||||||
|
avatarUrl: '',
|
||||||
|
userRole: 'notLogin',
|
||||||
|
phone: '',
|
||||||
|
nickName: ''
|
||||||
|
};
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
48
src/style.css
Normal file
48
src/style.css
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
html {
|
||||||
|
/* 滚动时采用平滑过渡 */
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 管理页面全局样式 */
|
||||||
|
|
||||||
|
.main-card {
|
||||||
|
min-height: calc(100vh - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
display: flex;
|
||||||
|
background-color: white;
|
||||||
|
height: auto;
|
||||||
|
box-shadow: 0 0 1px 1px #dedede;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.middle-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-box {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 0 1px 1px #dedede;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
margin: 20px 0 20px 0;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.txt {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 500px;
|
||||||
|
}
|
7
src/types/wangeditor.d.ts
vendored
Normal file
7
src/types/wangeditor.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
// src/types/wangeditor.d.ts
|
||||||
|
declare module '@wangeditor/editor-for-vue' {
|
||||||
|
import { Component } from 'vue'
|
||||||
|
export const Editor: Component
|
||||||
|
export const Toolbar: Component
|
||||||
|
// 如果库有其他导出,需一并声明
|
||||||
|
}
|
3
src/view/Index.vue
Normal file
3
src/view/Index.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
测试页面
|
||||||
|
</template>
|
369
src/view/Login.vue
Normal file
369
src/view/Login.vue
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
<template>
|
||||||
|
<div id="login">
|
||||||
|
<form>
|
||||||
|
<div class="box" @submit.prevent>
|
||||||
|
<h2>欢迎登录青橙校园管理端</h2>
|
||||||
|
<div class="input-box">
|
||||||
|
|
||||||
|
<input type="text" placeholder="账号" v-model="userAccount"/>
|
||||||
|
</div>
|
||||||
|
<div class="input-box">
|
||||||
|
|
||||||
|
<input class="password" :type="type" placeholder="密码" v-model="userPassword"/>
|
||||||
|
<!-- <div @click="showPwd">-->
|
||||||
|
<!-- <img src="../assets/login/hidePassword.png" v-show="!showPassword" alt=""/>-->
|
||||||
|
<!-- <img src="../assets/login/showPassword.png" v-show="showPassword" alt=""/>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</div>
|
||||||
|
<div class="error-messages" v-if="userPassword && passwordErrors.length">
|
||||||
|
<!-- <div class="error-messages" v-if="false">-->
|
||||||
|
<div v-for="error in passwordErrors" :key="error" class="error-message">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="btn-box">
|
||||||
|
<div>
|
||||||
|
<button @click="onLogin" type="button" :disabled="!isFormValid">登录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {useRouter} from 'vue-router'
|
||||||
|
import {ref, computed, watch} from 'vue'
|
||||||
|
import {userStore} from "../store/userStore.ts";
|
||||||
|
import myAxios from "../api/myAxios";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
|
||||||
|
const store = userStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
//默认闭眼图标
|
||||||
|
// let showPassword = ref(false)
|
||||||
|
//登录密码隐藏
|
||||||
|
let type = ref('password')
|
||||||
|
const userAccount = ref('');
|
||||||
|
const userPassword = ref('');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 验证
|
||||||
|
* */
|
||||||
|
|
||||||
|
const passwordErrors = ref<string[]>([]);
|
||||||
|
|
||||||
|
const validatePassword = () => {
|
||||||
|
const errors: string[] = [];
|
||||||
|
const password = userPassword.value;
|
||||||
|
|
||||||
|
if (password.length < 6 || password.length > 11) {
|
||||||
|
errors.push('密码长度需为6到11位');
|
||||||
|
}
|
||||||
|
if (!/[A-Z]/.test(password)) {
|
||||||
|
errors.push('必须包含大写字母');
|
||||||
|
}
|
||||||
|
if (!/[a-z]/.test(password)) {
|
||||||
|
errors.push('必须包含小写字母');
|
||||||
|
}
|
||||||
|
if (!/[0-9]/.test(password)) {
|
||||||
|
errors.push('必须至少包含一个数字');
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordErrors.value = errors;
|
||||||
|
};
|
||||||
|
// const validatePassword = () => {
|
||||||
|
// passwordErrors.value = []; // 直接设置为空数组
|
||||||
|
// // 注释原有验证逻辑
|
||||||
|
// };
|
||||||
|
|
||||||
|
const isFormValid = computed(() => {
|
||||||
|
return userAccount.value.trim() !== '' &&
|
||||||
|
userPassword.value !== '' &&
|
||||||
|
passwordErrors.value.length === 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// const isFormValid = computed(() => {
|
||||||
|
// return userAccount.value.trim() !== '' &&
|
||||||
|
// userPassword.value !== ''
|
||||||
|
// // && passwordErrors.value.length === 0
|
||||||
|
// });
|
||||||
|
|
||||||
|
watch(userPassword, validatePassword);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录
|
||||||
|
*/
|
||||||
|
const onLogin = async () => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
"/userInfo/login",
|
||||||
|
{
|
||||||
|
userAccount: userAccount.value,
|
||||||
|
userPassword: userPassword.value // 发送加密后的密码
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'AfterScript': 'required-script'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(res);
|
||||||
|
if (res.code === 1 && res?.data) {
|
||||||
|
localStorage.setItem('token', res.data);
|
||||||
|
myAxios.defaults.headers.common['Authorization'] = res.data;
|
||||||
|
await store.getLoginUser();
|
||||||
|
if (store.loginUser) {
|
||||||
|
router.push('/index');
|
||||||
|
message.success('登录成功');
|
||||||
|
} else {
|
||||||
|
message.error('获取用户信息失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(`登录失败:${res.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
message.error('登录失败,请稍后再试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示密码
|
||||||
|
*/
|
||||||
|
// const showPwd = () => {
|
||||||
|
// showPassword.value = !showPassword.value
|
||||||
|
// if (showPassword.value == false) {
|
||||||
|
// type.value = 'password'
|
||||||
|
// } else {
|
||||||
|
// type.value = 'text'
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#login::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: url('../assets/Login.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
opacity: 0.3;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box {
|
||||||
|
z-index:1;
|
||||||
|
width: 450px;
|
||||||
|
height: 350px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
border-left: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
background: rgba(50, 50, 50, 0.2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
background-image: url('../assets/Login.png');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 100% 100%;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box > h2 {
|
||||||
|
color: #fff;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .input-box {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .input-box input {
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 250px;
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 15px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
outline: none;
|
||||||
|
padding: 0 12px;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .input-box .eye {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
bottom: 32px;
|
||||||
|
right: -210px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .input-box img {
|
||||||
|
float: right;
|
||||||
|
position: relative;
|
||||||
|
bottom: 32px;
|
||||||
|
right: 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .input-box input:focus {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box {
|
||||||
|
width: 250px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box > a {
|
||||||
|
outline: none;
|
||||||
|
display: block;
|
||||||
|
width: 250px;
|
||||||
|
text-align: end;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box > a:hover {
|
||||||
|
color: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box > div {
|
||||||
|
margin-top: 10px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box > div > button {
|
||||||
|
outline: none;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
border-radius: 5px;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box > div > button:nth-of-type(1) {
|
||||||
|
width: 250px;
|
||||||
|
height: 35px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
background: rgba(217, 224, 231, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box > div > button:nth-of-type(2) {
|
||||||
|
width: 120px;
|
||||||
|
height: 35px;
|
||||||
|
margin-left: 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
background: rgba(64, 149, 229, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box .btn-box > div > button:hover {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||||
|
background: rgb(249, 249, 208);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1440px) {
|
||||||
|
.container {
|
||||||
|
width: 28%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1366px) {
|
||||||
|
.container {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.container {
|
||||||
|
width: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agileheader h1 {
|
||||||
|
font-size: 41px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container h2 {
|
||||||
|
font-size: 27px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
.container {
|
||||||
|
width: 49%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.error-messages {
|
||||||
|
width: 250px;
|
||||||
|
margin: -8px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
color: #ff0000;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 禁用按钮样式 */
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: rgba(217, 224, 231, 0.3) !important;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled:hover {
|
||||||
|
background: rgba(217, 224, 231, 0.3) !important;
|
||||||
|
color: rgba(255, 255, 255, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
48
src/view/Test.vue
Normal file
48
src/view/Test.vue
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-alert {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #ffe3e3;
|
||||||
|
color: #ff4444;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-thead > tr > th) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row:hover) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
</style>
|
4
src/view/community/community.vue
Normal file
4
src/view/community/community.vue
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
<div>465</div>
|
||||||
|
</template>
|
||||||
|
|
133
src/view/components/RichTextEditor.vue
Normal file
133
src/view/components/RichTextEditor.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<div class="editor-container">
|
||||||
|
<Toolbar
|
||||||
|
:editor="editorRef"
|
||||||
|
:defaultConfig="toolbarConfig"
|
||||||
|
class="toolbar"
|
||||||
|
/>
|
||||||
|
<Editor
|
||||||
|
v-model="valueHtml"
|
||||||
|
:defaultConfig="editorConfig"
|
||||||
|
:mode="mode"
|
||||||
|
class="editor"
|
||||||
|
@onCreated="handleCreated"
|
||||||
|
@onFocus="handleFocus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { shallowRef, ref, watch, onBeforeUnmount } from 'vue'
|
||||||
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
|
import type { IDomEditor } from '@wangeditor/editor'
|
||||||
|
import '@wangeditor/editor/dist/css/style.css'
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
disable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'content-change'])
|
||||||
|
|
||||||
|
const editorRef = shallowRef<IDomEditor>()
|
||||||
|
const valueHtml = ref(props.modelValue)
|
||||||
|
const mode = 'default'
|
||||||
|
|
||||||
|
const toolbarConfig = {
|
||||||
|
excludeKeys: [
|
||||||
|
'insertVideo',
|
||||||
|
'uploadVideo',
|
||||||
|
'codeBlock',
|
||||||
|
'|',
|
||||||
|
'group-more-style'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorConfig = {
|
||||||
|
placeholder: '请输入内容...',
|
||||||
|
readOnly: props.disable,
|
||||||
|
MENU_CONF: {
|
||||||
|
uploadImage: {
|
||||||
|
allowedFileTypes: ['image/*'],
|
||||||
|
async customUpload(file: File, insertFn: (url: string) => void) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('biz', 'richText')
|
||||||
|
formData.append('file', file)
|
||||||
|
formData.append('new', '1')
|
||||||
|
formData.append('Ctrl', 'upload')
|
||||||
|
|
||||||
|
const res: any = await myAxios.post('/file/upload', formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
// 拼接完整 URL 地址再插入到富文本中
|
||||||
|
const imageUrl = 'http://27.30.77.229:9092/file/download/' + res.data
|
||||||
|
insertFn(imageUrl)
|
||||||
|
} else {
|
||||||
|
console.error('上传失败:', res.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片上传失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFocus = () => {
|
||||||
|
editorRef.value?.restoreSelection()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCreated = (editor: IDomEditor) => {
|
||||||
|
editorRef.value = editor
|
||||||
|
|
||||||
|
editor.on('menuClick', (menu: { key: string }) => { // ✅ 显式类型声明
|
||||||
|
if (menu.key === 'headerSelect') {
|
||||||
|
nextTick(() => {
|
||||||
|
editor.restoreSelection()
|
||||||
|
editor.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.modelValue, (newVal) => {
|
||||||
|
if (newVal !== valueHtml.value && editorRef.value?.isEmpty()) {
|
||||||
|
valueHtml.value = newVal
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(valueHtml, (newVal) => {
|
||||||
|
emit('update:modelValue', newVal)
|
||||||
|
emit('content-change', newVal)
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (editorRef.value) {
|
||||||
|
editorRef.value.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editor-container {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
.toolbar {
|
||||||
|
border-bottom: 1px solid #e2e8f0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
11
src/view/course/linkedCourse.vue
Normal file
11
src/view/course/linkedCourse.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>链接课程</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
11
src/view/course/localCurriculum.vue
Normal file
11
src/view/course/localCurriculum.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>本地课程</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
790
src/view/project/addProject.vue
Normal file
790
src/view/project/addProject.vue
Normal file
@ -0,0 +1,790 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit" class="modern-form">
|
||||||
|
<h2 class="form-title">新建推广项目</h2>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<!-- 左列 - 手机预览区域 -->
|
||||||
|
<div class="form-column phone-preview-container">
|
||||||
|
<div class="phone-frame">
|
||||||
|
<div class="phone-header">
|
||||||
|
<div class="phone-camera"></div>
|
||||||
|
<div class="phone-speaker"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="phone-screen">
|
||||||
|
<div class="phone-content">
|
||||||
|
<!-- 项目信息 -->
|
||||||
|
<div class="phone-project-info">
|
||||||
|
<h2 class="phone-project-title">{{ formData.projectName }}</h2>
|
||||||
|
<div class="phone-project-image" :style="{ backgroundImage: `url(${formData.projectImage})` }"></div>
|
||||||
|
<p class="phone-project-desc">{{ formData.projectDescription }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本内容区域 -->
|
||||||
|
<div class="phone-sections">
|
||||||
|
<!-- 结算说明 -->
|
||||||
|
<div class="phone-section">
|
||||||
|
<h3 class="phone-section-title">结算说明</h3>
|
||||||
|
<div class="phone-section-content" v-html="formData.settlementDesc"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 项目说明 -->
|
||||||
|
<div class="phone-section">
|
||||||
|
<h3 class="phone-section-title">项目说明</h3>
|
||||||
|
<div class="phone-section-content" v-html="formData.projectDesc"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 项目流程 -->
|
||||||
|
<div class="phone-section">
|
||||||
|
<h3 class="phone-section-title">项目流程</h3>
|
||||||
|
<div class="phone-section-content" v-html="formData.projectFlow"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 申请推广码说明 -->
|
||||||
|
<div class="phone-section">
|
||||||
|
<h3 class="phone-section-title">申请推广码说明</h3>
|
||||||
|
<div class="phone-section-content" v-html="formData.applyPromoCodeDesc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="phone-home-button"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右列 - 富文本编辑区域 -->
|
||||||
|
<div class="form-column">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">项目名称</span>
|
||||||
|
<input
|
||||||
|
v-model="formData.projectName"
|
||||||
|
type="text"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入项目名称"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">项目图片</span>
|
||||||
|
<div class="file-upload">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="file-input"
|
||||||
|
accept="image/*"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
ref="fileInput"
|
||||||
|
>
|
||||||
|
<div class="upload-button">
|
||||||
|
<span v-if="!formData.projectImage">点击上传图片</span>
|
||||||
|
<span v-else class="file-name">已选择:{{ fileName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">结算周期(天)</span>
|
||||||
|
<input
|
||||||
|
v-model.number="formData.projectSettlementCycle"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入结算周期"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">最大推广人数</span>
|
||||||
|
<input
|
||||||
|
v-model.number="formData.maxPromoterCount"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入最大人数"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">项目状态</span>
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<select
|
||||||
|
v-model="formData.projectStatus"
|
||||||
|
class="select-field"
|
||||||
|
>
|
||||||
|
<option value="running">运行中</option>
|
||||||
|
<option value="full">人数已满</option>
|
||||||
|
<option value="paused">已暂停</option>
|
||||||
|
</select>
|
||||||
|
<div class="select-arrow">▼</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">项目简介</span>
|
||||||
|
<div class="relative">
|
||||||
|
<textarea
|
||||||
|
v-model="formData.projectDescription"
|
||||||
|
class="textarea-field"
|
||||||
|
placeholder="请输入项目简介..."
|
||||||
|
maxlength="30"
|
||||||
|
></textarea>
|
||||||
|
<div class="absolute right-2 bottom-2 text-xs text-gray-500">
|
||||||
|
{{ formData.projectDescription.length }}/30
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-text-container">
|
||||||
|
<div class="rich-text-columns">
|
||||||
|
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">结算说明</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="formData.settlementDesc"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html:any) => formData.settlementDesc = html"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">项目说明</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="formData.projectDesc"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html:any) => formData.projectDesc = html"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">项目流程</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="formData.projectFlow"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html:any) => formData.projectFlow = html"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">申请推广码说明</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="formData.applyPromoCodeDesc"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html:any) => formData.applyPromoCodeDesc = html"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="submit-button">
|
||||||
|
<span>立即创建</span>
|
||||||
|
<div class="button-sparkles"></div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, onMounted, nextTick,ref } from 'vue';
|
||||||
|
import RichTextEditor from '../components/RichTextEditor.vue';
|
||||||
|
import '@vueup/vue-quill/dist/vue-quill.snow.css';
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import router from "../../router";
|
||||||
|
|
||||||
|
|
||||||
|
interface ProjectForm {
|
||||||
|
projectName: string;
|
||||||
|
projectImage: string;
|
||||||
|
projectSettlementCycle: number;
|
||||||
|
maxPromoterCount: number;
|
||||||
|
projectStatus: 'running' | 'full' | 'paused';
|
||||||
|
projectDescription: string;
|
||||||
|
settlementDesc: string;
|
||||||
|
projectDesc: string;
|
||||||
|
projectFlow: string;
|
||||||
|
applyPromoCodeDesc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const handleFileUpload = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 重命名局部变量避免冲突(修复错误❷)
|
||||||
|
const uploadFormData = new FormData();
|
||||||
|
uploadFormData.append('biz', 'project');
|
||||||
|
uploadFormData.append('file', file);
|
||||||
|
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post('/file/upload', uploadFormData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
// 正确访问响应式对象
|
||||||
|
formData.projectImage = res.data;
|
||||||
|
fileName.value = file.name;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
alert('文件上传失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 添加文件名响应式变量
|
||||||
|
const fileName = ref('');
|
||||||
|
|
||||||
|
// 响应式表单数据
|
||||||
|
const formData = reactive<ProjectForm>({
|
||||||
|
projectName: '',
|
||||||
|
projectSettlementCycle: 2,
|
||||||
|
maxPromoterCount: 200,
|
||||||
|
projectStatus: 'running',
|
||||||
|
projectDescription: '',
|
||||||
|
settlementDesc: '',
|
||||||
|
projectDesc: '',
|
||||||
|
projectFlow: '',
|
||||||
|
applyPromoCodeDesc: '',
|
||||||
|
projectImage: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 生命周期钩子
|
||||||
|
onMounted(() => {
|
||||||
|
// 初始处理
|
||||||
|
nextTick(() => {
|
||||||
|
// 为手机预览添加初始样式
|
||||||
|
const phoneScreen = document.querySelector('.phone-screen');
|
||||||
|
if (phoneScreen) {
|
||||||
|
// 添加阴影和动画效果
|
||||||
|
phoneScreen.classList.add('animate-fade-in');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
|
||||||
|
const res:any = await myAxios.post(`/project/add`,
|
||||||
|
JSON.stringify(formData),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
alert('项目创建成功!');
|
||||||
|
// router.back();
|
||||||
|
router.push('/project')
|
||||||
|
Object.assign(formData, {
|
||||||
|
projectName: '',
|
||||||
|
projectSettlementCycle: 2,
|
||||||
|
maxPromoterCount: 200,
|
||||||
|
projectStatus: 'running',
|
||||||
|
projectDescription: '',
|
||||||
|
settlementDesc: '',
|
||||||
|
projectDesc: '',
|
||||||
|
projectFlow: '',
|
||||||
|
applyPromoCodeDesc: '',
|
||||||
|
projectImage: ''
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert(`创建失败:${res.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.modern-form {
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 2.5rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field,
|
||||||
|
.select-wrapper,
|
||||||
|
.textarea-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 1.2rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus,
|
||||||
|
.select-wrapper:focus-within,
|
||||||
|
.textarea-field:focus {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-upload {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 2; /* 确保文件输入在按钮上方 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px dashed #cbd5e1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-field {
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: #94a3b8;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.textarea-field {
|
||||||
|
min-height: 100px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-sparkles {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(255,255,255,0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: sparkle 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0% { transform: scale(0) translate(0,0); }
|
||||||
|
50% { transform: scale(1) translate(100px, -50px); }
|
||||||
|
100% { transform: scale(0) translate(200px, -100px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-container {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr)); /* 修正列宽分配 */
|
||||||
|
gap: 1rem; /* 减小间距 */
|
||||||
|
align-items: start; /* 顶部对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem; /* 减小标签与编辑器间距 */
|
||||||
|
height: 100%; /* 保持等高布局 */
|
||||||
|
|
||||||
|
}
|
||||||
|
.rich-text-group:focus-within {
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 响应式调整 */
|
||||||
|
@media (max-width: 1440px) {
|
||||||
|
.rich-text-columns {
|
||||||
|
gap: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.rich-text-columns {
|
||||||
|
grid-template-columns: repeat(2, 1fr); /* 更早切换为2列 */
|
||||||
|
gap: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.rich-text-columns {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group {
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 320px;
|
||||||
|
height: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
/* 新增焦点状态 */
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #6366f1 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 调整标签与编辑器间距 */
|
||||||
|
.rich-text-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
/* 新增标签焦点状态联动 */
|
||||||
|
&:focus-within .label-text {
|
||||||
|
color: #6366f1;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持与其他输入组件的一致性 */
|
||||||
|
.rich-text-group .label-text {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b4151;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 手机预览样式 */
|
||||||
|
.phone-preview-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-frame {
|
||||||
|
position: relative;
|
||||||
|
width: 320px;
|
||||||
|
height: 640px;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 0 0 12px #1f1f1f, 0 0 30px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 180px;
|
||||||
|
height: 25px;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-camera {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #15294c;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-speaker {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 6px;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-home-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-project-info {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-project-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-project-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
border-radius: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-project-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4a5568;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-sections {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #2c3e50;
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #4a5568;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content h1,
|
||||||
|
.phone-section-content h2,
|
||||||
|
.phone-section-content h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content ul,
|
||||||
|
.phone-section-content ol {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
.file-upload {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px dashed #cbd5e1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
</style>
|
431
src/view/project/addprojectNotice.vue
Normal file
431
src/view/project/addprojectNotice.vue
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
<template>
|
||||||
|
<form @submit.prevent="handleSubmit" class="modern-form">
|
||||||
|
<h2 class="form-title">新建项目通知</h2>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<!-- 左列 - 手机预览区域 -->
|
||||||
|
<div class="form-column phone-preview-container">
|
||||||
|
<div class="phone-frame">
|
||||||
|
<div class="phone-header">
|
||||||
|
<div class="phone-camera"></div>
|
||||||
|
<div class="phone-speaker"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="phone-screen">
|
||||||
|
<div class="phone-content">
|
||||||
|
<!-- 通知预览 -->
|
||||||
|
<div class="phone-notification">
|
||||||
|
<h2 class="notification-title">{{ formData.notificationTitle }}</h2>
|
||||||
|
<div class="notification-content" v-html="formData.notificationContent"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="phone-home-button"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右列 - 编辑区域 -->
|
||||||
|
<div class="form-column">
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">通知标题</span>
|
||||||
|
<input
|
||||||
|
v-model="formData.notificationTitle"
|
||||||
|
type="text"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入通知标题"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label">
|
||||||
|
<span class="label-text">关联项目ID</span>
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model.number="formData.projectId"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
class="input-field"
|
||||||
|
required
|
||||||
|
placeholder="请输入项目ID"
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改后的富文本编辑器 -->
|
||||||
|
<div class="rich-text-container">
|
||||||
|
<div class="rich-text-columns">
|
||||||
|
<div class="rich-text-group">
|
||||||
|
<span class="label-text">通知内容</span>
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="formData.notificationContent"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html:any) => formData.notificationContent = html"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="submit-button">
|
||||||
|
<span>发布通知</span>
|
||||||
|
<div class="button-sparkles"></div>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { reactive, onMounted } from 'vue';
|
||||||
|
import router from "../../router";
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import RichTextEditor from '../components/RichTextEditor.vue';
|
||||||
|
import {useRoute} from "vue-router";
|
||||||
|
|
||||||
|
interface NotificationForm {
|
||||||
|
notificationTitle: string;
|
||||||
|
notificationContent: string;
|
||||||
|
projectId: string | number; // 关联项目ID,接收数值或字符串
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = reactive<NotificationForm>({
|
||||||
|
notificationTitle: '',
|
||||||
|
notificationContent: '',
|
||||||
|
projectId: 0 // 初始化值
|
||||||
|
});
|
||||||
|
|
||||||
|
const route = useRoute(); // 获取路由参数
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 从路由参数中获取projectId并填充到表单
|
||||||
|
if (typeof route.query.projectId === 'string') {
|
||||||
|
formData.projectId = Number(route.query.projectId); // 转换为数字
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
'/projectNotification/add',
|
||||||
|
JSON.stringify(formData),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
alert('通知发布成功!');
|
||||||
|
router.back();
|
||||||
|
Object.assign(formData, {
|
||||||
|
notificationTitle: '',
|
||||||
|
notificationContent: '',
|
||||||
|
projectId: ''
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
alert(`发布失败:${res.message}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
alert('通知发布失败,请检查网络或数据格式');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 保留原有样式,添加富文本编辑器样式 */
|
||||||
|
.phone-notification {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #4a5568;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-content >>> img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.modern-form {
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 2.5rem;
|
||||||
|
background: white;
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.8rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-text {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field{
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.8rem 1.2rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:focus{
|
||||||
|
border-color: #6366f1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(99, 102, 241, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-sparkles {
|
||||||
|
position: absolute;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: rgba(255,255,255,0.4);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: sparkle 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes sparkle {
|
||||||
|
0% { transform: scale(0) translate(0,0); }
|
||||||
|
50% { transform: scale(1) translate(100px, -50px); }
|
||||||
|
100% { transform: scale(0) translate(200px, -100px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.form-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modern-form {
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增富文本编辑器样式 */
|
||||||
|
.rich-text-container {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-columns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 320px;
|
||||||
|
height: auto;
|
||||||
|
padding: 0.8rem;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group:focus-within {
|
||||||
|
border-color: #6366f1 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group .label-text {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #3b4151;
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group:focus-within .label-text {
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机预览样式 */
|
||||||
|
.phone-preview-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-frame {
|
||||||
|
position: relative;
|
||||||
|
width: 320px;
|
||||||
|
height: 640px;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 40px;
|
||||||
|
padding: 15px;
|
||||||
|
box-shadow: 0 0 0 12px #1f1f1f, 0 0 30px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 180px;
|
||||||
|
height: 25px;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-camera {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 20px;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background: #15294c;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-speaker {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 60px;
|
||||||
|
height: 6px;
|
||||||
|
background: #1f1f1f;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-screen {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 28px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 20px;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-home-button {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1f1f1f;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.phone-section-content * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content h1,
|
||||||
|
.phone-section-content h2,
|
||||||
|
.phone-section-content h3 {
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-section-content ul,
|
||||||
|
.phone-section-content ol {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
</style>
|
692
src/view/project/moneyDetail.vue
Normal file
692
src/view/project/moneyDetail.vue
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted} from "vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import myAxios from "../../api/myAxios";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
import { reactive } from 'vue';
|
||||||
|
import { SmileOutlined } from '@ant-design/icons-vue';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '项目明细ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: 50,
|
||||||
|
key: 'id',
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目明细名称',
|
||||||
|
dataIndex: 'projectDetailName',
|
||||||
|
key: 'projectDetailName',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目结算价',
|
||||||
|
dataIndex: 'projectSettlementPrice',
|
||||||
|
width: 30,
|
||||||
|
key: 'projectSettlementPrice',
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目最小结算价',
|
||||||
|
dataIndex: 'projectMinSettlementPrice',
|
||||||
|
width: 30,
|
||||||
|
key: 'projectMinSettlementPrice',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最大抽成比例',
|
||||||
|
dataIndex: 'maxCommissionRate',
|
||||||
|
width: 40,
|
||||||
|
key: 'maxCommissionRate',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目ID',
|
||||||
|
dataIndex: 'projectId',
|
||||||
|
key: 'projectId',
|
||||||
|
width: 50,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 50,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
interface ProjectDetail {
|
||||||
|
id: number;
|
||||||
|
projectDetailName: string;
|
||||||
|
projectSettlementPrice: number;
|
||||||
|
projectMinSettlementPrice: number; // 修正接口字段
|
||||||
|
maxCommissionRate: number;
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const projectId = ref<string | number>("");
|
||||||
|
const tableData = ref<ProjectDetail[]>([]); // 改为数组存储
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref("");
|
||||||
|
const searchId = ref(""); // 新增搜索ID绑定
|
||||||
|
|
||||||
|
// 新增变量保存原始数据
|
||||||
|
const originalTableData = ref<ProjectDetail[]>([]);
|
||||||
|
|
||||||
|
// 修改 getMoneyDetail 方法
|
||||||
|
const getMoneyDetail = async (id: string | number) => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response:any = await myAxios.post(
|
||||||
|
"/projectDetail/query/pid",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(response)
|
||||||
|
if (response.code === 1) {
|
||||||
|
tableData.value = response.data;
|
||||||
|
// 保存原始数据
|
||||||
|
originalTableData.value = response.data;
|
||||||
|
} else {
|
||||||
|
error.value = "获取项目详情失败";
|
||||||
|
tableData.value = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = "数据加载失败,请重试";
|
||||||
|
tableData.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改搜索处理方法 - 前端过滤
|
||||||
|
const handleIdSearch = (value: string) => {
|
||||||
|
if (!value.trim()) {
|
||||||
|
// 如果搜索值为空,显示所有数据
|
||||||
|
tableData.value = [...originalTableData.value];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(value);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
message.warning("ID必须为数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端过滤逻辑
|
||||||
|
const filtered = originalTableData.value.filter(item => item.id === id);
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
message.warning("未找到匹配的项目明细");
|
||||||
|
tableData.value = [];
|
||||||
|
} else {
|
||||||
|
tableData.value = filtered;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改重置方法
|
||||||
|
const reset = () => {
|
||||||
|
searchId.value = "";
|
||||||
|
// 重置时显示所有原始数据
|
||||||
|
tableData.value = [...originalTableData.value];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增根据明细ID查询的方法
|
||||||
|
const queryDetailById = async (id: string | number) => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response:any = await myAxios.post(
|
||||||
|
"/projectDetail/queryById", // 使用文档中的接口路径
|
||||||
|
{ id }, // 请求参数
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(response.data)
|
||||||
|
if (response.code === 1) {
|
||||||
|
tableData.value = [response.data];
|
||||||
|
} else {
|
||||||
|
message.error(response.message || "查询失败");
|
||||||
|
tableData.value = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.error("查询请求失败");
|
||||||
|
tableData.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化逻辑
|
||||||
|
if (typeof route.query.id === "string") {
|
||||||
|
projectId.value = route.query.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (projectId.value) {
|
||||||
|
getMoneyDetail(projectId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteMoneyDetail = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
// 修正接口路径为项目明细的删除接口
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
"/projectDetail/delete", // 修改这里
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
'AfterScript': 'required-script'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('删除成功');
|
||||||
|
if (projectId.value) {
|
||||||
|
await getMoneyDetail(projectId.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
message.error('删除操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawerVisible = ref(false);
|
||||||
|
const formState = reactive({
|
||||||
|
id: 0,
|
||||||
|
projectDetailName: '',
|
||||||
|
projectSettlementPrice: 0,
|
||||||
|
projectMinSettlementPrice: 0,
|
||||||
|
maxCommissionRate: 0,
|
||||||
|
projectId: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改updateMoneyDetail方法
|
||||||
|
// const updateMoneyDetail = async (id: number) => {
|
||||||
|
// try {
|
||||||
|
// // 先获取明细数据
|
||||||
|
// const storedToken = localStorage.getItem('token');
|
||||||
|
// const res:any = await myAxios.post(
|
||||||
|
// "/projectDetail/queryById",
|
||||||
|
// { id },
|
||||||
|
// {
|
||||||
|
// headers: {
|
||||||
|
// Authorization: storedToken,
|
||||||
|
// "Content-Type": "application/json"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// console.log(res)
|
||||||
|
// if (res.code === 1) {
|
||||||
|
// // 映射字段到表单
|
||||||
|
// const detail = res.data;
|
||||||
|
// Object.assign(formState, {
|
||||||
|
// id: detail.id,
|
||||||
|
// projectDetailName: detail.projectDetailName,
|
||||||
|
// projectSettlementPrice: detail.projectSettlementPrice,
|
||||||
|
// projectMinSettlementPrice: detail.projectMinSettlementPrice,
|
||||||
|
// maxCommissionRate: detail.maxCommissionRate,
|
||||||
|
// projectId: detail.projectId
|
||||||
|
// });
|
||||||
|
// drawerVisible.value = true;
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// message.error('获取明细数据失败');
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
|
||||||
|
// 新增表单相关状态
|
||||||
|
const addDrawerVisible = ref(false);
|
||||||
|
const addFormState = reactive({
|
||||||
|
projectDetailName: '',
|
||||||
|
projectSettlementPrice: 0,
|
||||||
|
projectMinSettlementPrice: 0, // 注意字段名称需要与接口一致
|
||||||
|
maxCommissionRate: 0,
|
||||||
|
projectId: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打开新增表单
|
||||||
|
const openAddDrawer = () => {
|
||||||
|
addFormState.projectId = Number(projectId.value); // 关联当前项目
|
||||||
|
addDrawerVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交新增请求
|
||||||
|
const handleAddSubmit = async () => {
|
||||||
|
if (addFormState.projectMinSettlementPrice >= addFormState.projectSettlementPrice) {
|
||||||
|
message.error("最小结算价必须小于结算价");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
addFormState.projectSettlementPrice === 0 ||
|
||||||
|
addFormState.projectMinSettlementPrice === 0 ||
|
||||||
|
addFormState.maxCommissionRate === 0
|
||||||
|
) {
|
||||||
|
message.error("结算价、最小结算价和抽成比例不能为0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/projectDetail/add",
|
||||||
|
{
|
||||||
|
projectDetailName: addFormState.projectDetailName,
|
||||||
|
projectSettlementPrice: addFormState.projectSettlementPrice,
|
||||||
|
projectMinSettlementPrice: addFormState.projectMinSettlementPrice, // 字段映射
|
||||||
|
maxCommissionRate: addFormState.maxCommissionRate,
|
||||||
|
projectId: addFormState.projectId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('新增成功');
|
||||||
|
addDrawerVisible.value = false;
|
||||||
|
await getMoneyDetail(projectId.value); // 刷新列表
|
||||||
|
// 重置表单
|
||||||
|
Object.assign(addFormState, {
|
||||||
|
projectDetailName: '',
|
||||||
|
projectSettlementPrice: 0,
|
||||||
|
projectMinSettlementPrice: 0,
|
||||||
|
maxCommissionRate: 0,
|
||||||
|
projectId: Number(projectId.value)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '新增失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('新增请求失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改模板按钮绑定
|
||||||
|
const goAddProject = () => {
|
||||||
|
openAddDrawer();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改handleSubmit方法
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (formState.projectMinSettlementPrice >= formState.projectSettlementPrice) {
|
||||||
|
message.error("最小结算价必须小于结算价");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
formState.projectSettlementPrice === 0 ||
|
||||||
|
formState.projectMinSettlementPrice === 0 ||
|
||||||
|
formState.maxCommissionRate === 0
|
||||||
|
) {
|
||||||
|
message.error("结算价、最小结算价和抽成比例不能为0");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/projectDetail/update",
|
||||||
|
{
|
||||||
|
// 根据接口文档调整字段映射
|
||||||
|
id: formState.id,
|
||||||
|
projectDetailName: formState.projectDetailName,
|
||||||
|
projectSettlementPrice: formState.projectSettlementPrice,
|
||||||
|
projectMinSettlementPrice: formState.projectMinSettlementPrice,
|
||||||
|
maxCommissionRate: formState.maxCommissionRate,
|
||||||
|
projectId: formState.projectId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('接口响应:', res); // 添加详细日志
|
||||||
|
|
||||||
|
if (res.code === 1) { // 注意响应结构层级
|
||||||
|
message.success('更新成功');
|
||||||
|
drawerVisible.value = false;
|
||||||
|
// 根据当前查看模式刷新
|
||||||
|
if (searchId.value) {
|
||||||
|
await queryDetailById(formState.id);
|
||||||
|
} else {
|
||||||
|
await getMoneyDetail(projectId.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(res.message || `更新失败,错误码:${res.code}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('完整错误信息:', error); // 输出完整错误对象
|
||||||
|
message.error(`更新失败'}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
//返回上一级
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 返回上一级方法
|
||||||
|
const goBack = () => {
|
||||||
|
// router.go(-1); // 返回上一页
|
||||||
|
router.push('/project')
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="search-box">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-space>
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input-search
|
||||||
|
style="width: 300px"
|
||||||
|
placeholder="请输入项目明细ID"
|
||||||
|
enter-button
|
||||||
|
@search="handleIdSearch"
|
||||||
|
v-model:value="searchId"
|
||||||
|
type="number"
|
||||||
|
class="custom-search"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-button class="custom-button" @click="goAddProject">新增项目明细</a-button>
|
||||||
|
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||||
|
<a-button @click="goBack" class="custom-button">返回</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableData"
|
||||||
|
:scroll="{ x: 1500, y: 450 }"
|
||||||
|
:loading="loading"
|
||||||
|
bordered
|
||||||
|
rowKey="id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- 格式化金额显示 -->
|
||||||
|
<template v-if="column.dataIndex === 'projectSettlementPrice'">
|
||||||
|
¥{{ record.projectSettlementPrice.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="column.dataIndex === 'projectMinSettlementPrice'">
|
||||||
|
¥{{ record.projectMinSettlementPrice.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'maxCommissionRate'">
|
||||||
|
{{ record.maxCommissionRate}}%
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
@click="deleteMoneyDetail(record.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
<!-- <a-button-->
|
||||||
|
<!-- size="small"-->
|
||||||
|
<!-- @click="updateMoneyDetail(record.id)"-->
|
||||||
|
<!-- >-->
|
||||||
|
<!-- 编辑-->
|
||||||
|
<!-- </a-button>-->
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
<!-- 错误提示 -->
|
||||||
|
<div v-if="error" class="error-alert">
|
||||||
|
<span class="error-icon">!</span>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 修改编辑抽屉的表单项 -->
|
||||||
|
<a-drawer
|
||||||
|
title="编辑项目明细"
|
||||||
|
placement="right"
|
||||||
|
:visible="drawerVisible"
|
||||||
|
@close="drawerVisible = false"
|
||||||
|
width="600"
|
||||||
|
>
|
||||||
|
|
||||||
|
<a-form
|
||||||
|
:model="formState"
|
||||||
|
layout="vertical"
|
||||||
|
@finish="handleSubmit"
|
||||||
|
>
|
||||||
|
<a-form-item label="项目明细名称" required>
|
||||||
|
<a-input v-model:value="formState.projectDetailName" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="项目结算价" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="formState.projectSettlementPrice"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="项目最小结算价" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="formState.projectMinSettlementPrice"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="最大抽成比例" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="formState.maxCommissionRate"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-button class="custom-button" html-type="submit">提交</a-button>
|
||||||
|
<a-button style="margin-left: 10px" @click="drawerVisible = false">取消</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-drawer>
|
||||||
|
|
||||||
|
<!-- 修改新增抽屉的表单项 -->
|
||||||
|
<a-drawer
|
||||||
|
title="新增项目明细"
|
||||||
|
placement="right"
|
||||||
|
:visible="addDrawerVisible"
|
||||||
|
@close="addDrawerVisible = false"
|
||||||
|
width="600"
|
||||||
|
>
|
||||||
|
<a-alert message="项目结算价必须大于最小结算价" type="warning" show-icon class="addAlert">
|
||||||
|
<template #icon><smile-outlined /></template>
|
||||||
|
</a-alert>
|
||||||
|
<a-form
|
||||||
|
:model="addFormState"
|
||||||
|
layout="vertical"
|
||||||
|
@finish="handleAddSubmit"
|
||||||
|
>
|
||||||
|
<a-form-item label="明细名称" required>
|
||||||
|
<a-input v-model:value="addFormState.projectDetailName" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="结算价 (¥)" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="addFormState.projectSettlementPrice"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="最小结算价 (¥)" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="addFormState.projectMinSettlementPrice"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="抽成比例 (%)" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="addFormState.maxCommissionRate"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-button class="custom-button" html-type="submit">提交</a-button>
|
||||||
|
<a-button style="margin-left: 10px" @click="addDrawerVisible = false">
|
||||||
|
取消
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-drawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-alert {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #ffe3e3;
|
||||||
|
color: #ff4444;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-thead) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row:hover) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*橙色按钮*/
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮样式 */
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持原有的其他样式不变 */
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button) {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button:hover),
|
||||||
|
.custom-search :deep(.ant-input-search-button:focus) {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持输入框原有样式不变 */
|
||||||
|
.custom-search :deep(.ant-input) {
|
||||||
|
border-right-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 调整分页器位置 */
|
||||||
|
:deep(.ant-table-pagination.ant-pagination) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.addAlert {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
376
src/view/project/noticeDetail.vue
Normal file
376
src/view/project/noticeDetail.vue
Normal file
@ -0,0 +1,376 @@
|
|||||||
|
<template>
|
||||||
|
<div class="notification-detail-container">
|
||||||
|
<a-spin :spinning="loading" tip="加载中...">
|
||||||
|
<a-card v-if="projectNotice.id" class="notification-card">
|
||||||
|
<a-button class="custom-button" @click="goBack">返回</a-button>
|
||||||
|
<a-button v-if="!isEditing" class="custom-button" @click="enterEditMode" style="margin-left: 10px">编辑</a-button>
|
||||||
|
<template v-else>
|
||||||
|
<a-button class="custom-button" @click="saveNotice" style="margin-left: 10px">保存</a-button>
|
||||||
|
<a-button class="custom-button" @click="cancelEdit" style="margin-left: 10px">取消</a-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="header">
|
||||||
|
<h1 v-if="!isEditing" class="title">{{ projectNotice.notificationTitle }}</h1>
|
||||||
|
<a-input v-else v-model:value="editableNotice.notificationTitle" placeholder="请输入通知标题" />
|
||||||
|
|
||||||
|
<div class="meta-info">
|
||||||
|
<span class="project-id">项目ID: {{ projectNotice.projectId }}</span>
|
||||||
|
<span class="create-time">创建时间: {{ projectNotice.createTime }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<div class="content-header">
|
||||||
|
<notification-outlined class="icon" />
|
||||||
|
<h2>通知内容</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本显示和编辑区域 -->
|
||||||
|
<div v-if="!isEditing" class="content-html" v-html="projectNotice.notificationContent"></div>
|
||||||
|
<div v-else class="rich-text-group">
|
||||||
|
<RichTextEditor
|
||||||
|
v-model="editableNotice.notificationContent"
|
||||||
|
:disable="false"
|
||||||
|
@content-change="(html:any) => editableNotice.notificationContent = html"
|
||||||
|
class="rich-text-editor"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
<a-empty v-else-if="!loading" :description="errorMessage || '未找到通知详情'" />
|
||||||
|
</a-spin>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, onMounted } from "vue";
|
||||||
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import { NotificationOutlined } from '@ant-design/icons-vue';
|
||||||
|
import RichTextEditor from '../components/RichTextEditor.vue'; // 引入富文本编辑器
|
||||||
|
import '@vueup/vue-quill/dist/vue-quill.snow.css'; // 引入富文本编辑器样式
|
||||||
|
|
||||||
|
interface NotificationDetail {
|
||||||
|
id: string; // 改为 string 类型
|
||||||
|
notificationTitle: string;
|
||||||
|
projectId: string; // 改为 string 类型
|
||||||
|
createTime: string;
|
||||||
|
notificationContent: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectNotice = ref<Partial<NotificationDetail>>({
|
||||||
|
id: '',
|
||||||
|
notificationTitle: '',
|
||||||
|
projectId: '',
|
||||||
|
createTime: '',
|
||||||
|
notificationContent: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const editableNotice = ref<Partial<NotificationDetail>>({});
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const errorMessage = ref("");
|
||||||
|
|
||||||
|
// 进入编辑模式
|
||||||
|
const enterEditMode = () => {
|
||||||
|
// 创建可编辑数据的副本
|
||||||
|
editableNotice.value = {
|
||||||
|
...projectNotice.value
|
||||||
|
};
|
||||||
|
isEditing.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取消编辑
|
||||||
|
const cancelEdit = () => {
|
||||||
|
isEditing.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存编辑 - 根据接口要求修改
|
||||||
|
// 修改保存函数
|
||||||
|
const saveNotice = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
|
||||||
|
// 创建符合接口要求的JSON对象
|
||||||
|
const requestData = {
|
||||||
|
id: editableNotice.value.id,
|
||||||
|
notificationTitle: editableNotice.value.notificationTitle,
|
||||||
|
notificationContent: editableNotice.value.notificationContent,
|
||||||
|
projectId: editableNotice.value.projectId
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送JSON格式的数据
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/projectNotification/update",
|
||||||
|
requestData, // 直接发送JSON对象
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
'Content-Type': 'application/json' // 关键修改:使用JSON格式
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('更新成功');
|
||||||
|
// 更新本地数据并退出编辑模式
|
||||||
|
projectNotice.value = { ...editableNotice.value };
|
||||||
|
isEditing.value = false;
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '更新失败');
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('更新通知失败:', error);
|
||||||
|
// 改进错误处理
|
||||||
|
const errMsg = error.response?.data?.message ||
|
||||||
|
error.message ||
|
||||||
|
'更新通知失败,请检查网络';
|
||||||
|
message.error(errMsg);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const fetchNotificationDetail = async (id: number) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
errorMessage.value = "";
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/projectNotification/queryById",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1 && res.data) {
|
||||||
|
projectNotice.value = {
|
||||||
|
id: res.data.id,
|
||||||
|
notificationTitle: res.data.notificationTitle,
|
||||||
|
notificationContent: res.data.notificationContent,
|
||||||
|
projectId: res.data.projectId,
|
||||||
|
createTime: res.data.createTime
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
errorMessage.value = res.message || '获取通知详情失败';
|
||||||
|
message.error(errorMessage.value);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取通知详情失败:', error);
|
||||||
|
errorMessage.value = error.response?.message ||
|
||||||
|
error.message ||
|
||||||
|
'获取通知详情失败,请检查网络';
|
||||||
|
message.error(errorMessage.value);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const id = Number(route.params.id || route.query.id);
|
||||||
|
if (id && !isNaN(id)) {
|
||||||
|
fetchNotificationDetail(id);
|
||||||
|
} else {
|
||||||
|
errorMessage.value = '无效的通知ID';
|
||||||
|
message.error('无效的通知ID');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 基本样式保持不变 */
|
||||||
|
.notification-detail-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 24px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification-card {
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
border: none;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 24px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: #86909c;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
margin: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-header h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 0 0 8px;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 20px;
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 富文本样式 */
|
||||||
|
.rich-text-group {
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
min-height: 320px;
|
||||||
|
height: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
&:focus-within {
|
||||||
|
border-color: #6366f1 !important;
|
||||||
|
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-editor {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 富文本内容显示样式 */
|
||||||
|
.content-html {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #4e5969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-html * {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-html img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-html h1,
|
||||||
|
.content-html h2,
|
||||||
|
.content-html h3 {
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-top: 15px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-html p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-html ul,
|
||||||
|
.content-html ol {
|
||||||
|
margin-left: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.notification-detail-container {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-info {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-group {
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*橙色按钮*/
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮样式 */
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
</style>
|
564
src/view/project/project.vue
Normal file
564
src/view/project/project.vue
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
<template>
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="search-box">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-space>
|
||||||
|
<a-form-item label="项目名称">
|
||||||
|
<a-input-search
|
||||||
|
style="width: 300px"
|
||||||
|
placeholder="请输入项目名称"
|
||||||
|
enter-button
|
||||||
|
@search="handleProjectSearch"
|
||||||
|
v-model:value="searchProjectName"
|
||||||
|
class="custom-search"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-button class="custom-button" @click="goAddProject">新增项目</a-button>
|
||||||
|
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据-->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="tableData"
|
||||||
|
:scroll="{ x: 2000, y: 550 }"
|
||||||
|
:loading="loading"
|
||||||
|
:pagination="pagination"
|
||||||
|
bordered
|
||||||
|
rowKey="id"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- 项目图片 -->
|
||||||
|
<template v-if="column.key === 'projectImage'">
|
||||||
|
<a-avatar :src="downLoadImage+record.projectImage" shape="square" :size="64"/>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'currentPromotionCount'">
|
||||||
|
{{record.currentPromotionCount }}人
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'projectSettlementCycle'">
|
||||||
|
T+{{record.projectSettlementCycle}}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'maxPromoterCount'">
|
||||||
|
{{record.maxPromoterCount}}人
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'projectPrice'">
|
||||||
|
¥{{record.projectPrice }}
|
||||||
|
</template>
|
||||||
|
<!-- 项目状态 -->
|
||||||
|
<template v-if="column.key === 'projectStatus'">
|
||||||
|
<a-tag :color="getStatusColor(record.projectStatus)">
|
||||||
|
{{ getStatusText(record.projectStatus) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 是否上架 -->
|
||||||
|
<template v-if="column.key === 'isShelves'">
|
||||||
|
<a-tag :color="record.isShelves ? 'green' : 'red'">
|
||||||
|
{{ record.isShelves ? '已上架' : '已下架' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-tag
|
||||||
|
size="small"
|
||||||
|
:color="record.isShelves ? 'orange' : 'red'"
|
||||||
|
@click="toggleShelves(record)"
|
||||||
|
>
|
||||||
|
{{ record.isShelves ? '去下架' : '去上架' }}
|
||||||
|
</a-tag>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
@click="deleteProject(record.id)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="showDetails(record.id)"
|
||||||
|
>
|
||||||
|
详情
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="moneyDetails(record.id)"
|
||||||
|
>
|
||||||
|
项目明细
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="projectNotice(record.id)"
|
||||||
|
>
|
||||||
|
项目通知
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
type="link"
|
||||||
|
@click="promotionCode(record.id)"
|
||||||
|
>
|
||||||
|
推广码
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import { message, Modal } from "ant-design-vue";
|
||||||
|
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||||
|
import router from "../../router";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const total = ref(0);
|
||||||
|
|
||||||
|
const searchProjectName = ref(""); // 改为项目名称搜索参数
|
||||||
|
const searchParams = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortField: "id",
|
||||||
|
sortOrder: "ascend",
|
||||||
|
userRole: null,
|
||||||
|
projectName: "" // 新增项目名称查询参数
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const statusMap: { [key: string]: string } = {
|
||||||
|
running: '项目运行',
|
||||||
|
full: '人数已满',
|
||||||
|
paused: '项目暂停'
|
||||||
|
};
|
||||||
|
return statusMap[status] || '未知状态';
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colorMap: { [key: string]: string } = {
|
||||||
|
running: 'blue',
|
||||||
|
full: 'orange',
|
||||||
|
paused: 'red'
|
||||||
|
};
|
||||||
|
return colorMap[status] || 'gray';
|
||||||
|
};
|
||||||
|
|
||||||
|
//用户表
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '项目ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: 10,
|
||||||
|
key: 'id',
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: '项目图片',
|
||||||
|
dataIndex: 'projectImage',
|
||||||
|
key: 'projectImage',
|
||||||
|
width: 10,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目名称',
|
||||||
|
dataIndex: 'projectName',
|
||||||
|
width: 15,
|
||||||
|
key: 'projectName',
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目价格',
|
||||||
|
dataIndex: 'projectPrice',
|
||||||
|
width: 20,
|
||||||
|
key: 'projectPrice',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '当前推广人数',
|
||||||
|
dataIndex: 'currentPromotionCount',
|
||||||
|
width: 20,
|
||||||
|
key: 'currentPromotionCount',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '结算周期',
|
||||||
|
dataIndex: 'projectSettlementCycle',
|
||||||
|
key: 'projectSettlementCycle',
|
||||||
|
width: 10,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最大推广人数',
|
||||||
|
dataIndex: 'maxPromoterCount',
|
||||||
|
key: 'maxPromoterCount',
|
||||||
|
width: 20,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目状态',
|
||||||
|
dataIndex: 'projectStatus',
|
||||||
|
key: 'projectStatus',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '是否上架',
|
||||||
|
dataIndex: 'isShelves',
|
||||||
|
key: 'isShelves',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 40,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
// 新增上架/下架操作
|
||||||
|
const toggleShelves = async (record: any) => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
"/project/shelves",
|
||||||
|
{
|
||||||
|
id: record.id,
|
||||||
|
},
|
||||||
|
{ headers: { Authorization: storedToken } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('状态更新成功');
|
||||||
|
// 使用 Object.assign 触发响应式更新
|
||||||
|
Object.assign(record, { isShelves: !record.isShelves });
|
||||||
|
// 或重新获取数据(推荐)
|
||||||
|
await getProjectList();
|
||||||
|
}else {
|
||||||
|
message.error(res.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error);
|
||||||
|
message.error('状态更新失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ProjectRecord {
|
||||||
|
// 这里根据实际数据结构定义属性
|
||||||
|
superHostList?: string[];
|
||||||
|
// 其他属性...
|
||||||
|
}
|
||||||
|
// 项目名称搜索方法
|
||||||
|
const handleProjectSearch = async () => {
|
||||||
|
// 将搜索参数同步到分页查询参数
|
||||||
|
searchParams.value.projectName = searchProjectName.value;
|
||||||
|
searchParams.value.current = 1; // 重置到第一页
|
||||||
|
await getProjectList();
|
||||||
|
};
|
||||||
|
//用户分页查询
|
||||||
|
const getProjectList = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) throw new Error('未找到登录信息');
|
||||||
|
|
||||||
|
const res:any = await myAxios.post("/project/page",
|
||||||
|
{
|
||||||
|
...searchParams.value,
|
||||||
|
projectName: searchParams.value.projectName
|
||||||
|
},
|
||||||
|
{ headers: { Authorization: storedToken } }
|
||||||
|
);
|
||||||
|
console.log(res)
|
||||||
|
if (res.code === 1 && res.data && Array.isArray(res.data.records)) {
|
||||||
|
tableData.value = res.data.records.map((item: ProjectRecord) => ({
|
||||||
|
...item,
|
||||||
|
superUserList: item.superHostList? item.superHostList.join(', ') : '无'
|
||||||
|
}));
|
||||||
|
// 同步总条数到分页组件
|
||||||
|
total.value = res.data.total;
|
||||||
|
pagination.value.total = res.data.total; // 新增此行
|
||||||
|
pagination.value.current = searchParams.value.current; // 同步当前页
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '请求失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("请求失败:", error);
|
||||||
|
message.error('获取数据失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(getProjectList);
|
||||||
|
//分页
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = ref({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`,
|
||||||
|
pageSizeOptions: ['10', '20', '50', '100']
|
||||||
|
});
|
||||||
|
const handleTableChange = (pag: any, _: any, sorter: any) => {
|
||||||
|
// 处理排序参数
|
||||||
|
let sortField = "id"; // 默认排序字段
|
||||||
|
let sortOrder = "ascend"; // 默认排序方式
|
||||||
|
if (sorter.field) {
|
||||||
|
sortField = sorter.field;
|
||||||
|
sortOrder = sorter.order;
|
||||||
|
}
|
||||||
|
searchParams.value = {
|
||||||
|
...searchParams.value,
|
||||||
|
current: pag.current,
|
||||||
|
pageSize: pag.pageSize,
|
||||||
|
sortField: sortField, // 设置排序字段
|
||||||
|
sortOrder: sortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
// 同步到分页组件
|
||||||
|
pagination.value = {
|
||||||
|
...pagination.value,
|
||||||
|
current: pag.current,
|
||||||
|
pageSize: pag.pageSize
|
||||||
|
};
|
||||||
|
|
||||||
|
getProjectList();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ID查询方法
|
||||||
|
interface Project {
|
||||||
|
id: number;
|
||||||
|
projectName: string;
|
||||||
|
projectImage: string;
|
||||||
|
projectSettlementCycle: number;
|
||||||
|
maxPromoterCount: number;
|
||||||
|
projectStatus: string;
|
||||||
|
projectDescription: string;
|
||||||
|
settlementDesc: string;
|
||||||
|
// 其他可能存在的属性根据实际情况补充
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableData = ref<Project[]>([]);
|
||||||
|
|
||||||
|
|
||||||
|
// 删除项目 - 添加确认弹窗
|
||||||
|
const deleteProject = (id: number) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除该项目吗?删除后数据将无法恢复!',
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/project/delete",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
'AfterScript': 'required-script'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('删除成功');
|
||||||
|
await getProjectList();
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
message.error('删除操作失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel() {
|
||||||
|
// 用户点击取消,不做操作
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置按钮
|
||||||
|
const reset = () => {
|
||||||
|
searchProjectName.value = "";
|
||||||
|
searchParams.value = {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
sortField: "id",
|
||||||
|
sortOrder: "ascend",
|
||||||
|
userRole: null,
|
||||||
|
projectName: ""
|
||||||
|
};
|
||||||
|
getProjectList();
|
||||||
|
};
|
||||||
|
|
||||||
|
//去新增项目
|
||||||
|
const goAddProject=()=>{
|
||||||
|
router.push('/addproject')
|
||||||
|
}
|
||||||
|
|
||||||
|
const showDetails=(id:string)=>{
|
||||||
|
router.push({
|
||||||
|
path:'/projectDetail',
|
||||||
|
query:{
|
||||||
|
id:String(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
//去项目明细
|
||||||
|
const moneyDetails=(id:string)=>{
|
||||||
|
router.push({
|
||||||
|
path:'/moneyDetail',
|
||||||
|
query:{
|
||||||
|
id:String(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//去项目通知
|
||||||
|
const projectNotice=(id:number)=>{
|
||||||
|
router.push({
|
||||||
|
path:'/projectNotice',
|
||||||
|
query:{
|
||||||
|
id:String(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//去推广码
|
||||||
|
const promotionCode=(id:number)=>{
|
||||||
|
router.push({
|
||||||
|
path:'/promotionCode',
|
||||||
|
query:{
|
||||||
|
id:String(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.action-btn {
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
/* 分割线样式 */
|
||||||
|
:deep(.ant-divider-vertical) {
|
||||||
|
border-color: rgba(0, 0, 0, 0.15);
|
||||||
|
height: 1.2em;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:deep(.ant-descriptions-item-label) {
|
||||||
|
font-weight: 600;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.info-card :deep(.ant-card-head) {
|
||||||
|
background: #fafafa;
|
||||||
|
border-bottom: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* 角色颜色映射 */
|
||||||
|
:root {
|
||||||
|
--role-user: #87d068;
|
||||||
|
--role-admin: #f50;
|
||||||
|
--role-boss: #722ed1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*橙色按钮*/
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮样式 */
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持原有的其他样式不变 */
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button) {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button:hover),
|
||||||
|
.custom-search :deep(.ant-input-search-button:focus) {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持输入框原有样式不变 */
|
||||||
|
.custom-search :deep(.ant-input) {
|
||||||
|
border-right-color: #ffa940;
|
||||||
|
}
|
||||||
|
</style>
|
692
src/view/project/projectDetail.vue
Normal file
692
src/view/project/projectDetail.vue
Normal file
@ -0,0 +1,692 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import myAxios from "../../api/myAxios";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import { QuillEditor } from '@vueup/vue-quill';
|
||||||
|
import {Form} from 'ant-design-vue'
|
||||||
|
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||||
|
|
||||||
|
interface ProjectDetail {
|
||||||
|
id: number;
|
||||||
|
projectName: string;
|
||||||
|
projectImage: string;
|
||||||
|
projectSettlementCycle: number;
|
||||||
|
maxPromoterCount: number;
|
||||||
|
projectStatus: string;
|
||||||
|
projectDescription: string;
|
||||||
|
settlementDesc: string;
|
||||||
|
projectDesc: string;
|
||||||
|
projectFlow: string;
|
||||||
|
applyPromoCodeDesc: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const loading = ref(false);
|
||||||
|
const projectData = ref<ProjectDetail>({
|
||||||
|
id: 0,
|
||||||
|
projectName: '',
|
||||||
|
projectImage: '',
|
||||||
|
projectSettlementCycle: 0,
|
||||||
|
maxPromoterCount: 0,
|
||||||
|
projectStatus: '',
|
||||||
|
projectDescription: '',
|
||||||
|
settlementDesc: '',
|
||||||
|
projectDesc: '',
|
||||||
|
projectFlow: '',
|
||||||
|
applyPromoCodeDesc: ''
|
||||||
|
});
|
||||||
|
const projectId = ref<string | null>(null);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const fileInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
// 图片上传处理逻辑
|
||||||
|
const handleFileUpload = async (event: Event) => {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const file = input.files?.[0];
|
||||||
|
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 本地预览
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
// 仅设置临时预览,不覆盖实际值
|
||||||
|
previewImage.value = e.target?.result as string;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
|
||||||
|
// 上传服务器
|
||||||
|
const uploadFormData = new FormData();
|
||||||
|
uploadFormData.append('biz', 'project');
|
||||||
|
uploadFormData.append('file', file);
|
||||||
|
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post('/file/upload', uploadFormData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 1 && res.data) {
|
||||||
|
// 核心修改:将返回的文件标识符赋值给projectImage
|
||||||
|
projectData.value.projectImage = res.data;
|
||||||
|
message.success('图片上传成功');
|
||||||
|
} else {
|
||||||
|
throw new Error(res.message || '上传失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
message.error('文件上传失败');
|
||||||
|
previewImage.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const previewImage = ref('');
|
||||||
|
|
||||||
|
const formRef = ref<typeof Form>(); // 添加表单引用
|
||||||
|
|
||||||
|
// 添加数字输入校验方法
|
||||||
|
const handleNumberInput = (e: KeyboardEvent) => {
|
||||||
|
const allowedKeys = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab'];
|
||||||
|
if (!allowedKeys.includes(e.key)) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加粘贴校验方法
|
||||||
|
const handlePaste = (e: ClipboardEvent) => {
|
||||||
|
const pasteData = e.clipboardData?.getData('text/plain');
|
||||||
|
if (pasteData && !/^\d+$/.test(pasteData)) {
|
||||||
|
e.preventDefault();
|
||||||
|
message.error('只能粘贴数字');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新项目时提交处理
|
||||||
|
const finishEditing = async () => {
|
||||||
|
//isEditing.value = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) throw new Error('未找到登录信息');
|
||||||
|
|
||||||
|
// 转换富文本内容为字符串
|
||||||
|
const payload = {
|
||||||
|
...projectData.value,
|
||||||
|
settlementDesc: String(projectData.value.settlementDesc),
|
||||||
|
projectDesc: String(projectData.value.projectDesc),
|
||||||
|
projectFlow: String(projectData.value.projectFlow),
|
||||||
|
applyPromoCodeDesc: String(projectData.value.applyPromoCodeDesc)
|
||||||
|
};
|
||||||
|
|
||||||
|
const res:any = await myAxios.post("/project/update",
|
||||||
|
JSON.stringify(payload),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(res)
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('项目更新成功');
|
||||||
|
router.push('/project')
|
||||||
|
|
||||||
|
}else {
|
||||||
|
message.error(res.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("更新失败:", error);
|
||||||
|
|
||||||
|
message.error('项目更新失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof route.query.id === 'string') {
|
||||||
|
projectId.value = route.query.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if(projectId.value !== null) {
|
||||||
|
handleIdSearch(projectId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const handleIdSearch = async (id: string) => {
|
||||||
|
if (!id) return message.warning("请输入项目ID");
|
||||||
|
if (!/^\d+$/.test(id)) return message.warning("ID必须为数字");
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
if (!storedToken) throw new Error('未找到登录信息');
|
||||||
|
|
||||||
|
const res:any = await myAxios.post("/project/queryById",
|
||||||
|
{ id: parseInt(id) },
|
||||||
|
{ headers: { Authorization: storedToken } }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1 && res.data) {
|
||||||
|
console.log('Received project data:', res.data);
|
||||||
|
projectData.value = res.data;
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '查询失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("查询失败:", error);
|
||||||
|
message.error('项目查询失败');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateProject = () => {
|
||||||
|
if (!projectData.value) {
|
||||||
|
message.warning('项目数据尚未加载完成,请稍后再试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isEditing.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push('/project')
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="project-detail-container">
|
||||||
|
|
||||||
|
<!-- 加载中 -->
|
||||||
|
<div v-if="loading" class="loading">加载中...</div>
|
||||||
|
|
||||||
|
<!-- 详情页 -->
|
||||||
|
<div v-if="projectData && !isEditing" class="detail-card">
|
||||||
|
<!-- 头部区域 -->
|
||||||
|
<div class="header-section">
|
||||||
|
<h1 class="project-title">{{ projectData.projectName }}</h1>
|
||||||
|
<a-button class="custom-button" @click="updateProject">编辑项目</a-button>
|
||||||
|
<a-button class="custom-button" @click="goBack">返回</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<div class="basic-info">
|
||||||
|
<div class="project-image">
|
||||||
|
<img :src="downLoadImage+projectData.projectImage" alt="项目封面图">
|
||||||
|
</div>
|
||||||
|
<div class="info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<label>结算周期</label>
|
||||||
|
<div class="value">T+{{ projectData.projectSettlementCycle }} 天</div>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<label>最大推广人数</label>
|
||||||
|
<div class="value">{{ projectData.maxPromoterCount }}人</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 项目简介 -->
|
||||||
|
<div class="section">
|
||||||
|
<h2 class="section-title">项目简介</h2>
|
||||||
|
<div class="section-content">{{ projectData.projectDescription }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 富文本内容区块 -->
|
||||||
|
<div class="rich-section" v-if="projectData.settlementDesc">
|
||||||
|
<h2 class="section-title">结算说明</h2>
|
||||||
|
<div class="rich-content" v-html="projectData.settlementDesc"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-section" v-if="projectData.projectDesc">
|
||||||
|
<h2 class="section-title">项目说明</h2>
|
||||||
|
<div class="rich-content" v-html="projectData.projectDesc"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-section" v-if="projectData.projectFlow">
|
||||||
|
<h2 class="section-title">项目流程</h2>
|
||||||
|
<div class="rich-content" v-html="projectData.projectFlow"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rich-section" v-if="projectData.applyPromoCodeDesc">
|
||||||
|
<h2 class="section-title">申请推广码说明</h2>
|
||||||
|
<div class="rich-content" v-html="projectData.applyPromoCodeDesc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ✅ 编辑页 - 使用 addProject.vue 结构 -->
|
||||||
|
<div v-if="isEditing && projectData" class="add-project-container">
|
||||||
|
<a-form ref="formRef" :model="projectData" layout="vertical" @submit.prevent="finishEditing">
|
||||||
|
<div class="form-section">
|
||||||
|
<div class="form-header">
|
||||||
|
<h2>项目基本信息</h2>
|
||||||
|
<a-button class="custom-button" @click="isEditing = false">返回</a-button>
|
||||||
|
</div>
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="项目名称">
|
||||||
|
<a-input v-model:value="projectData.projectName" placeholder="请输入项目名称" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item label="项目状态">
|
||||||
|
<a-select v-model:value="projectData.projectStatus" placeholder="请选择状态">
|
||||||
|
<a-select-option value="running">运行中</a-select-option>
|
||||||
|
<a-select-option value="full">人数已满</a-select-option>
|
||||||
|
<a-select-option value="paused">已暂停</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-row :gutter="16">
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="结算周期(天)"
|
||||||
|
name="projectSettlementCycle"
|
||||||
|
:rules="[{
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
min: 0,
|
||||||
|
message: '请输入有效的正整数'
|
||||||
|
}]"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="projectData.projectSettlementCycle"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="0"
|
||||||
|
@keypress="handleNumberInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
label="最大推广人数"
|
||||||
|
name="maxPromoterCount"
|
||||||
|
:rules="[{
|
||||||
|
required: true,
|
||||||
|
type: 'number',
|
||||||
|
min: 1,
|
||||||
|
message: '请输入有效的正整数'
|
||||||
|
}]"
|
||||||
|
>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="projectData.maxPromoterCount"
|
||||||
|
style="width: 100%"
|
||||||
|
:min="1"
|
||||||
|
@keypress="handleNumberInput"
|
||||||
|
@paste="handlePaste"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
|
||||||
|
<a-form-item label="项目图片">
|
||||||
|
<div class="file-upload">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
class="file-input"
|
||||||
|
accept="image/*"
|
||||||
|
@change="handleFileUpload"
|
||||||
|
ref="fileInput"
|
||||||
|
>
|
||||||
|
<div class="upload-button" @click="fileInput?.click()">
|
||||||
|
<span v-if="!projectData.projectImage && !previewImage">点击上传图片</span>
|
||||||
|
<span v-else class="file-name">已选择图片</span>
|
||||||
|
</div>
|
||||||
|
<!-- 修改:优先显示预览图,没有预览图时显示服务器图片 -->
|
||||||
|
<div v-if="previewImage || projectData.projectImage" class="preview-image">
|
||||||
|
<img
|
||||||
|
v-if="previewImage"
|
||||||
|
:src="previewImage"
|
||||||
|
alt="上传预览"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
:src="downLoadImage + projectData.projectImage"
|
||||||
|
alt="项目封面预览"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="项目描述">
|
||||||
|
<a-textarea v-model:value="projectData.projectDescription" :rows="4" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-section">
|
||||||
|
<h2>富文本配置</h2>
|
||||||
|
|
||||||
|
<a-form-item label="结算说明">
|
||||||
|
<QuillEditor v-model:content="projectData.settlementDesc" content-type="html" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="项目说明">
|
||||||
|
<QuillEditor v-model:content="projectData.projectDesc" content-type="html" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="项目流程">
|
||||||
|
<QuillEditor v-model:content="projectData.projectFlow" content-type="html" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="申请推广码说明">
|
||||||
|
<QuillEditor v-model:content="projectData.applyPromoCodeDesc" content-type="html" />
|
||||||
|
</a-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a-button class="custom-button" htmlType="submit">完成编辑</a-button>
|
||||||
|
</div>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 原有样式保持不变 */
|
||||||
|
.project-detail-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #666;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-tag {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 16px;
|
||||||
|
color: white;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.basic-info {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 100px 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-image img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item label {
|
||||||
|
display: block;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-item .value {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border-left: 4px solid #ffa940;
|
||||||
|
padding-left: 1rem;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-section {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(p) {
|
||||||
|
margin: 0.8em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(h2) {
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin: 1.2em 0 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(ul) {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-content :deep(li) {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.basic-info {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-title {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.6rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group textarea {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-project-container {
|
||||||
|
max-width: 960px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section {
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-section h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #1a1a1a;
|
||||||
|
border-left: 4px solid #ffa940;
|
||||||
|
padding-left: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ql-container {
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
.preview-image {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-image img {
|
||||||
|
max-width: 200px;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持原有上传样式 */
|
||||||
|
.file-upload {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 2px dashed #cbd5e1;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-button:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-name {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*橙色按钮*/
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮样式 */
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button) {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button:hover),
|
||||||
|
.custom-search :deep(.ant-input-search-button:focus) {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持输入框原有样式不变 */
|
||||||
|
.custom-search :deep(.ant-input) {
|
||||||
|
border-right-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
382
src/view/project/projectNotice.vue
Normal file
382
src/view/project/projectNotice.vue
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted} from "vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import myAxios from "../../api/myAxios";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '项目通知ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: 20,
|
||||||
|
key: 'id',
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通知标题',
|
||||||
|
dataIndex: 'notificationTitle',
|
||||||
|
key: 'notificationTitle',
|
||||||
|
width: 30,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '通知内容',
|
||||||
|
dataIndex: 'notificationContent',
|
||||||
|
key: 'notificationContent',
|
||||||
|
width: 150,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目ID',
|
||||||
|
dataIndex: 'projectId',
|
||||||
|
key: 'projectId',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
key: 'createTime',
|
||||||
|
width: 70,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 100,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 修改接口数据类型
|
||||||
|
interface ProjectNotification {
|
||||||
|
id: number;
|
||||||
|
notificationTitle: string;
|
||||||
|
notificationContent: string;
|
||||||
|
projectId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const projectId = ref<string | number>("");
|
||||||
|
const originalTableData = ref<ProjectNotification[]>([]);
|
||||||
|
const searchedData = ref<ProjectNotification[]>([]); // 存储搜索结果
|
||||||
|
const displayData = ref<ProjectNotification[]>([]); // 实际显示的数据
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref("");
|
||||||
|
const searchId = ref("");
|
||||||
|
|
||||||
|
// 修改搜索处理方法,不再调用后端接口
|
||||||
|
const handleIdSearch = () => {
|
||||||
|
const value = searchId.value.trim();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
message.warning("请输入有效的项目通知ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(value);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
message.warning("ID必须为数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在原始数据中过滤
|
||||||
|
const result = originalTableData.value.filter(item => item.id === id);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
message.warning("未找到匹配的项目通知ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新显示数据为搜索结果
|
||||||
|
searchedData.value = result;
|
||||||
|
displayData.value = result;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
searchId.value = "";
|
||||||
|
displayData.value = originalTableData.value;
|
||||||
|
searchedData.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof route.query.id === "string") {
|
||||||
|
projectId.value = route.query.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (projectId.value) {
|
||||||
|
getNotifications(projectId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const getNotifications = async (id: string | number) => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response:any = await myAxios.post(
|
||||||
|
"/projectNotification/query/pid",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(response)
|
||||||
|
if (response.code === 1) {
|
||||||
|
originalTableData.value = response.data;
|
||||||
|
displayData.value = response.data; // 初始显示所有数据
|
||||||
|
} else {
|
||||||
|
error.value = "获取通知列表失败";
|
||||||
|
originalTableData.value = [];
|
||||||
|
displayData.value = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error.value = "数据加载失败,请重试";
|
||||||
|
originalTableData.value = [];
|
||||||
|
displayData.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (projectId.value) {
|
||||||
|
getNotifications(projectId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const goToNotificationDetail = (id: number) => {
|
||||||
|
router.push({
|
||||||
|
path: '/noticeDetail',
|
||||||
|
query: { id }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(getNotifications)
|
||||||
|
|
||||||
|
//删除操作
|
||||||
|
const deleteNotification = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res: any = await myAxios.post(
|
||||||
|
"/projectNotification/delete",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
'AfterScript': 'required-script'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('删除成功');
|
||||||
|
if (projectId.value) {
|
||||||
|
await getNotifications (projectId.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error);
|
||||||
|
message.error('删除操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 从路由参数中获取项目ID
|
||||||
|
if (typeof route.query.id === "string") {
|
||||||
|
projectId.value = route.query.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改新增按钮的路由跳转,传递projectId到新增页面
|
||||||
|
const goAddProjectNotice = () => {
|
||||||
|
router.push({
|
||||||
|
path: '/addprojectNotice',
|
||||||
|
query: { projectId: projectId.value } // 传递项目ID到新增页面
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goBack = () => {
|
||||||
|
router.push('/project')
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 搜索框 -->
|
||||||
|
<div class="search-box">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-space>
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input-search
|
||||||
|
style="width: 300px"
|
||||||
|
placeholder="请输入项目通知ID"
|
||||||
|
enter-button
|
||||||
|
@search="handleIdSearch"
|
||||||
|
v-model:value="searchId"
|
||||||
|
type="number"
|
||||||
|
class="custom-search"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-button class="custom-button" @click="goAddProjectNotice">新增项目通知</a-button>
|
||||||
|
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
<!-- 修改表格模板 -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="displayData"
|
||||||
|
:scroll="{ x: 1200, y: 450 }"
|
||||||
|
:loading="loading"
|
||||||
|
bordered
|
||||||
|
rowKey="id"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button size="small" danger @click="deleteNotification(record.id)">
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="goToNotificationDetail(record.id)">
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div class="table-footer">
|
||||||
|
<a-button @click="goBack" class="back-button">返回</a-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-alert {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #ffe3e3;
|
||||||
|
color: #ff4444;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ff4444;
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-thead) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row:hover) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*橙色按钮*/
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮样式 */
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持原有的其他样式不变 */
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button) {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button:hover),
|
||||||
|
.custom-search :deep(.ant-input-search-button:focus) {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持输入框原有样式不变 */
|
||||||
|
.custom-search :deep(.ant-input) {
|
||||||
|
border-right-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增表格页脚样式 */
|
||||||
|
.table-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整分页器位置 */
|
||||||
|
:deep(.ant-table-pagination.ant-pagination) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
856
src/view/project/promotionCode.vue
Normal file
856
src/view/project/promotionCode.vue
Normal file
@ -0,0 +1,856 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {ref, onMounted,reactive} from "vue";
|
||||||
|
import {useRoute, useRouter} from "vue-router";
|
||||||
|
import myAxios from "../../api/myAxios";
|
||||||
|
import {message} from "ant-design-vue";
|
||||||
|
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '推广码ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
width: 80,
|
||||||
|
key: 'id',
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '信息Key',
|
||||||
|
dataIndex: 'promoCodeInfoKey',
|
||||||
|
key: 'promoCodeInfoKey',
|
||||||
|
width: 120,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '推广链接',
|
||||||
|
dataIndex: 'promoCodeLink',
|
||||||
|
key: 'promoCodeLink',
|
||||||
|
width: 200,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '推广码图片',
|
||||||
|
dataIndex: 'promoCodeImage',
|
||||||
|
key: 'promoCodeImage',
|
||||||
|
width: 150,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目ID',
|
||||||
|
dataIndex: 'projectId',
|
||||||
|
key: 'projectId',
|
||||||
|
width: 100,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'promoCodeStatus',
|
||||||
|
key: 'promoCodeStatus',
|
||||||
|
width: 100,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 100,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 接口数据类型
|
||||||
|
interface PromoCode {
|
||||||
|
id: number;
|
||||||
|
promoCodeInfoKey: string;
|
||||||
|
promoCodeLink: string;
|
||||||
|
promoCodeImage: string;
|
||||||
|
projectId: number;
|
||||||
|
promoCodeStatus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const projectId = ref<string | number>("");
|
||||||
|
const originalTableData = ref<PromoCode[]>([]); // 存储所有原始数据
|
||||||
|
const displayData = ref<PromoCode[]>([]); // 实际显示的数据
|
||||||
|
const loading = ref(false);
|
||||||
|
const searchId = ref("");
|
||||||
|
const previewVisible = ref(false);
|
||||||
|
const previewImage = ref("");
|
||||||
|
|
||||||
|
// 主查询方法 - 获取项目所有推广码
|
||||||
|
const getPromoCodes = async (id: string | number) => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response: any = await myAxios.post(
|
||||||
|
"/promoCode/queryByPid",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(response);
|
||||||
|
|
||||||
|
if (response.code === 1) {
|
||||||
|
// 确保data是数组,即使为null也转为空数组
|
||||||
|
const data = Array.isArray(response.data) ? response.data : [];
|
||||||
|
originalTableData.value = data;
|
||||||
|
displayData.value = data; // 初始显示所有数据
|
||||||
|
} else {
|
||||||
|
message.error(response.message || "获取数据失败");
|
||||||
|
originalTableData.value = [];
|
||||||
|
displayData.value = [];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("查询错误:", err);
|
||||||
|
message.error("数据加载失败,请重试");
|
||||||
|
originalTableData.value = [];
|
||||||
|
displayData.value = [];
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化逻辑
|
||||||
|
if (typeof route.query.id === "string") {
|
||||||
|
projectId.value = route.query.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (projectId.value) {
|
||||||
|
getPromoCodes(projectId.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 修改搜索处理 - 前端过滤
|
||||||
|
const handleIdSearch = () => {
|
||||||
|
const value = searchId.value.trim();
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
message.warning("请输入有效的推广码ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(value);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
message.warning("ID必须为数字");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在前端原始数据中过滤
|
||||||
|
const result = originalTableData.value.filter(item => item.id === id);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
message.warning("未找到匹配的推广码ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新显示数据
|
||||||
|
displayData.value = result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索 - 显示所有数据
|
||||||
|
const reset = () => {
|
||||||
|
searchId.value = "";
|
||||||
|
displayData.value = originalTableData.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 预览图片
|
||||||
|
const handlePreview = (imageUrl: string) => {
|
||||||
|
previewImage.value = imageUrl;
|
||||||
|
previewVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 新增推广码表单相关状态
|
||||||
|
const addDrawerVisible = ref(false);
|
||||||
|
const addFormState = reactive({
|
||||||
|
promoCodeInfoKey: '',
|
||||||
|
promoCodeLink: '',
|
||||||
|
promoCodeImage: '',
|
||||||
|
projectId: 0,
|
||||||
|
promoCodeStatus: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 打开新增表单
|
||||||
|
const openAddDrawer = () => {
|
||||||
|
addFormState.projectId = Number(projectId.value); // 自动关联当前项目
|
||||||
|
addDrawerVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 提交新增请求
|
||||||
|
const handleAddSubmit = async () => {
|
||||||
|
// 如果有文件需要上传
|
||||||
|
if (fileList.value.length > 0) {
|
||||||
|
const imageUrl = await handleUpload();
|
||||||
|
if (!imageUrl) {
|
||||||
|
message.error('图片上传失败,无法提交');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 更新表单中的图片URL
|
||||||
|
addFormState.promoCodeImage = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/promoCode/add",
|
||||||
|
{
|
||||||
|
promoCodeInfoKey: addFormState.promoCodeInfoKey,
|
||||||
|
promoCodeLink: addFormState.promoCodeLink,
|
||||||
|
promoCodeImage: addFormState.promoCodeImage,
|
||||||
|
projectId: addFormState.projectId,
|
||||||
|
promoCodeStatus: addFormState.promoCodeStatus
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('新增成功');
|
||||||
|
addDrawerVisible.value = false;
|
||||||
|
await getPromoCodes(projectId.value);
|
||||||
|
// 重置表单和文件列表
|
||||||
|
Object.assign(addFormState, {
|
||||||
|
promoCodeInfoKey: '',
|
||||||
|
promoCodeLink: '',
|
||||||
|
promoCodeImage: '',
|
||||||
|
promoCodeStatus: false,
|
||||||
|
projectId: Number(projectId.value)
|
||||||
|
});
|
||||||
|
fileList.value = [];
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '新增失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('新增请求失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绑定新增按钮点击事件
|
||||||
|
const goAddCode = () => {
|
||||||
|
openAddDrawer();
|
||||||
|
};
|
||||||
|
|
||||||
|
//删除操作
|
||||||
|
const deletePromoCode = async (id: number) => {
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/promoCode/delete",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('删除成功');
|
||||||
|
// 刷新数据
|
||||||
|
await getPromoCodes(projectId.value);
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//编辑
|
||||||
|
// 编辑相关状态
|
||||||
|
const editDrawerVisible = ref(false);
|
||||||
|
const editFormState = reactive({
|
||||||
|
id: 0,
|
||||||
|
promoCodeInfoKey: '',
|
||||||
|
promoCodeLink: '',
|
||||||
|
promoCodeImage: '',
|
||||||
|
projectId: 0,
|
||||||
|
promoCodeStatus: false
|
||||||
|
});
|
||||||
|
const getPromoCodeDetail = async (id: number) => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
const response:any = await myAxios.post(
|
||||||
|
"/promoCode/queryById",
|
||||||
|
{ id },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (response.code === 1) {
|
||||||
|
editFormState.id = response.data.id;
|
||||||
|
editFormState.promoCodeInfoKey = response.data.promoCodeInfoKey;
|
||||||
|
editFormState.promoCodeLink = response.data.promoCodeLink;
|
||||||
|
editFormState.promoCodeImage = response.data.promoCodeImage;
|
||||||
|
editFormState.projectId = response.data.projectId;
|
||||||
|
editFormState.promoCodeStatus = response.data.promoCodeStatus;
|
||||||
|
} else {
|
||||||
|
message.error(response.message || "获取编辑数据失败");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
message.error("获取编辑数据失败,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = async (id: number) => {
|
||||||
|
await getPromoCodeDetail(id);
|
||||||
|
editDrawerVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSubmit = async () => {
|
||||||
|
// 如果有文件需要上传
|
||||||
|
if (fileList.value.length > 0) {
|
||||||
|
const imageUrl = await handleUpload();
|
||||||
|
if (!imageUrl) {
|
||||||
|
message.error('图片上传失败,无法提交');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 更新表单中的图片URL
|
||||||
|
editFormState.promoCodeImage = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/promoCode/update",
|
||||||
|
{
|
||||||
|
id: editFormState.id,
|
||||||
|
promoCodeInfoKey: editFormState.promoCodeInfoKey,
|
||||||
|
promoCodeLink: editFormState.promoCodeLink,
|
||||||
|
promoCodeImage: editFormState.promoCodeImage,
|
||||||
|
projectId: editFormState.projectId,
|
||||||
|
promoCodeStatus: editFormState.promoCodeStatus
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('编辑成功');
|
||||||
|
editDrawerVisible.value = false;
|
||||||
|
await getPromoCodes(projectId.value);
|
||||||
|
// 重置文件列表
|
||||||
|
fileList.value = [];
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '编辑失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('编辑请求失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//返回上一级
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 返回上一级方法
|
||||||
|
const goBack = () => {
|
||||||
|
router.go(-1); // 返回上一页
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加文件上传相关状态
|
||||||
|
const fileList = ref<any[]>([]);
|
||||||
|
const uploading = ref(false);
|
||||||
|
|
||||||
|
// 图片上传处理
|
||||||
|
const handleUpload = async () => {
|
||||||
|
const formData = new FormData();
|
||||||
|
fileList.value.forEach(file => {
|
||||||
|
formData.append('file', file);
|
||||||
|
});
|
||||||
|
formData.append('biz', 'project'); // 添加biz参数
|
||||||
|
|
||||||
|
try {
|
||||||
|
uploading.value = true;
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/file/upload",
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': storedToken,
|
||||||
|
'AflatScript': 'required', // 添加AflatScript头部
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success('上传成功');
|
||||||
|
// 将返回的文件路径保存到表单中
|
||||||
|
addFormState.promoCodeImage = res.data;
|
||||||
|
return res.data;
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '上传失败');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传失败:', error);
|
||||||
|
message.error('上传失败');
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
uploading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// 文件上传前的处理
|
||||||
|
const beforeUpload = (file: any) => {
|
||||||
|
// 检查文件类型
|
||||||
|
const isImage = file.type.includes('image');
|
||||||
|
if (!isImage) {
|
||||||
|
message.error('只能上传图片文件!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小 (限制为5MB)
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||||
|
if (!isLt5M) {
|
||||||
|
message.error('图片大小不能超过5MB!');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到文件列表
|
||||||
|
fileList.value = [file];
|
||||||
|
return false; // 手动上传
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清除文件
|
||||||
|
const clearFile = () => {
|
||||||
|
fileList.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加批量选择相关状态
|
||||||
|
const selectedRowKeys = ref<number[]>([]); // 存储选中的行ID
|
||||||
|
const batchDeleteLoading = ref(false); // 批量删除加载状态
|
||||||
|
|
||||||
|
// 行选择配置
|
||||||
|
const rowSelection = {
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (selectedKeys: number[]) => {
|
||||||
|
selectedRowKeys.value = selectedKeys;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量删除方法
|
||||||
|
const batchDelete = async () => {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
message.warning('请至少选择一条记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
batchDeleteLoading.value = true;
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
const res:any = await myAxios.post(
|
||||||
|
"/promoCode/delBatch",
|
||||||
|
{ ids: selectedRowKeys.value }, // 发送选中ID数组
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: storedToken,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (res.code === 1) {
|
||||||
|
message.success(`成功删除 ${selectedRowKeys.value.length} 条记录`);
|
||||||
|
// 刷新数据
|
||||||
|
await getPromoCodes(projectId.value);
|
||||||
|
// 清空选择
|
||||||
|
selectedRowKeys.value = [];
|
||||||
|
} else {
|
||||||
|
message.error(res.message || '批量删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('批量删除操作失败');
|
||||||
|
} finally {
|
||||||
|
batchDeleteLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="search-box">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-space>
|
||||||
|
<a-form-item label="推广码ID">
|
||||||
|
<a-input-search
|
||||||
|
style="width: 300px"
|
||||||
|
placeholder="输入推广码ID进行查询"
|
||||||
|
enter-button
|
||||||
|
@search="handleIdSearch"
|
||||||
|
v-model:value="searchId"
|
||||||
|
type="number"
|
||||||
|
class="custom-search"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||||
|
<a-button class="custom-button" @click="goAddCode">新增推广码</a-button>
|
||||||
|
<a-button @click="goBack" class="custom-button">返回</a-button>
|
||||||
|
|
||||||
|
<!-- 添加批量删除按钮 -->
|
||||||
|
<a-popconfirm
|
||||||
|
title="确定要删除选中的推广码吗?"
|
||||||
|
@confirm="batchDelete"
|
||||||
|
ok-text="确定"
|
||||||
|
cancel-text="取消"
|
||||||
|
>
|
||||||
|
<a-button
|
||||||
|
class="custom-button"
|
||||||
|
type="danger"
|
||||||
|
:disabled="selectedRowKeys.length === 0"
|
||||||
|
:loading="batchDeleteLoading"
|
||||||
|
>
|
||||||
|
批量删除 ({{ selectedRowKeys.length }})
|
||||||
|
</a-button>
|
||||||
|
</a-popconfirm>
|
||||||
|
|
||||||
|
<a-form-item label="当前项目ID">
|
||||||
|
<a-tag color="blue">{{ projectId }}</a-tag>
|
||||||
|
</a-form-item>
|
||||||
|
</a-space>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<a-table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="displayData"
|
||||||
|
:scroll="{ x: 1000, y: 550 }"
|
||||||
|
:loading="loading"
|
||||||
|
bordered
|
||||||
|
rowKey="id"
|
||||||
|
locale="{ emptyText: '暂无数据' }"
|
||||||
|
:row-selection="rowSelection"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'promoCodeStatus'">
|
||||||
|
<a-tag :color="record.promoCodeStatus ? 'red' : 'green'">
|
||||||
|
{{ record.promoCodeStatus ? '占用' : '空闲' }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="column.key === 'promoCodeImage'">
|
||||||
|
<a-image
|
||||||
|
v-if="record.promoCodeImage"
|
||||||
|
:width="80"
|
||||||
|
:src="downLoadImage+record.promoCodeImage"
|
||||||
|
:preview="false"
|
||||||
|
@click="handlePreview(record.promoCodeImage)"
|
||||||
|
/>
|
||||||
|
<span v-else>无图片</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button
|
||||||
|
size="small"
|
||||||
|
danger
|
||||||
|
@click="deletePromoCode(record.id)"
|
||||||
|
:loading="loading"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" @click="handleEdit(record.id)">
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
<!-- 图片预览模态框 -->
|
||||||
|
<a-modal :visible="previewVisible" :footer="null" @cancel="previewVisible = false">
|
||||||
|
<img alt="预览图片" style="width: 100%" :src="downLoadImage+previewImage" />
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
|
<!-- 新增推广码抽屉 -->
|
||||||
|
<a-drawer
|
||||||
|
title="新增推广码"
|
||||||
|
placement="right"
|
||||||
|
:visible="addDrawerVisible"
|
||||||
|
@close="addDrawerVisible = false"
|
||||||
|
width="600"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
:model="addFormState"
|
||||||
|
layout="vertical"
|
||||||
|
@finish="handleAddSubmit"
|
||||||
|
>
|
||||||
|
<a-form-item label="信息Key" required>
|
||||||
|
<a-input v-model:value="addFormState.promoCodeInfoKey" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="推广链接" required>
|
||||||
|
<a-input v-model:value="addFormState.promoCodeLink" />
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="推广码图片" required>
|
||||||
|
<a-upload
|
||||||
|
:file-list="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:remove="clearFile"
|
||||||
|
list-type="picture-card"
|
||||||
|
accept="image/*"
|
||||||
|
:max-count="1"
|
||||||
|
>
|
||||||
|
<div v-if="fileList.length < 1">
|
||||||
|
<plus-outlined />
|
||||||
|
<div class="ant-upload-text">上传图片</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
<div v-if="addFormState.promoCodeImage">
|
||||||
|
已上传图片:
|
||||||
|
<a :href="addFormState.promoCodeImage" target="_blank">查看</a>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="状态">
|
||||||
|
<a-input-number
|
||||||
|
value="空闲"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="项目ID">
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="addFormState.projectId"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-button
|
||||||
|
class="custom-button"
|
||||||
|
html-type="submit"
|
||||||
|
:loading="uploading"
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</a-button>
|
||||||
|
<a-button style="margin-left: 10px" @click="addDrawerVisible = false">
|
||||||
|
取消
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-drawer>
|
||||||
|
<!-- 编辑推广码抽屉 -->
|
||||||
|
<a-drawer
|
||||||
|
title="编辑推广码"
|
||||||
|
placement="right"
|
||||||
|
:visible="editDrawerVisible"
|
||||||
|
@close="editDrawerVisible = false"
|
||||||
|
width="600"
|
||||||
|
>
|
||||||
|
<a-form
|
||||||
|
:model="editFormState"
|
||||||
|
layout="vertical"
|
||||||
|
@finish="handleEditSubmit"
|
||||||
|
>
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input v-model:value="editFormState.id" :disabled="true" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="信息Key" required>
|
||||||
|
<a-input v-model:value="editFormState.promoCodeInfoKey" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="推广链接" required>
|
||||||
|
<a-input v-model:value="editFormState.promoCodeLink" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="推广码图片" required>
|
||||||
|
<a-upload
|
||||||
|
:file-list="fileList"
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:remove="clearFile"
|
||||||
|
list-type="picture-card"
|
||||||
|
accept="image/*"
|
||||||
|
:max-count="1"
|
||||||
|
>
|
||||||
|
<div v-if="fileList.length < 1">
|
||||||
|
<plus-outlined />
|
||||||
|
<div class="ant-upload-text">上传图片</div>
|
||||||
|
</div>
|
||||||
|
</a-upload>
|
||||||
|
<div v-if="editFormState.promoCodeImage">
|
||||||
|
当前图片:
|
||||||
|
<a :href="editFormState.promoCodeImage" target="_blank">查看</a>
|
||||||
|
</div>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="状态">
|
||||||
|
<a-tag>
|
||||||
|
{{ editFormState.promoCodeStatus ? '占用' : '空闲' }}
|
||||||
|
</a-tag>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="项目ID">
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="editFormState.projectId"
|
||||||
|
:disabled="true"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<a-button
|
||||||
|
class="custom-button"
|
||||||
|
html-type="submit"
|
||||||
|
:loading="uploading"
|
||||||
|
>
|
||||||
|
提交
|
||||||
|
</a-button>
|
||||||
|
<a-button style="margin-left: 10px" @click="editDrawerVisible = false">
|
||||||
|
取消
|
||||||
|
</a-button>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-drawer>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-thead) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row:hover) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-thead) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row:hover) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 新增表格页脚样式 */
|
||||||
|
.table-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整分页器位置 */
|
||||||
|
:deep(.ant-table-pagination.ant-pagination) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*橙色按钮*/
|
||||||
|
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button[disabled] {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
opacity: 0.6;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险按钮样式 */
|
||||||
|
.custom-button.ant-btn-dangerous {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button.ant-btn-dangerous:hover,
|
||||||
|
.custom-button.ant-btn-dangerous:focus {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持原有的其他样式不变 */
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button) {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button:hover),
|
||||||
|
.custom-search :deep(.ant-input-search-button:focus) {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持输入框原有样式不变 */
|
||||||
|
.custom-search :deep(.ant-input) {
|
||||||
|
border-right-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-upload-select-picture-card i) {
|
||||||
|
font-size: 32px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-upload-select-picture-card .ant-upload-text) {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*批量删除按钮操作*/
|
||||||
|
.batch-delete-button {
|
||||||
|
margin-left: 10px;
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
border-color: #ff4d4f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-delete-button:hover {
|
||||||
|
background-color: #ff7875;
|
||||||
|
border-color: #ff7875;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 添加选择计数样式 */
|
||||||
|
.selection-count {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #ff4d4f;
|
||||||
|
}
|
||||||
|
</style>
|
257
src/view/settlement/applicationRecord.vue
Normal file
257
src/view/settlement/applicationRecord.vue
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<a-table
|
||||||
|
:scroll="{ x: 2000, y: 550 }"
|
||||||
|
:dataSource="dataSource"
|
||||||
|
:columns="columns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
@change="handleTableChange"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.dataIndex === 'projectImage'">
|
||||||
|
<!-- 修复:添加 alt 属性 -->
|
||||||
|
<img
|
||||||
|
:src="downLoadImage+record.projectImage"
|
||||||
|
alt="项目图片"
|
||||||
|
style="width: 50px; height: 50px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.dataIndex === 'projectCodeImage'">
|
||||||
|
<!-- 修复:添加 alt 属性 -->
|
||||||
|
<img
|
||||||
|
:src="downLoadImage+record.projectCodeImage"
|
||||||
|
alt="推广码"
|
||||||
|
style="width: 50px; height: 50px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-tag
|
||||||
|
color="orange"
|
||||||
|
@click="settlementRecord(record)"
|
||||||
|
>
|
||||||
|
结算记录
|
||||||
|
</a-tag>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import type { TableProps } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import router from "../../router";
|
||||||
|
import {downLoadImage} from "../../api/ImageUrl.ts";
|
||||||
|
|
||||||
|
interface PromoRecord {
|
||||||
|
id: number;
|
||||||
|
salespersonName: string;
|
||||||
|
salespersonPhone: string;
|
||||||
|
promoCodeInfoKey: string;
|
||||||
|
promoCodeLink: string;
|
||||||
|
projectName: string;
|
||||||
|
projectImage: string;
|
||||||
|
projectId: number;
|
||||||
|
userId: number;
|
||||||
|
createTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列配置保持不变
|
||||||
|
const columns = ref([
|
||||||
|
{
|
||||||
|
title: 'ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 10,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['ascend', 'descend']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目名称',
|
||||||
|
dataIndex: 'projectName',
|
||||||
|
key: 'projectName',
|
||||||
|
width: 20,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['ascend', 'descend']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '推广人员',
|
||||||
|
dataIndex: 'salespersonName',
|
||||||
|
key: 'salespersonName',
|
||||||
|
width: 20,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['ascend', 'descend']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'salespersonPhone',
|
||||||
|
key: 'salespersonPhone',
|
||||||
|
width: 20,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['ascend', 'descend']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '推广信息key',
|
||||||
|
dataIndex: 'promoCodeInfoKey',
|
||||||
|
key: 'promoCodeInfoKey',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '推广链接',
|
||||||
|
dataIndex: 'promoCodeLink',
|
||||||
|
key: 'promoCodeLink',
|
||||||
|
width: 60,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '推广码',
|
||||||
|
dataIndex: 'projectCodeImage',
|
||||||
|
key: 'projectCodeImage',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目图片',
|
||||||
|
dataIndex: 'projectImage',
|
||||||
|
key: 'projectImage',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目ID',
|
||||||
|
dataIndex: 'projectId',
|
||||||
|
key: 'projectId',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户ID',
|
||||||
|
dataIndex: 'userId',
|
||||||
|
key: 'userId',
|
||||||
|
width: 20,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
dataIndex: 'createTime',
|
||||||
|
key: 'createTime',
|
||||||
|
width: 40,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true,
|
||||||
|
sortDirections: ['ascend', 'descend']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 30,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dataSource = ref<PromoRecord[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 添加排序参数
|
||||||
|
const sortParams = reactive({
|
||||||
|
sortField: 'id',
|
||||||
|
sortOrder: 'ascend'
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// fetchData 方法保持不变
|
||||||
|
const fetchData = async () => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response:any = await myAxios.post('/promoCodeApply/page', {
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
sortField: sortParams.sortField,
|
||||||
|
sortOrder: sortParams.sortOrder
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code === 1) {
|
||||||
|
dataSource.value = response.data.records;
|
||||||
|
pagination.total = response.data.total;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修复:简化排序处理逻辑
|
||||||
|
const handleTableChange: TableProps['onChange'] = (pag, _, sorter) => {
|
||||||
|
// 处理分页参数
|
||||||
|
if (pag) {
|
||||||
|
pagination.current = pag.current!;
|
||||||
|
pagination.pageSize = pag.pageSize!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复:简化排序处理
|
||||||
|
if (sorter) {
|
||||||
|
// 处理排序参数(只处理单列排序)
|
||||||
|
const { field, order } = sorter as { field?: string; order?: string };
|
||||||
|
|
||||||
|
if (field && order) {
|
||||||
|
sortParams.sortField = field;
|
||||||
|
sortParams.sortOrder = order;
|
||||||
|
} else {
|
||||||
|
// 取消排序时重置
|
||||||
|
sortParams.sortField = 'id';
|
||||||
|
sortParams.sortOrder = 'ascend';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const settlementRecord = (record: PromoRecord) => {
|
||||||
|
router.push({
|
||||||
|
path: '/moneyRecord',
|
||||||
|
query: {
|
||||||
|
id: String(record.id),
|
||||||
|
projectId: String(record.projectId),
|
||||||
|
userId: String(record.userId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
517
src/view/settlement/moneyRecord.vue
Normal file
517
src/view/settlement/moneyRecord.vue
Normal file
@ -0,0 +1,517 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<!-- 搜索和筛选区域 -->
|
||||||
|
<div class="search-box">
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-space>
|
||||||
|
<a-form-item label="ID">
|
||||||
|
<a-input-search
|
||||||
|
style="width: 300px"
|
||||||
|
placeholder="请输入项目结算记录ID"
|
||||||
|
enter-button
|
||||||
|
@search="handleIdSearch"
|
||||||
|
v-model:value="searchId"
|
||||||
|
type="number"
|
||||||
|
class="custom-search"
|
||||||
|
min="0"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="收益来源">
|
||||||
|
<a-select
|
||||||
|
v-model:value="revenueSourceFilter"
|
||||||
|
style="width: 120px"
|
||||||
|
@change="applyFilters"
|
||||||
|
>
|
||||||
|
<a-select-option value="all">全部</a-select-option>
|
||||||
|
<a-select-option :value="false">推广码</a-select-option>
|
||||||
|
<a-select-option :value="true">抽成</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-button class="custom-button" @click="reset">重置搜索</a-button>
|
||||||
|
|
||||||
|
<a-button class="custom-button" @click="showModal">新增项目明细</a-button>
|
||||||
|
<a-button class="custom-button" @click="goBack">返回</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<a-table
|
||||||
|
:dataSource="paginatedData"
|
||||||
|
:columns="columns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
@change="handleTableChange"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<!-- 格式化金额显示 -->
|
||||||
|
<template v-if="column.dataIndex === 'settlementRevenue'">
|
||||||
|
¥{{ record.settlementRevenue.toFixed(2) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 收益来源列 -->
|
||||||
|
<template v-if="column.dataIndex === 'revenueSource'">
|
||||||
|
<a-tag v-if="record.revenueSource" color="orange">抽成</a-tag>
|
||||||
|
<a-tag v-else color="green">推广码</a-tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 操作列 -->
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button size="small" danger @click="deleteRecord(record.id)">删除</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
|
||||||
|
<!-- 新增项目明细弹窗 -->
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="visible"
|
||||||
|
title="新增项目明细"
|
||||||
|
@ok="handleAdd"
|
||||||
|
@cancel="handleCancel"
|
||||||
|
>
|
||||||
|
<a-form :model="form">
|
||||||
|
<a-form-item label="项目明细名称" required>
|
||||||
|
<a-input v-model:value="form.projectDetailName" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="结算数量" :rules="[{ required: true, message: '请输入结算数量' }]" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="form.settlementQuantity"
|
||||||
|
min="0"
|
||||||
|
:precision="0"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入格式为1"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="结算收益" :rules="[{ required: true, message: '请输入结算收益' }]" required>
|
||||||
|
<a-input-number
|
||||||
|
v-model:value="form.settlementRevenue"
|
||||||
|
min="0"
|
||||||
|
:precision="2"
|
||||||
|
style="width: 100%"
|
||||||
|
placeholder="请输入格式为0.01"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="作业时间" required>
|
||||||
|
<a-date-picker v-model:value="form.workTime" format="YYYY-MM-DD" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="结算时间" required>
|
||||||
|
<a-date-picker v-model:value="form.settlementTime" format="YYYY-MM-DD" />
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="推广码申请ID">
|
||||||
|
<a-input-number v-model:value="form.promoCodeApplyId" disabled/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="项目ID">
|
||||||
|
<a-input-number v-model:value="form.projectId" disabled/>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="用户ID">
|
||||||
|
<a-input-number v-model:value="form.userId" disabled/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue';
|
||||||
|
import type { TableProps } from 'ant-design-vue';
|
||||||
|
import myAxios from '../../api/myAxios.ts';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import router from '../../router';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
|
// 项目结算记录接口类型
|
||||||
|
interface SettlementRecord {
|
||||||
|
id: number;
|
||||||
|
projectDetailName: string;
|
||||||
|
settlementQuantity: number;
|
||||||
|
settlementRevenue: number;
|
||||||
|
settlementTime: string;
|
||||||
|
workTime: string;
|
||||||
|
revenueSource: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns = ref([
|
||||||
|
{
|
||||||
|
title: '项目结算记录ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '项目明细名称',
|
||||||
|
dataIndex: 'projectDetailName',
|
||||||
|
key: 'projectDetailName',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '结算数量',
|
||||||
|
dataIndex: 'settlementQuantity',
|
||||||
|
key: 'settlementQuantity',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '结算收益',
|
||||||
|
dataIndex: 'settlementRevenue',
|
||||||
|
key: 'settlementRevenue',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '结算时间',
|
||||||
|
dataIndex: 'settlementTime',
|
||||||
|
key: 'settlementTime',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '作业时间',
|
||||||
|
dataIndex: 'workTime',
|
||||||
|
key: 'workTime',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '收益来源',
|
||||||
|
dataIndex: 'revenueSource',
|
||||||
|
key: 'revenueSource',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 100,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const allData = ref<SettlementRecord[]>([]); // 存储所有数据
|
||||||
|
const filteredData = ref<SettlementRecord[]>([]); // 存储过滤后的数据
|
||||||
|
const loading = ref(false);
|
||||||
|
const searchId = ref(''); // 新增搜索ID绑定
|
||||||
|
const originalData = ref<SettlementRecord[]>([]); // 存储原始数据
|
||||||
|
|
||||||
|
// 分页配置
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取路由参数
|
||||||
|
const route = useRoute();
|
||||||
|
const idFromRoute = route.query.id as string | undefined;
|
||||||
|
const projectIdFromRoute = route.query.projectId as string | undefined;
|
||||||
|
const userIdFromRoute = route.query.userId as string | undefined;
|
||||||
|
|
||||||
|
// 计算属性:根据分页参数生成表格渲染用的分页数据
|
||||||
|
const paginatedData = computed(() => {
|
||||||
|
const start = (pagination.current - 1) * pagination.pageSize;
|
||||||
|
const end = start + pagination.pageSize;
|
||||||
|
return filteredData.value.slice(start, end);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取项目结算记录数据
|
||||||
|
const fetchData = async () => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
const response: any = await myAxios.post(
|
||||||
|
'/projectSettlement/queryByPId',
|
||||||
|
{ id: idFromRoute }, // 使用从路由获取的id
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: storedToken,
|
||||||
|
AfterScript: 'required-script',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('接口响应:', response);
|
||||||
|
|
||||||
|
if (response.code === 1) {
|
||||||
|
allData.value = response.data;
|
||||||
|
originalData.value = [...response.data]; // 保存原始数据
|
||||||
|
filteredData.value = [...response.data];
|
||||||
|
pagination.total = response.data.length;
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '获取数据失败');
|
||||||
|
allData.value = [];
|
||||||
|
originalData.value = [];
|
||||||
|
filteredData.value = [];
|
||||||
|
pagination.total = 0;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
message.error('请求数据失败,请稍后重试');
|
||||||
|
allData.value = [];
|
||||||
|
originalData.value = [];
|
||||||
|
filteredData.value = [];
|
||||||
|
pagination.total = 0;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ID搜索处理 - 前端过滤
|
||||||
|
const handleIdSearch = (value: string) => {
|
||||||
|
const idStr = value.trim();
|
||||||
|
if (!idStr) {
|
||||||
|
// 如果搜索值为空,显示所有数据
|
||||||
|
reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = Number(idStr);
|
||||||
|
if (isNaN(id)) {
|
||||||
|
message.warning('ID必须为数字');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 前端过滤 originalData 数据
|
||||||
|
const result = originalData.value.filter((record) => record.id === id);
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
filteredData.value = result;
|
||||||
|
pagination.total = result.length;
|
||||||
|
pagination.current = 1;
|
||||||
|
} else {
|
||||||
|
message.info('未找到匹配的记录');
|
||||||
|
filteredData.value = [];
|
||||||
|
pagination.total = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
const reset = () => {
|
||||||
|
searchId.value = '';
|
||||||
|
filteredData.value = [...originalData.value];
|
||||||
|
pagination.total = originalData.value.length;
|
||||||
|
pagination.current = 1; // 重置页码
|
||||||
|
revenueSourceFilter.value = 'all'; // 重置收益来源筛选
|
||||||
|
applyFilters();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 表格分页变更处理
|
||||||
|
const handleTableChange: TableProps['onChange'] = (pag) => {
|
||||||
|
if (pag) {
|
||||||
|
pagination.current = pag.current!;
|
||||||
|
pagination.pageSize = pag.pageSize!;
|
||||||
|
// 计算属性自动根据 pagination 变化更新,无需额外调用
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回按钮(修正拼写,保持语义清晰)
|
||||||
|
const goBack = () => {
|
||||||
|
router.push('/applicationRecord');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除记录
|
||||||
|
const deleteRecord = async (id: number) => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response: any = await myAxios.post(
|
||||||
|
'/projectSettlement/delete',
|
||||||
|
{ id }, // 要删除的记录ID
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: storedToken,
|
||||||
|
AfterScript: 'required-script',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('删除接口响应:', response);
|
||||||
|
|
||||||
|
if (response.code === 1) {
|
||||||
|
message.success('删除成功');
|
||||||
|
// 重新加载数据
|
||||||
|
if (searchId.value) {
|
||||||
|
// 如果当前在搜索状态,重新搜索
|
||||||
|
handleIdSearch(searchId.value);
|
||||||
|
} else {
|
||||||
|
// 否则重新获取所有数据
|
||||||
|
await fetchData(); // 异步函数调用加 await
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除请求失败:', error);
|
||||||
|
message.error('删除数据失败,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const visible = ref(false);
|
||||||
|
const form = reactive({
|
||||||
|
projectDetailName: '',
|
||||||
|
settlementQuantity: '',
|
||||||
|
settlementRevenue: '',
|
||||||
|
workTime: null,
|
||||||
|
settlementTime: null,
|
||||||
|
// 新增:自动填充路由参数
|
||||||
|
promoCodeApplyId: idFromRoute || 0,
|
||||||
|
projectId: projectIdFromRoute || 0,
|
||||||
|
userId: userIdFromRoute || 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
visible.value = true;
|
||||||
|
// 重置非路由传递的字段(避免重复填充)
|
||||||
|
form.projectDetailName = '';
|
||||||
|
form.settlementQuantity = '';
|
||||||
|
form.settlementRevenue ='';
|
||||||
|
form.workTime = null;
|
||||||
|
form.settlementTime = null;
|
||||||
|
};
|
||||||
|
const handleCancel = () => {
|
||||||
|
visible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = async () => {
|
||||||
|
if (!form.projectDetailName || !form.settlementQuantity || !form.settlementRevenue || !form.workTime || !form.settlementTime || !form.promoCodeApplyId || !form.projectId || !form.userId) {
|
||||||
|
message.error('请填写完整信息');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response:any = await myAxios.post(
|
||||||
|
'/projectSettlement/add',
|
||||||
|
{
|
||||||
|
projectDetailName: form.projectDetailName,
|
||||||
|
settlementQuantity: form.settlementQuantity,
|
||||||
|
settlementRevenue: form.settlementRevenue,
|
||||||
|
workTime: dayjs(form.workTime).format('YYYY-MM-DD'),
|
||||||
|
settlementTime: dayjs(form.settlementTime).format('YYYY-MM-DD'),
|
||||||
|
promoCodeApplyId: form.promoCodeApplyId,
|
||||||
|
projectId: form.projectId,
|
||||||
|
userId: form.userId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: storedToken,
|
||||||
|
AfterScript: 'required-script',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.code === 1) {
|
||||||
|
message.success('新增成功');
|
||||||
|
visible.value = false;
|
||||||
|
await fetchData(); // 刷新数据
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '新增失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
message.error('新增数据失败,请稍后重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const revenueSourceFilter = ref<string | boolean>('all'); // 收益来源筛选值
|
||||||
|
|
||||||
|
// 应用筛选条件
|
||||||
|
const applyFilters = () => {
|
||||||
|
let result = [...originalData.value];
|
||||||
|
|
||||||
|
// 应用ID筛选
|
||||||
|
if (searchId.value.trim() !== '') {
|
||||||
|
const id = Number(searchId.value.trim());
|
||||||
|
if (!isNaN(id)) {
|
||||||
|
result = result.filter(record => record.id === id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用收益来源筛选
|
||||||
|
if (revenueSourceFilter.value !== 'all') {
|
||||||
|
result = result.filter(record => record.revenueSource === revenueSourceFilter.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredData.value = result;
|
||||||
|
pagination.total = result.length;
|
||||||
|
pagination.current = 1;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 橙色按钮样式 */
|
||||||
|
.custom-button {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-button:hover,
|
||||||
|
.custom-button:focus {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式调整 */
|
||||||
|
:deep(.ant-table-thead) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-table-row:hover) {
|
||||||
|
background-color: #f9f9f9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索按钮样式 */
|
||||||
|
.custom-search :deep(.ant-input-search-button) {
|
||||||
|
background-color: #ffa940;
|
||||||
|
border-color: #ffa940;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-search :deep(.ant-input-search-button:hover),
|
||||||
|
.custom-search :deep(.ant-input-search-button:focus) {
|
||||||
|
background-color: #fa8c16;
|
||||||
|
border-color: #fa8c16;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持输入框原有样式不变 */
|
||||||
|
.custom-search :deep(.ant-input) {
|
||||||
|
border-right-color: #ffa940;
|
||||||
|
}
|
||||||
|
</style>
|
237
src/view/settlement/withdrawalApplicationRecord.vue
Normal file
237
src/view/settlement/withdrawalApplicationRecord.vue
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<a-table
|
||||||
|
:scroll="{ x: 2000, y: 550 }"
|
||||||
|
:dataSource="dataSource"
|
||||||
|
:columns="columns"
|
||||||
|
:pagination="pagination"
|
||||||
|
:loading="loading"
|
||||||
|
@change="handleTableChange"
|
||||||
|
bordered
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'withdrawnAmount'">
|
||||||
|
¥{{record.withdrawnAmount }}
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'withdrawalStatus'">
|
||||||
|
<a-tag :color="getStatusColor(record.withdrawalStatus)">
|
||||||
|
{{ getStatusText(record.withdrawalStatus) }}
|
||||||
|
</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-tag
|
||||||
|
color="orange"
|
||||||
|
@click="viewDetails(record)"
|
||||||
|
>
|
||||||
|
查看详情
|
||||||
|
</a-tag>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue';
|
||||||
|
import type { TableProps } from 'ant-design-vue';
|
||||||
|
import myAxios from "../../api/myAxios.ts";
|
||||||
|
import router from "../../router";
|
||||||
|
|
||||||
|
interface WithdrawalRecord {
|
||||||
|
id: number;
|
||||||
|
cardHolder: string;
|
||||||
|
idCardNumber: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
bankCardNumber: string;
|
||||||
|
openBank: string;
|
||||||
|
withdrawAmount: number;
|
||||||
|
withdrawalStatus: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改后的列配置 - 添加排序功能
|
||||||
|
const columns = ref([
|
||||||
|
{
|
||||||
|
title: '提现申请记录ID',
|
||||||
|
dataIndex: 'id',
|
||||||
|
key: 'id',
|
||||||
|
width: 80,
|
||||||
|
fixed: 'left',
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '持卡人',
|
||||||
|
dataIndex: 'cardHolder',
|
||||||
|
key: 'cardHolder',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '身份证号',
|
||||||
|
dataIndex: 'idCardNumber',
|
||||||
|
key: 'idCardNumber',
|
||||||
|
width: 180,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phoneNumber',
|
||||||
|
key: 'phoneNumber',
|
||||||
|
width: 140,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '银行卡号',
|
||||||
|
dataIndex: 'bankCardNumber',
|
||||||
|
key: 'bankCardNumber',
|
||||||
|
width: 180,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '开户银行',
|
||||||
|
dataIndex: 'openBank',
|
||||||
|
key: 'openBank',
|
||||||
|
width: 150,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提现金额',
|
||||||
|
dataIndex: 'withdrawnAmount',
|
||||||
|
key: 'withdrawnAmount',
|
||||||
|
width: 120,
|
||||||
|
align: 'center',
|
||||||
|
sorter: true, // 添加排序功能
|
||||||
|
sortDirections: ['ascend', 'descend'] // 允许升序降序
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '提取状态',
|
||||||
|
dataIndex: 'withdrawalStatus',
|
||||||
|
key: 'withdrawalStatus',
|
||||||
|
width: 120,
|
||||||
|
align: 'center'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 100,
|
||||||
|
align: 'center'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dataSource = ref<WithdrawalRecord[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 添加排序参数
|
||||||
|
const sortParams = reactive({
|
||||||
|
sortField: 'id',
|
||||||
|
sortOrder: 'ascend'
|
||||||
|
});
|
||||||
|
|
||||||
|
const pagination = reactive({
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 状态映射函数
|
||||||
|
const getStatusText = (status: string) => {
|
||||||
|
const statusMap: Record<string, string> = {
|
||||||
|
'processing': '提现中',
|
||||||
|
'success': '提现成功',
|
||||||
|
'failed': '提现失败'
|
||||||
|
};
|
||||||
|
return statusMap[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 状态颜色映射函数
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
'processing': 'blue',
|
||||||
|
'success': 'green',
|
||||||
|
'failed': 'red'
|
||||||
|
};
|
||||||
|
return colorMap[status] || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改后的 fetchData 方法 - 添加排序参数
|
||||||
|
const fetchData = async () => {
|
||||||
|
const storedToken = localStorage.getItem('token');
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const response: any = await myAxios.post('/withdrawalApply/page', {
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
sortField: sortParams.sortField, // 添加排序字段
|
||||||
|
sortOrder: sortParams.sortOrder, // 添加排序方式
|
||||||
|
withdrawalStatus: 'processing'
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': storedToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code === 1) {
|
||||||
|
dataSource.value = response.data.records;
|
||||||
|
pagination.total = response.data.total;
|
||||||
|
pagination.current = response.data.current;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('请求失败:', error);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改后的 handleTableChange 方法 - 处理排序
|
||||||
|
const handleTableChange: TableProps['onChange'] = (pag, _, sorter) => {
|
||||||
|
// 处理分页参数
|
||||||
|
if (pag) {
|
||||||
|
pagination.current = pag.current!;
|
||||||
|
pagination.pageSize = pag.pageSize!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理排序参数
|
||||||
|
if (sorter) {
|
||||||
|
const { field, order } = sorter as { field?: string; order?: string };
|
||||||
|
|
||||||
|
if (field && order) {
|
||||||
|
sortParams.sortField = field;
|
||||||
|
sortParams.sortOrder = order;
|
||||||
|
} else {
|
||||||
|
// 取消排序时重置
|
||||||
|
sortParams.sortField = 'id';
|
||||||
|
sortParams.sortOrder = 'ascend';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData();
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData();
|
||||||
|
});
|
||||||
|
|
||||||
|
const viewDetails = (record: WithdrawalRecord) => {
|
||||||
|
router.push({
|
||||||
|
path: '/withdrawalDetail',
|
||||||
|
query: { id: record.id.toString() }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
1518
src/view/userList/userList.vue
Normal file
1518
src/view/userList/userList.vue
Normal file
File diff suppressed because it is too large
Load Diff
3
src/view/work/workDetail.vue
Normal file
3
src/view/work/workDetail.vue
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
123
|
||||||
|
</template>
|
4
src/view/work/workList.vue
Normal file
4
src/view/work/workList.vue
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<template>
|
||||||
|
789
|
||||||
|
</template>
|
||||||
|
|
16
src/vite-env.d.ts
vendored
Normal file
16
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
declare module '*.vue' {
|
||||||
|
import type {DefineComponent} from 'vue'
|
||||||
|
const vueComponent: DefineComponent<{}, {}, any>
|
||||||
|
export default vueComponent
|
||||||
|
}
|
||||||
|
declare module '*.mjs'
|
||||||
|
declare module 'dayjs'
|
||||||
|
// interface ImportMetaEnv {
|
||||||
|
// readonly VITE_RSA_PUBLIC_KEY: string
|
||||||
|
// // 添加其他环境变量...
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// interface ImportMeta {
|
||||||
|
// readonly env: ImportMetaEnv
|
||||||
|
// }
|
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal 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
33
tsconfig.json
Normal 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
11
tsconfig.node.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
1
tsconfig.node.tsbuildinfo
Normal file
1
tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal 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
2
vite.config.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
declare const _default: import("vite").UserConfig;
|
||||||
|
export default _default;
|
38
vite.config.js
Normal file
38
vite.config.js
Normal 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
39
vite.config.ts
Normal 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" // 显式包含依赖进行预构建
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
})
|
Reference in New Issue
Block a user