first commit

This commit is contained in:
aaron 2025-03-08 20:28:16 +08:00
commit 8a0801b2ab
18 changed files with 1629 additions and 0 deletions

73
.gitignore vendored Normal file
View File

@ -0,0 +1,73 @@
# 依赖包
/node_modules
/.pnp
.pnp.js
# 构建输出
/dist
/build
# 测试覆盖率
/coverage
# 本地环境文件
.env.local
.env.*.local
# 日志文件
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
logs
*.log
# 编辑器目录和文件
.idea
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.DS_Store
Thumbs.db
# 缓存文件
.cache
.npm
.eslintcache
.stylelintcache
.temp
.cache-loader
# 临时文件
.tmp
temp
tmp
# 打包文件
*.tgz
*.tar.gz
*.zip
# 自动生成的文件
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
desktop.ini
# 本地配置文件
.env
.env.development
.env.test
.env.production
# 其他
.history
.sass-cache

69
README.md Normal file
View File

@ -0,0 +1,69 @@
# 蜂快·运营商平台
基于Vue 3和Ant Design Vue的蜂快·运营商平台。
## 功能特点
- 基于Vue 3、Vuex 4和Vue Router 4
- 使用Ant Design Vue 3作为UI组件库
- 响应式布局,适配不同屏幕尺寸
- 包含常见的后台功能模块:
- 用户管理
- 系统设置
- 仪表盘数据展示
## 开发环境
- Node.js >= 18.0.0
- npm >= 8.6.0
## 安装与运行
1. 安装依赖
```bash
npm install
```
2. 开发模式运行
```bash
npm run dev
```
3. 构建生产版本
```bash
npm run build
```
## 项目结构
```
├── public/ # 静态资源
├── src/ # 源代码
│ ├── assets/ # 资源文件
│ ├── components/ # 公共组件
│ ├── layouts/ # 布局组件
│ ├── router/ # 路由配置
│ ├── store/ # Vuex存储
│ ├── utils/ # 工具函数
│ ├── views/ # 页面组件
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── .babelrc # Babel配置
├── package.json # 项目依赖
├── webpack.config.js # Webpack配置
└── README.md # 项目说明
```
## 登录信息
默认用户名和密码:
- 用户名admin
- 密码admin123
## 许可证
ISC

5
babel.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@babel/preset-env'
]
};

39
package.json Normal file
View File

@ -0,0 +1,39 @@
{
"name": "partner-admin",
"version": "1.0.0",
"description": "基于Ant Design Vue的蜂快·运营商平台",
"main": "index.js",
"scripts": {
"dev": "webpack serve --mode development",
"build": "webpack --mode production",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"vue",
"ant-design-vue",
"admin",
"dashboard"
],
"author": "",
"license": "ISC",
"dependencies": {
"ant-design-vue": "^3.0.0",
"axios": "^1.6.0",
"vue": "^3.0.0",
"vue-router": "^4.0.0",
"vuex": "^4.0.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@vue/compiler-sfc": "^3.0.0",
"babel-loader": "^10.0.0",
"css-loader": "^7.1.2",
"html-webpack-plugin": "^5.0.0",
"style-loader": "^4.0.0",
"vue-loader": "^17.0.0",
"webpack": "^5.0.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
}
}

12
public/index.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>蜂快·运营商平台</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ant-design/icons-svg@4.2.1/lib/asn/index.min.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

24
src/App.vue Normal file
View File

@ -0,0 +1,24 @@
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
}
#app {
height: 100%;
}
</style>

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

BIN
src/assets/sub_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

141
src/layouts/AdminLayout.vue Normal file
View File

@ -0,0 +1,141 @@
<template>
<a-layout style="min-height: 100vh">
<!-- 侧边栏 -->
<a-layout-sider v-model:collapsed="collapsed" collapsible>
<div class="logo">
<h1 v-if="!collapsed">蜂快·运营商平台</h1>
<h1 v-else>蜂快</h1>
</div>
<a-menu
v-model:selectedKeys="selectedKeys"
theme="dark"
mode="inline"
>
<a-menu-item key="dashboard" @click="() => $router.push('/dashboard')">
<template #icon><dashboard-outlined /></template>
<span>仪表盘</span>
</a-menu-item>
<a-menu-item key="user" @click="() => $router.push('/user')">
<template #icon><user-outlined /></template>
<span>用户管理</span>
</a-menu-item>
<a-menu-item key="settings" @click="() => $router.push('/settings')">
<template #icon><setting-outlined /></template>
<span>系统设置</span>
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<!-- 头部 -->
<a-layout-header style="background: #fff; padding: 0">
<div class="header-right">
<a-dropdown>
<a class="ant-dropdown-link" @click.prevent>
<a-avatar :src="userInfo?.avatar" />
<span style="margin-left: 8px">{{ userInfo?.name || '用户' }}</span>
</a>
<template #overlay>
<a-menu>
<a-menu-item key="0">
<a href="javascript:;" @click="handleLogout">退出登录</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
</a-layout-header>
<!-- 内容区 -->
<a-layout-content style="margin: 16px">
<div :style="{ padding: '24px', background: '#fff', minHeight: '360px' }">
<router-view></router-view>
</div>
</a-layout-content>
<!-- 底部 -->
<a-layout-footer style="text-align: center">
蜂快·运营商平台 ©2025 Created by Admin
</a-layout-footer>
</a-layout>
</a-layout>
</template>
<script>
import { ref, computed, watch, onMounted } from 'vue';
import { useStore } from 'vuex';
import { useRoute, useRouter } from 'vue-router';
import {
DashboardOutlined,
UserOutlined,
SettingOutlined
} from '@ant-design/icons-vue';
export default {
name: 'AdminLayout',
components: {
DashboardOutlined,
UserOutlined,
SettingOutlined
},
setup() {
const store = useStore();
const route = useRoute();
const router = useRouter();
const collapsed = computed({
get: () => store.getters.sidebarCollapsed,
set: (value) => store.commit('TOGGLE_SIDEBAR')
});
const userInfo = computed(() => store.getters.userInfo);
const selectedKeys = ref([]);
//
watch(() => route.path, (path) => {
const key = path.split('/')[1] || 'dashboard';
selectedKeys.value = [key];
}, { immediate: true });
//
onMounted(() => {
if (!store.getters.isAuthenticated) {
router.push('/login');
}
});
const handleLogout = () => {
store.dispatch('logout');
router.push('/login');
};
return {
collapsed,
selectedKeys,
userInfo,
handleLogout
};
}
};
</script>
<style scoped>
.logo {
height: 32px;
margin: 16px;
color: white;
text-align: center;
overflow: hidden;
}
.logo h1 {
color: white;
font-size: 18px;
margin: 0;
}
.header-right {
float: right;
margin-right: 24px;
}
</style>

14
src/main.js Normal file
View File

@ -0,0 +1,14 @@
import { createApp } from 'vue';
import Antd from 'ant-design-vue';
import App from './App.vue';
import router from './router';
import store from './store';
import 'ant-design-vue/dist/antd.css';
const app = createApp(App);
app.use(Antd);
app.use(router);
app.use(store);
app.mount('#app');

54
src/router/index.js Normal file
View File

@ -0,0 +1,54 @@
import { createRouter, createWebHistory } from 'vue-router';
import AdminLayout from '../layouts/AdminLayout.vue';
const routes = [
{
path: '/',
component: AdminLayout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
meta: { title: '仪表盘', icon: 'dashboard' }
},
{
path: 'user',
name: 'User',
component: () => import('../views/User.vue'),
meta: { title: '用户管理', icon: 'user' }
},
{
path: 'settings',
name: 'Settings',
component: () => import('../views/Settings.vue'),
meta: { title: '系统设置', icon: 'setting' }
}
]
},
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue')
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 路由守卫
router.beforeEach((to, from, next) => {
// 这里可以添加身份验证逻辑
document.title = to.meta.title ? `${to.meta.title} - 蜂快·运营商平台` : '蜂快·运营商平台';
next();
});
export default router;

52
src/store/index.js Normal file
View File

@ -0,0 +1,52 @@
import { createStore } from 'vuex';
export default createStore({
state: {
user: null,
token: localStorage.getItem('token') || '',
sidebar: {
collapsed: false
}
},
mutations: {
SET_TOKEN(state, token) {
state.token = token;
localStorage.setItem('token', token);
},
CLEAR_TOKEN(state) {
state.token = '';
localStorage.removeItem('token');
},
SET_USER(state, user) {
state.user = user;
},
TOGGLE_SIDEBAR(state) {
state.sidebar.collapsed = !state.sidebar.collapsed;
}
},
actions: {
login({ commit }, userInfo) {
// 这里应该有实际的登录API调用
return new Promise((resolve) => {
// 模拟登录成功
const token = 'mock-token-' + Date.now();
commit('SET_TOKEN', token);
commit('SET_USER', {
name: userInfo.username,
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
roles: ['admin']
});
resolve();
});
},
logout({ commit }) {
commit('CLEAR_TOKEN');
commit('SET_USER', null);
}
},
getters: {
isAuthenticated: state => !!state.token,
userInfo: state => state.user,
sidebarCollapsed: state => state.sidebar.collapsed
}
});

145
src/views/Dashboard.vue Normal file
View File

@ -0,0 +1,145 @@
<template>
<div class="dashboard">
<a-row :gutter="16">
<a-col :span="6">
<a-card>
<template #title>
<span><user-outlined /> 用户总数</span>
</template>
<div class="card-content">
<h2>1,286</h2>
<p>较上周 <a-tag color="green">+12%</a-tag></p>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<template #title>
<span><shopping-outlined /> 订单总数</span>
</template>
<div class="card-content">
<h2>4,389</h2>
<p>较上周 <a-tag color="green">+8%</a-tag></p>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<template #title>
<span><dollar-outlined /> 销售额</span>
</template>
<div class="card-content">
<h2>¥ 126,560</h2>
<p>较上周 <a-tag color="green">+23%</a-tag></p>
</div>
</a-card>
</a-col>
<a-col :span="6">
<a-card>
<template #title>
<span><team-outlined /> 新增会员</span>
</template>
<div class="card-content">
<h2>128</h2>
<p>较上周 <a-tag color="red">-5%</a-tag></p>
</div>
</a-card>
</a-col>
</a-row>
<a-row :gutter="16" style="margin-top: 16px">
<a-col :span="16">
<a-card title="销售趋势">
<div style="height: 300px; display: flex; justify-content: center; align-items: center;">
<p>这里将显示销售趋势图表</p>
</div>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="最近活动">
<a-list item-layout="horizontal" :data-source="activities">
<template #renderItem="{ item }">
<a-list-item>
<a-list-item-meta>
<template #avatar>
<a-avatar :src="item.avatar" />
</template>
<template #title>{{ item.title }}</template>
<template #description>{{ item.description }}</template>
</a-list-item-meta>
</a-list-item>
</template>
</a-list>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script>
import { ref } from 'vue';
import {
UserOutlined,
ShoppingOutlined,
DollarOutlined,
TeamOutlined
} from '@ant-design/icons-vue';
export default {
name: 'Dashboard',
components: {
UserOutlined,
ShoppingOutlined,
DollarOutlined,
TeamOutlined
},
setup() {
const activities = ref([
{
title: '张三',
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description: '刚刚完成了一笔订单'
},
{
title: '李四',
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description: '刚刚注册成为新用户'
},
{
title: '王五',
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description: '提交了一个工单'
},
{
title: '系统通知',
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description: '服务器将于今晚23:00进行维护'
}
]);
return {
activities
};
}
};
</script>
<style scoped>
.dashboard {
padding: 8px;
}
.card-content {
text-align: center;
}
.card-content h2 {
font-size: 24px;
margin-bottom: 8px;
}
.card-content p {
margin: 0;
color: rgba(0, 0, 0, 0.45);
}
</style>

280
src/views/Login.vue Normal file
View File

@ -0,0 +1,280 @@
<template>
<div class="login-container">
<div class="login-box">
<div class="login-left">
<div class="logo-container">
<img src="../assets/logo.png" alt="蜂快到家" class="logo" />
<div class="logo-text">蜂快到家</div>
</div>
<div class="feature-list">
<div class="feature-item">
<check-circle-outlined />
<span>高效管理配送</span>
</div>
<div class="feature-item">
<check-circle-outlined />
<span>实时监控订单</span>
</div>
<div class="feature-item">
<check-circle-outlined />
<span>便捷数据分析</span>
</div>
</div>
</div>
<div class="login-right">
<div class="welcome-text">
<h1>欢迎使用 蜂快·运营商平台</h1>
<p>请使用您的手机号和密码登录</p>
</div>
<a-form
:model="formState"
name="login"
@finish="onFinish"
autocomplete="off"
class="login-form"
>
<a-form-item
name="username"
:rules="[{ required: true, message: '请输入手机号!' }]"
>
<a-input
v-model:value="formState.username"
placeholder="请输入手机号"
size="large"
class="login-input"
>
<template #prefix><user-outlined /></template>
</a-input>
</a-form-item>
<a-form-item
name="password"
:rules="[{ required: true, message: '请输入密码!' }]"
>
<a-input-password
v-model:value="formState.password"
placeholder="请输入密码"
size="large"
class="login-input"
>
<template #prefix><lock-outlined /></template>
</a-input-password>
</a-form-item>
<div class="login-options">
<a-checkbox v-model:checked="formState.remember">记住我</a-checkbox>
</div>
<a-form-item>
<a-button type="primary" html-type="submit" class="login-button" :loading="loading" size="large">
</a-button>
</a-form-item>
</a-form>
<div class="login-footer">
<p>© 2025 蜂快到家. 保留所有权利</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { reactive, ref } from 'vue';
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { UserOutlined, LockOutlined, CheckCircleOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
export default {
name: 'Login',
components: {
UserOutlined,
LockOutlined,
CheckCircleOutlined
},
setup() {
const store = useStore();
const router = useRouter();
const loading = ref(false);
const formState = reactive({
username: 'admin',
password: 'admin123',
remember: true
});
const onFinish = async (values) => {
loading.value = true;
try {
await store.dispatch('login', {
username: formState.username,
password: formState.password
});
message.success('登录成功');
router.push('/');
} catch (error) {
message.error('登录失败: ' + error.message);
} finally {
loading.value = false;
}
};
return {
formState,
loading,
onFinish
};
}
};
</script>
<style scoped>
.login-container {
height: 100vh;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: #f0f8ff;
}
.login-box {
width: 900px;
height: 600px;
display: flex;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.login-left {
width: 40%;
background-color: #2b7aee;
padding: 40px;
display: flex;
flex-direction: column;
justify-content: space-between;
color: white;
}
.logo-container {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 60px;
}
.logo {
width: 100px;
height: auto;
}
.logo-text {
font-size: 24px;
font-weight: bold;
margin-top: 16px;
}
.feature-list {
margin-bottom: 100px;
}
.feature-item {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: 16px;
}
.feature-item .anticon {
margin-right: 12px;
font-size: 18px;
}
.login-right {
width: 60%;
background-color: white;
padding: 40px 60px;
display: flex;
flex-direction: column;
}
.welcome-text {
margin-bottom: 40px;
}
.welcome-text h1 {
font-size: 24px;
font-weight: bold;
color: #333;
margin-bottom: 8px;
}
.welcome-text p {
font-size: 14px;
color: #666;
}
.login-form {
flex: 1;
}
.login-input {
height: 50px;
border-radius: 4px;
}
.login-options {
display: flex;
justify-content: space-between;
margin-bottom: 24px;
}
.login-button {
width: 100%;
height: 50px;
font-size: 16px;
border-radius: 4px;
background-color: #2b7aee;
border-color: #2b7aee;
}
.login-button:hover {
background-color: #1e6cd8;
border-color: #1e6cd8;
}
.login-footer {
text-align: center;
color: #999;
font-size: 14px;
margin-top: 20px;
}
@media (max-width: 768px) {
.login-box {
width: 90%;
height: auto;
flex-direction: column;
}
.login-left {
width: 100%;
padding: 30px;
}
.login-right {
width: 100%;
padding: 30px;
}
.logo-container {
margin-top: 20px;
}
.feature-list {
margin-bottom: 30px;
}
}
</style>

31
src/views/NotFound.vue Normal file
View File

@ -0,0 +1,31 @@
<template>
<div class="not-found">
<a-result
status="404"
title="404"
sub-title="抱歉,您访问的页面不存在。"
>
<template #extra>
<a-button type="primary" @click="$router.push('/')">
返回首页
</a-button>
</template>
</a-result>
</div>
</template>
<script>
export default {
name: 'NotFound'
}
</script>
<style scoped>
.not-found {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
background-color: #f0f2f5;
}
</style>

253
src/views/Settings.vue Normal file
View File

@ -0,0 +1,253 @@
<template>
<div class="settings">
<a-card title="系统设置" :bordered="false">
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="基本设置">
<a-form
:model="basicForm"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 14 }"
@finish="onBasicFormFinish"
>
<a-form-item label="系统名称" name="systemName" :rules="[{ required: true, message: '请输入系统名称' }]">
<a-input v-model:value="basicForm.systemName" />
</a-form-item>
<a-form-item label="系统描述" name="description">
<a-textarea v-model:value="basicForm.description" :rows="4" />
</a-form-item>
<a-form-item label="系统Logo" name="logo">
<a-upload
name="logo"
list-type="picture-card"
class="logo-uploader"
:show-upload-list="false"
action="https://www.mocky.io/v2/5cc8019d300000980a055e76"
:before-upload="beforeUpload"
@change="handleLogoChange"
>
<img v-if="basicForm.logoUrl" :src="basicForm.logoUrl" alt="logo" style="width: 100%" />
<div v-else>
<plus-outlined />
<div style="margin-top: 8px">上传</div>
</div>
</a-upload>
</a-form-item>
<a-form-item label="备案信息" name="icp">
<a-input v-model:value="basicForm.icp" />
</a-form-item>
<a-form-item label="版权信息" name="copyright">
<a-input v-model:value="basicForm.copyright" />
</a-form-item>
<a-form-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" html-type="submit">保存设置</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" tab="安全设置">
<a-form
:model="securityForm"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 14 }"
@finish="onSecurityFormFinish"
>
<a-form-item label="密码策略" name="passwordPolicy">
<a-select v-model:value="securityForm.passwordPolicy">
<a-select-option value="simple">简单 (至少6位)</a-select-option>
<a-select-option value="medium">中等 (至少8位包含字母和数字)</a-select-option>
<a-select-option value="strong"> (至少10位包含大小写字母数字和特殊字符)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="登录失败锁定" name="loginLockEnabled">
<a-switch v-model:checked="securityForm.loginLockEnabled" />
</a-form-item>
<a-form-item label="锁定阈值" name="loginLockThreshold" v-if="securityForm.loginLockEnabled">
<a-input-number v-model:value="securityForm.loginLockThreshold" :min="1" :max="10" />
<span style="margin-left: 8px"></span>
</a-form-item>
<a-form-item label="锁定时间" name="loginLockTime" v-if="securityForm.loginLockEnabled">
<a-input-number v-model:value="securityForm.loginLockTime" :min="1" :max="1440" />
<span style="margin-left: 8px">分钟</span>
</a-form-item>
<a-form-item label="会话超时" name="sessionTimeout">
<a-input-number v-model:value="securityForm.sessionTimeout" :min="1" :max="1440" />
<span style="margin-left: 8px">分钟</span>
</a-form-item>
<a-form-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" html-type="submit">保存设置</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab="邮件设置">
<a-form
:model="emailForm"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 14 }"
@finish="onEmailFormFinish"
>
<a-form-item label="SMTP服务器" name="smtpServer" :rules="[{ required: true, message: '请输入SMTP服务器' }]">
<a-input v-model:value="emailForm.smtpServer" />
</a-form-item>
<a-form-item label="SMTP端口" name="smtpPort" :rules="[{ required: true, message: '请输入SMTP端口' }]">
<a-input-number v-model:value="emailForm.smtpPort" :min="1" :max="65535" style="width: 100%" />
</a-form-item>
<a-form-item label="发件人邮箱" name="senderEmail" :rules="[{ required: true, message: '请输入发件人邮箱' }, { type: 'email', message: '请输入有效的邮箱地址' }]">
<a-input v-model:value="emailForm.senderEmail" />
</a-form-item>
<a-form-item label="发件人名称" name="senderName">
<a-input v-model:value="emailForm.senderName" />
</a-form-item>
<a-form-item label="SMTP用户名" name="smtpUsername" :rules="[{ required: true, message: '请输入SMTP用户名' }]">
<a-input v-model:value="emailForm.smtpUsername" />
</a-form-item>
<a-form-item label="SMTP密码" name="smtpPassword" :rules="[{ required: true, message: '请输入SMTP密码' }]">
<a-input-password v-model:value="emailForm.smtpPassword" />
</a-form-item>
<a-form-item label="启用SSL" name="smtpSsl">
<a-switch v-model:checked="emailForm.smtpSsl" />
</a-form-item>
<a-form-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" html-type="submit" style="margin-right: 10px">保存设置</a-button>
<a-button @click="testEmailSettings">测试连接</a-button>
</a-form-item>
</a-form>
</a-tab-pane>
</a-tabs>
</a-card>
</div>
</template>
<script>
import { reactive } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
export default {
name: 'Settings',
components: {
PlusOutlined
},
setup() {
//
const basicForm = reactive({
systemName: '蜂快·运营商平台',
description: '基于Ant Design Vue的蜂快·运营商平台',
logoUrl: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
icp: '京ICP备12345678号',
copyright: '©2025 蜂快·运营商平台'
});
//
const securityForm = reactive({
passwordPolicy: 'medium',
loginLockEnabled: true,
loginLockThreshold: 5,
loginLockTime: 30,
sessionTimeout: 120
});
//
const emailForm = reactive({
smtpServer: 'smtp.example.com',
smtpPort: 465,
senderEmail: 'admin@example.com',
senderName: '系统管理员',
smtpUsername: 'admin@example.com',
smtpPassword: 'password',
smtpSsl: true
});
//
const onBasicFormFinish = (values) => {
console.log('基本设置表单提交:', values);
message.success('基本设置已保存');
};
//
const onSecurityFormFinish = (values) => {
console.log('安全设置表单提交:', values);
message.success('安全设置已保存');
};
//
const onEmailFormFinish = (values) => {
console.log('邮件设置表单提交:', values);
message.success('邮件设置已保存');
};
//
const testEmailSettings = () => {
message.loading('正在测试邮件设置...', 2.5)
.then(() => message.success('邮件设置测试成功'));
};
// Logo
const beforeUpload = (file) => {
const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
if (!isJpgOrPng) {
message.error('只能上传JPG或PNG格式的图片!');
}
const isLt2M = file.size / 1024 / 1024 < 2;
if (!isLt2M) {
message.error('图片必须小于2MB!');
}
return isJpgOrPng && isLt2M;
};
// Logo
const handleLogoChange = (info) => {
if (info.file.status === 'uploading') {
return;
}
if (info.file.status === 'done') {
// URL
basicForm.logoUrl = info.file.response.url;
message.success('Logo上传成功');
} else if (info.file.status === 'error') {
message.error('Logo上传失败');
}
};
return {
basicForm,
securityForm,
emailForm,
onBasicFormFinish,
onSecurityFormFinish,
onEmailFormFinish,
testEmailSettings,
beforeUpload,
handleLogoChange
};
}
};
</script>
<style scoped>
.settings {
padding: 8px;
}
.logo-uploader {
width: 128px;
height: 128px;
}
</style>

383
src/views/User.vue Normal file
View File

@ -0,0 +1,383 @@
<template>
<div class="user-management">
<a-card title="用户管理" :bordered="false">
<!-- 搜索和操作区域 -->
<div class="table-operations">
<a-row :gutter="16">
<a-col :span="6">
<a-input-search
v-model:value="searchValue"
placeholder="搜索用户名/邮箱"
@search="onSearch"
style="width: 100%"
/>
</a-col>
<a-col :span="18" style="text-align: right">
<a-button type="primary" @click="showModal">
<template #icon><plus-outlined /></template>
添加用户
</a-button>
</a-col>
</a-row>
</div>
<!-- 用户表格 -->
<a-table
:columns="columns"
:data-source="userData"
:pagination="pagination"
:loading="loading"
@change="handleTableChange"
style="margin-top: 16px"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status === 'active' ? '正常' : '禁用' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="editUser(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此用户吗?"
ok-text="确定"
cancel-text="取消"
@confirm="deleteUser(record)"
>
<a>删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
<!-- 添加/编辑用户对话框 -->
<a-modal
v-model:visible="modalVisible"
:title="modalTitle"
@ok="handleModalOk"
@cancel="handleModalCancel"
>
<a-form
:model="formState"
:rules="rules"
ref="formRef"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 16 }"
>
<a-form-item label="用户名" name="username">
<a-input v-model:value="formState.username" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formState.email" />
</a-form-item>
<a-form-item label="手机号" name="phone">
<a-input v-model:value="formState.phone" />
</a-form-item>
<a-form-item label="角色" name="role">
<a-select v-model:value="formState.role">
<a-select-option value="admin">管理员</a-select-option>
<a-select-option value="user">普通用户</a-select-option>
<a-select-option value="guest">访客</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="状态" name="status">
<a-switch
v-model:checked="formState.statusActive"
checked-children="正常"
un-checked-children="禁用"
/>
</a-form-item>
</a-form>
</a-modal>
</a-card>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue';
import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue';
export default {
name: 'UserManagement',
components: {
PlusOutlined
},
setup() {
const searchValue = ref('');
const loading = ref(false);
const modalVisible = ref(false);
const modalTitle = ref('添加用户');
const formRef = ref(null);
const isEdit = ref(false);
const currentUserId = ref(null);
//
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '用户名',
dataIndex: 'username',
key: 'username'
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email'
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone'
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
filters: [
{ text: '管理员', value: 'admin' },
{ text: '普通用户', value: 'user' },
{ text: '访客', value: 'guest' }
]
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '正常', value: 'active' },
{ text: '禁用', value: 'inactive' }
]
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
sorter: true
},
{
title: '操作',
key: 'action',
width: 150
}
];
//
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
showSizeChanger: true,
showTotal: (total) => `${total} 条记录`
});
//
const formState = reactive({
username: '',
email: '',
phone: '',
role: 'user',
statusActive: true
});
//
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间', trigger: 'blur' }
],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
],
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入有效的手机号码', trigger: 'blur' }
],
role: [
{ required: true, message: '请选择角色', trigger: 'change' }
]
};
//
const userData = ref([
{
id: 1,
username: 'admin',
email: 'admin@example.com',
phone: '13800138000',
role: '管理员',
status: 'active',
createTime: '2025-01-01 12:00:00'
},
{
id: 2,
username: 'user1',
email: 'user1@example.com',
phone: '13800138001',
role: '普通用户',
status: 'active',
createTime: '2025-01-02 12:00:00'
},
{
id: 3,
username: 'user2',
email: 'user2@example.com',
phone: '13800138002',
role: '普通用户',
status: 'inactive',
createTime: '2025-01-03 12:00:00'
},
{
id: 4,
username: 'guest',
email: 'guest@example.com',
phone: '13800138003',
role: '访客',
status: 'active',
createTime: '2025-01-04 12:00:00'
}
]);
//
const loadUserData = () => {
loading.value = true;
// API
setTimeout(() => {
pagination.total = userData.value.length;
loading.value = false;
}, 500);
};
//
const onSearch = (value) => {
console.log('搜索:', value);
// API
loadUserData();
};
//
const handleTableChange = (pag, filters, sorter) => {
console.log('表格变化:', pag, filters, sorter);
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
// API
loadUserData();
};
//
const showModal = () => {
resetForm();
isEdit.value = false;
modalTitle.value = '添加用户';
modalVisible.value = true;
};
//
const editUser = (record) => {
isEdit.value = true;
currentUserId.value = record.id;
modalTitle.value = '编辑用户';
//
formState.username = record.username;
formState.email = record.email;
formState.phone = record.phone;
formState.role = record.role === '管理员' ? 'admin' : record.role === '普通用户' ? 'user' : 'guest';
formState.statusActive = record.status === 'active';
modalVisible.value = true;
};
//
const deleteUser = (record) => {
console.log('删除用户:', record);
// API
message.success(`用户 ${record.username} 已删除`);
loadUserData();
};
//
const handleModalOk = () => {
formRef.value.validate().then(() => {
//
const userData = {
...formState,
status: formState.statusActive ? 'active' : 'inactive'
};
if (isEdit.value) {
console.log('更新用户:', userData);
message.success('用户信息已更新');
} else {
console.log('添加用户:', userData);
message.success('用户已添加');
}
modalVisible.value = false;
loadUserData();
}).catch(error => {
console.log('表单验证失败:', error);
});
};
//
const handleModalCancel = () => {
modalVisible.value = false;
};
//
const resetForm = () => {
formState.username = '';
formState.email = '';
formState.phone = '';
formState.role = 'user';
formState.statusActive = true;
if (formRef.value) {
formRef.value.resetFields();
}
};
onMounted(() => {
loadUserData();
});
return {
searchValue,
loading,
columns,
userData,
pagination,
modalVisible,
modalTitle,
formState,
rules,
formRef,
onSearch,
handleTableChange,
showModal,
editUser,
deleteUser,
handleModalOk,
handleModalCancel
};
}
};
</script>
<style scoped>
.user-management {
padding: 8px;
}
.table-operations {
margin-bottom: 16px;
}
</style>

54
webpack.config.js Normal file
View File

@ -0,0 +1,54 @@
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset/resource'
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
})
],
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
historyApiFallback: true,
port: 8080,
hot: true
}
};