Compare commits
39
Commits
eec987041c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de8035a829 | ||
|
|
a0cdd2b851 | ||
|
|
1f5f4d16e6 | ||
|
|
cffc718284 | ||
|
|
b727bae790 | ||
|
|
488b50c28c | ||
|
|
76f90f50c0 | ||
|
|
c4b24bfc05 | ||
|
|
90b290af1f | ||
|
|
f9db706769 | ||
|
|
0ad1dc31a1 | ||
|
|
e3ab9c05bd | ||
|
|
9ddcdf71b9 | ||
|
|
db585c5a32 | ||
|
|
70e581b288 | ||
|
|
b904c5619d | ||
|
|
66e747dd8c | ||
|
|
183ade379b | ||
|
|
a5a02eb8c9 | ||
|
|
108ec50978 | ||
|
|
b8761ad3e4 | ||
|
|
e291e2c48f | ||
|
|
07d3614d9c | ||
|
|
f8437d15b4 | ||
|
|
ef5eb809bc | ||
|
|
6686cee4d9 | ||
|
|
0b268c65d6 | ||
|
|
86ea16ec4b | ||
|
|
4cbb93b19a | ||
|
|
9669766eb3 | ||
|
|
6c904ef3cd | ||
|
|
186315d82c | ||
|
|
c2921b9cc0 | ||
|
|
db23de308f | ||
|
|
578e412473 | ||
|
|
32e7f2b21d | ||
|
|
5ffeccad1d | ||
|
|
a4339a1cf5 | ||
|
|
858f7500bd |
@@ -1,3 +0,0 @@
|
||||
# 开发环境接口
|
||||
# VITE_API_BASE_URL='https://backend.yunzer.cn'
|
||||
VITE_API_BASE_URL = 'http://localhost:8000'
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
# 生产环境接口
|
||||
VITE_API_BASE_URL='https://backend.yunzer.cn'
|
||||
VITE_API_BASE_URL='https://api.yunzer.cn'
|
||||
|
||||
Vendored
+2
-1
@@ -6,5 +6,6 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
const ElMessage: typeof import('element-plus/es').ElMessage
|
||||
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -21,6 +21,7 @@ declare module 'vue' {
|
||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
@@ -47,7 +48,6 @@ declare module 'vue' {
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
ElOption: typeof import('element-plus/es')['ElOption']
|
||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||
@@ -55,7 +55,6 @@ declare module 'vue' {
|
||||
ElRow: typeof import('element-plus/es')['ElRow']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||
ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
|
||||
@@ -5,6 +5,9 @@ import { onMounted } from 'vue';
|
||||
// 使用 auth store 获取用户信息
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 获取租户ID
|
||||
const tenantId = (authStore.user as any)?.tid;
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = authStore.user;
|
||||
if (userInfo && userInfo.id) {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/svgs/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||
<meta http-equiv="Pragma" content="no-cache">
|
||||
|
||||
Generated
+590
-1353
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -22,6 +22,7 @@
|
||||
"os": "^0.1.2",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.22",
|
||||
"vue-img-cutter": "^3.0.7",
|
||||
"vue-router": "^4.6.3",
|
||||
"vue3-pdf-app": "^1.0.3",
|
||||
"xlsx": "^0.18.5"
|
||||
@@ -29,7 +30,6 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.10.7",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"sass-embedded": "^1.93.3",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^30.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ import request from "@/utils/request";
|
||||
// 获取内容统计
|
||||
export function getContentStats() {
|
||||
return request({
|
||||
url: "/admin/contentstats",
|
||||
url: "/backend/contentstats",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export function getContentStats() {
|
||||
// 获取用户统计
|
||||
export function getUserStats() {
|
||||
return request({
|
||||
url: "/admin/usersstats",
|
||||
url: "/backend/usersstats",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
+19
-19
@@ -4,7 +4,7 @@ import request from "@/utils/request";
|
||||
// 获取文章列表
|
||||
export function listArticles(params) {
|
||||
return request({
|
||||
url: `/admin/articlesList`,
|
||||
url: `/backend/articlesList`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
@@ -13,7 +13,7 @@ export function listArticles(params) {
|
||||
// 获取文章所有文章
|
||||
export function listAllArticles(params) {
|
||||
return request({
|
||||
url: `/admin/allarticles`,
|
||||
url: `/backend/allarticles`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
@@ -22,7 +22,7 @@ export function listAllArticles(params) {
|
||||
// 获取文章详情
|
||||
export function getArticle(id) {
|
||||
return request({
|
||||
url: `/admin/articles/${id}`,
|
||||
url: `/backend/articles/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export function getArticle(id) {
|
||||
// 创建文章
|
||||
export function createArticle(data) {
|
||||
return request({
|
||||
url: '/admin/createarticle',
|
||||
url: '/backend/createarticle',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -39,7 +39,7 @@ export function createArticle(data) {
|
||||
// 编辑文章
|
||||
export function editArticle(id, data) {
|
||||
return request({
|
||||
url: `/admin/editarticle/${id}`,
|
||||
url: `/backend/editarticle/${id}`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -48,7 +48,7 @@ export function editArticle(id, data) {
|
||||
// 删除文章
|
||||
export function deleteArticle(id) {
|
||||
return request({
|
||||
url: `/admin/deletearticle/${id}`,
|
||||
url: `/backend/deletearticle/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export function deleteArticle(id) {
|
||||
// 发布文章
|
||||
export function publishArticle(id,uid) {
|
||||
return request({
|
||||
url: `/admin/publisharticle/${id}`,
|
||||
url: `/backend/publisharticle/${id}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
uid
|
||||
@@ -67,7 +67,7 @@ export function publishArticle(id,uid) {
|
||||
// 下架文章
|
||||
export function unPublishArticle(id) {
|
||||
return request({
|
||||
url: `/admin/unPublisharticle/${id}`,
|
||||
url: `/backend/unPublisharticle/${id}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export function unPublishArticle(id) {
|
||||
// 文章推荐
|
||||
export function articleRecommend(id) {
|
||||
return request({
|
||||
url: `/admin/articleRecommend/${id}`,
|
||||
url: `/backend/articleRecommend/${id}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
@@ -83,7 +83,7 @@ export function articleRecommend(id) {
|
||||
// 取消文章推荐
|
||||
export function unArticleRecommend(id) {
|
||||
return request({
|
||||
url: `/admin/unArticleRecommend/${id}`,
|
||||
url: `/backend/unArticleRecommend/${id}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export function unArticleRecommend(id) {
|
||||
// 文章置顶
|
||||
export function articleTop(id) {
|
||||
return request({
|
||||
url: `/admin/articleTop/${id}`,
|
||||
url: `/backend/articleTop/${id}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export function articleTop(id) {
|
||||
// 取消文章置顶
|
||||
export function unArticleTop(id) {
|
||||
return request({
|
||||
url: `/admin/unArticleTop/${id}`,
|
||||
url: `/backend/unArticleTop/${id}`,
|
||||
method: 'post'
|
||||
});
|
||||
}
|
||||
@@ -111,7 +111,7 @@ export function unArticleTop(id) {
|
||||
// 获取所有分类列表
|
||||
export function allCategories(params) {
|
||||
return request({
|
||||
url: `/admin/allcategories`,
|
||||
url: `/backend/allcategories`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
@@ -120,7 +120,7 @@ export function allCategories(params) {
|
||||
// 获取分类列表
|
||||
export function listCategories(params) {
|
||||
return request({
|
||||
url: `/admin/categories`,
|
||||
url: `/backend/categories`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
@@ -129,7 +129,7 @@ export function listCategories(params) {
|
||||
// 获取分类详情
|
||||
export function getCategory(id) {
|
||||
return request({
|
||||
url: `/admin/categories/${id}`,
|
||||
url: `/backend/categories/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -137,7 +137,7 @@ export function getCategory(id) {
|
||||
// 创建分类
|
||||
export function createCategory(data) {
|
||||
return request({
|
||||
url: `/admin/createCategory`,
|
||||
url: `/backend/createCategory`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
@@ -146,7 +146,7 @@ export function createCategory(data) {
|
||||
// 更新分类
|
||||
export function editCategory(id, data) {
|
||||
return request({
|
||||
url: `/admin/editCategory/${id}`,
|
||||
url: `/backend/editCategory/${id}`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
@@ -155,7 +155,7 @@ export function editCategory(id, data) {
|
||||
// 删除分类
|
||||
export function deleteCategory(id) {
|
||||
return request({
|
||||
url: `/admin/categories/${id}`,
|
||||
url: `/backend/categories/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export function deleteCategory(id) {
|
||||
// 更新分类状态
|
||||
export function updateCategoryStatus(id, status) {
|
||||
return request({
|
||||
url: `/admin/categories/${id}/status`,
|
||||
url: `/backend/categories/${id}/status`,
|
||||
method: "patch",
|
||||
data: { status },
|
||||
});
|
||||
|
||||
+32
-21
@@ -10,8 +10,8 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getBabyList() {
|
||||
return request({
|
||||
url: "/admin/babys/list",
|
||||
method: "get",
|
||||
url: '/backend/babys/list',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function getBabyList() {
|
||||
*/
|
||||
export function getBabyDetail(id) {
|
||||
return request({
|
||||
url: `/admin/babys/${id}`,
|
||||
url: `/backend/babys/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export function getBabyDetail(id) {
|
||||
*/
|
||||
export function createBaby(data) {
|
||||
return request({
|
||||
url: "/admin/babys",
|
||||
url: "/backend/babys",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
@@ -43,17 +43,12 @@ export function createBaby(data) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新宝贝数据
|
||||
* @param {number} id 宝贝ID
|
||||
* @param {Object} data 更新的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
// 更新宝贝信息
|
||||
export function editBaby(id, data) {
|
||||
return request({
|
||||
url: `/admin/babys/${id}`,
|
||||
method: "post",
|
||||
data: { ...data, _method: 'PUT' }
|
||||
url: `/backend/baby/update/${id}`,
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
@@ -64,7 +59,7 @@ export function editBaby(id, data) {
|
||||
*/
|
||||
export function deleteBaby(id) {
|
||||
return request({
|
||||
url: `/admin/babys/${id}`,
|
||||
url: `/backend/babys/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -77,7 +72,7 @@ export function deleteBaby(id) {
|
||||
*/
|
||||
export function bindParent(id, data) {
|
||||
return request({
|
||||
url: `/admin/babys/bindparents/${id}`,
|
||||
url: `/backend/babys/bindparents/${id}`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
@@ -90,7 +85,7 @@ export function bindParent(id, data) {
|
||||
*/
|
||||
export function getParents(id) {
|
||||
return request({
|
||||
url: `/admin/babys/getParents/${id}`,
|
||||
url: `/backend/babys/getParents/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -105,7 +100,7 @@ export function getParents(id) {
|
||||
*/
|
||||
export function getUserList() {
|
||||
return request({
|
||||
url: "/admin/babyhealthUser/list",
|
||||
url: "/backend/babyhealthUser/list",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -117,7 +112,7 @@ export function getUserList() {
|
||||
*/
|
||||
export function getUserDetail(id) {
|
||||
return request({
|
||||
url: `/admin/babyhealthUser/${id}`,
|
||||
url: `/backend/babyhealthUser/detail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -129,7 +124,7 @@ export function getUserDetail(id) {
|
||||
*/
|
||||
export function createUser(data) {
|
||||
return request({
|
||||
url: "/admin/babyhealthUser",
|
||||
url: "/backend/babyhealthUser/create",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
@@ -146,7 +141,7 @@ export function createUser(data) {
|
||||
*/
|
||||
export function updateUser(id, data) {
|
||||
return request({
|
||||
url: `/admin/babyhealthUser/${id}`,
|
||||
url: `/backend/babyhealthUser/update/${id}`,
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
@@ -162,7 +157,23 @@ export function updateUser(id, data) {
|
||||
*/
|
||||
export function deleteUser(id) {
|
||||
return request({
|
||||
url: `/admin/babyhealthUser/${id}`,
|
||||
url: `/backend/babyhealthUser/delete/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/*************************************************
|
||||
****************** 仪表盘相关接口 ******************
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* dashborad总体输出
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getDashborad() {
|
||||
return request({
|
||||
url: "/backend/babyhealthDashborad/dashborad",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
+4
-4
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getBanners() {
|
||||
return request({
|
||||
url: "/admin/allbanners",
|
||||
url: "/backend/allbanners",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function getBanners() {
|
||||
*/
|
||||
export function createBanner(formData) {
|
||||
return request({
|
||||
url: "/admin/createbanner",
|
||||
url: "/backend/createbanner",
|
||||
method: "post",
|
||||
data: formData,
|
||||
headers: {
|
||||
@@ -35,7 +35,7 @@ export function createBanner(formData) {
|
||||
*/
|
||||
export function editBanner(id, bannerData) {
|
||||
return request({
|
||||
url: `/admin/editbanner/${id}`,
|
||||
url: `/backend/editbanner/${id}`,
|
||||
method: "post",
|
||||
data: bannerData,
|
||||
});
|
||||
@@ -48,7 +48,7 @@ export function editBanner(id, bannerData) {
|
||||
*/
|
||||
export function deleteBanner(id) {
|
||||
return request({
|
||||
url: `/admin/deletebanner/${id}`,
|
||||
url: `/backend/deletebanner/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ import request from '@/utils/request'
|
||||
|
||||
export function listContacts(params) {
|
||||
return request({
|
||||
url: '/admin/crm/contact/list',
|
||||
url: '/backend/crm/contact/list',
|
||||
method: 'get',
|
||||
params,
|
||||
})
|
||||
@@ -10,7 +10,7 @@ export function listContacts(params) {
|
||||
|
||||
export function createContact(data) {
|
||||
return request({
|
||||
url: '/admin/crm/contact/add',
|
||||
url: '/backend/crm/contact/add',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
@@ -18,7 +18,7 @@ export function createContact(data) {
|
||||
|
||||
export function updateContact(data) {
|
||||
return request({
|
||||
url: '/admin/crm/contact/edit',
|
||||
url: '/backend/crm/contact/edit',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
@@ -26,7 +26,7 @@ export function updateContact(data) {
|
||||
|
||||
export function deleteContact(data) {
|
||||
return request({
|
||||
url: '/admin/crm/contact/delete',
|
||||
url: '/backend/crm/contact/delete',
|
||||
method: 'post',
|
||||
data,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getPlatformStats() {
|
||||
return request({
|
||||
url: "/admin/dashboard/platform-stats",
|
||||
url: "/backend/dashboard/platform-stats",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function getPlatformStats() {
|
||||
*/
|
||||
export function getTenantStats() {
|
||||
return request({
|
||||
url: "/admin/dashboard/tenant-stats",
|
||||
url: "/backend/dashboard/tenant-stats",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -30,7 +30,7 @@ export function getTenantStats() {
|
||||
*/
|
||||
export function getActivityLogs(pageNum = 1, pageSize = 10) {
|
||||
return request({
|
||||
url: "/admin/dashboard/user-activity-logs",
|
||||
url: "/backend/dashboard/user-activity-logs",
|
||||
method: "get",
|
||||
params: {
|
||||
page_num: pageNum,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取需求列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getDemandList() {
|
||||
return request({
|
||||
url: "/backend/demandList",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增需求
|
||||
* @param {Object} data 需求数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addDemand(data) {
|
||||
return request({
|
||||
url: "/backend/addDemand",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑需求
|
||||
* @param {number} id 需求ID
|
||||
* @param {Object} data 需求数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function editDemand(id, data) {
|
||||
return request({
|
||||
url: `/backend/editDemand/${id}`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除需求
|
||||
* @param {number} id 需求ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteDemand(id) {
|
||||
return request({
|
||||
url: `/backend/deleteDemand/${id}`,
|
||||
method: "post",
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import request from '@/utils/request';
|
||||
// 获取租户下的所有部门
|
||||
export function getTenantDepartments(tenantId) {
|
||||
return request({
|
||||
url: `/admin/departments/tenant/${tenantId}`,
|
||||
url: `/backend/departments/tenant/${tenantId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export function getTenantDepartments(tenantId) {
|
||||
// 获取部门详情
|
||||
export function getDepartmentInfo(departmentId) {
|
||||
return request({
|
||||
url: `/admin/departments/${departmentId}`,
|
||||
url: `/backend/departments/${departmentId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function getDepartmentInfo(departmentId) {
|
||||
// 添加部门
|
||||
export function addDepartment(data) {
|
||||
return request({
|
||||
url: '/admin/departments',
|
||||
url: '/backend/departments',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -28,7 +28,7 @@ export function addDepartment(data) {
|
||||
// 更新部门信息
|
||||
export function editDepartment(departmentId, data) {
|
||||
return request({
|
||||
url: `/admin/departments/${departmentId}`,
|
||||
url: `/backend/departments/${departmentId}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
@@ -37,7 +37,7 @@ export function editDepartment(departmentId, data) {
|
||||
// 删除部门
|
||||
export function deleteDepartment(departmentId) {
|
||||
return request({
|
||||
url: `/admin/departments/${departmentId}`,
|
||||
url: `/backend/departments/${departmentId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
+12
-12
@@ -3,7 +3,7 @@ import request from '@/utils/request'
|
||||
// 获取字典类型列表
|
||||
export function getDictTypes(params) {
|
||||
return request({
|
||||
url: '/admin/dict/types',
|
||||
url: '/backend/dict/types',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
@@ -12,7 +12,7 @@ export function getDictTypes(params) {
|
||||
// 根据ID获取字典类型
|
||||
export function getDictTypeById(id) {
|
||||
return request({
|
||||
url: `/admin/dict/types/${id}`,
|
||||
url: `/backend/dict/types/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export function getDictTypeById(id) {
|
||||
// 添加字典类型
|
||||
export function addDictType(data) {
|
||||
return request({
|
||||
url: '/admin/dict/types',
|
||||
url: '/backend/dict/types',
|
||||
method: 'post',
|
||||
data: {
|
||||
...data,
|
||||
@@ -32,7 +32,7 @@ export function addDictType(data) {
|
||||
// 更新字典类型
|
||||
export function updateDictType(id, data) {
|
||||
return request({
|
||||
url: `/admin/dict/types/${id}`,
|
||||
url: `/backend/dict/types/${id}`,
|
||||
method: 'put',
|
||||
data: {
|
||||
...data,
|
||||
@@ -44,7 +44,7 @@ export function updateDictType(id, data) {
|
||||
// 删除字典类型
|
||||
export function deleteDictType(id) {
|
||||
return request({
|
||||
url: `/admin/dict/types/${id}`,
|
||||
url: `/backend/dict/types/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export function deleteDictType(id) {
|
||||
// 获取字典项列表
|
||||
export function getDictItems(params) {
|
||||
return request({
|
||||
url: '/admin/dict/items',
|
||||
url: '/backend/dict/items',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
@@ -61,7 +61,7 @@ export function getDictItems(params) {
|
||||
// 根据ID获取字典项
|
||||
export function getDictItemById(id) {
|
||||
return request({
|
||||
url: `/admin/dict/items/${id}`,
|
||||
url: `/backend/dict/items/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
@@ -69,7 +69,7 @@ export function getDictItemById(id) {
|
||||
// 添加字典项
|
||||
export function addDictItem(data) {
|
||||
return request({
|
||||
url: '/admin/dict/items',
|
||||
url: '/backend/dict/items',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -78,7 +78,7 @@ export function addDictItem(data) {
|
||||
// 更新字典项
|
||||
export function updateDictItem(id, data) {
|
||||
return request({
|
||||
url: `/admin/dict/items/${id}`,
|
||||
url: `/backend/dict/items/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
@@ -87,7 +87,7 @@ export function updateDictItem(id, data) {
|
||||
// 删除字典项
|
||||
export function deleteDictItem(id) {
|
||||
return request({
|
||||
url: `/admin/dict/items/${id}`,
|
||||
url: `/backend/dict/items/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -95,7 +95,7 @@ export function deleteDictItem(id) {
|
||||
// 根据字典编码获取字典项(用于业务查询)
|
||||
export function getDictItemsByCode(code, includeDisabled = false) {
|
||||
return request({
|
||||
url: `/admin/dict/items/code/${code}`,
|
||||
url: `/backend/dict/items/code/${code}`,
|
||||
method: 'get',
|
||||
params: {
|
||||
include_disabled: includeDisabled ? '1' : '0'
|
||||
@@ -106,7 +106,7 @@ export function getDictItemsByCode(code, includeDisabled = false) {
|
||||
// 批量更新字典项排序
|
||||
export function batchUpdateDictItemSort(data) {
|
||||
return request({
|
||||
url: '/admin/dict/items/sort',
|
||||
url: '/backend/dict/items/sort',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// ==================== 主域名池管理 ====================
|
||||
|
||||
// 获取域名池列表
|
||||
export function getDomainPoolList(params) {
|
||||
return request({
|
||||
url: '/backend/domain/pool/index',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取启用的主域名列表
|
||||
export function getEnabledDomains() {
|
||||
return request({
|
||||
url: '/backend/domain/pool/getEnabledDomains',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 创建主域名
|
||||
export function createDomainPool(data) {
|
||||
return request({
|
||||
url: '/backend/domain/pool/create',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 更新主域名
|
||||
export function updateDomainPool(data) {
|
||||
return request({
|
||||
url: '/backend/domain/pool/update',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 删除主域名
|
||||
export function deleteDomainPool(id) {
|
||||
return request({
|
||||
url: `/backend/domain/pool/delete/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
// 切换主域名状态
|
||||
export function toggleDomainPoolStatus(id) {
|
||||
return request({
|
||||
url: '/backend/domain/pool/toggleStatus',
|
||||
method: 'post',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 租户域名管理 ====================
|
||||
|
||||
// 获取租户域名列表(管理员)
|
||||
export function getTenantDomainList(params) {
|
||||
return request({
|
||||
url: '/backend/domain/tenant/index',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 获取当前租户的域名列表
|
||||
export function getMyDomains(params) {
|
||||
return request({
|
||||
url: '/backend/domain/tenant/myDomains',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 申请二级域名
|
||||
export function applyTenantDomain(data) {
|
||||
return request({
|
||||
url: '/backend/domain/tenant/apply',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 审核租户域名
|
||||
export function auditTenantDomain(data) {
|
||||
return request({
|
||||
url: '/backend/domain/tenant/audit',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 禁用/启用租户域名
|
||||
export function toggleTenantDomainStatus(id) {
|
||||
return request({
|
||||
url: '/backend/domain/tenant/toggleStatus',
|
||||
method: 'post',
|
||||
data: { id }
|
||||
})
|
||||
}
|
||||
|
||||
// 删除租户域名
|
||||
export function deleteTenantDomain(id) {
|
||||
return request({
|
||||
url: `/backend/domain/tenant/delete/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取邮箱信息
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getEmailInfo() {
|
||||
return request({
|
||||
url: "/backend/email/info",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑邮箱信息
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function editEmailInfo(data) {
|
||||
return request({
|
||||
url: "/backend/email/editinfo",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试邮件
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function sendTestEmail(data) {
|
||||
return request({
|
||||
url: "/backend/email/sendtestemail",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/*************************************************
|
||||
****************** 组织机构相关接口 ******************
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* 获取组织机构列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getOrganizationList() {
|
||||
return request({
|
||||
url: '/backend/erp/getOrganization',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组织机构详情
|
||||
* @param {number} id 组织机构ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getOrganizationDetail(id) {
|
||||
return request({
|
||||
url: `/backend/erp/getOrganizationDetail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建组织机构数据
|
||||
* @param {Object} data 组织机构数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createOrganization(data) {
|
||||
return request({
|
||||
url: "/backend/erp/createOrganization",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新组织机构信息
|
||||
export function editOrganization(id, data) {
|
||||
return request({
|
||||
url: `/backend/erp/editOrganization/${id}`,
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除组织机构数据
|
||||
* @param {number} id 组织机构ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteOrganization(id) {
|
||||
return request({
|
||||
url: `/backend/erp/deleteOrganization/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业单位列表
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getCompanys() {
|
||||
return request({
|
||||
url: '/backend/erp/getCompanys',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门列表
|
||||
* @param {number} parentId 隶属单位ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getDepartments(parentId) {
|
||||
return request({
|
||||
url: '/backend/erp/getDepartments',
|
||||
method: 'get',
|
||||
params: parentId ? { parent_id: parentId } : {}
|
||||
});
|
||||
}
|
||||
|
||||
/*************************************************
|
||||
****************** 员工相关接口 ******************
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* 获取员工列表
|
||||
* @param {number} tenantId 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getEmployeeList(tenantId) {
|
||||
return request({
|
||||
url: '/backend/erp/getEmployee',
|
||||
method: 'get',
|
||||
params: { tid: tenantId }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取员工详情
|
||||
* @param {number} id 员工ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getEmployeeDetail(id) {
|
||||
return request({
|
||||
url: `/backend/erp/getEmployeeDetail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建员工数据
|
||||
* @param {Object} data 员工数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createEmployee(data) {
|
||||
return request({
|
||||
url: "/backend/erp/createEmployee",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新员工信息
|
||||
export function editEmployee(id, data) {
|
||||
return request({
|
||||
url: `/backend/erp/editEmployee/${id}`,
|
||||
method: 'post',
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除员工数据
|
||||
* @param {number} id 员工ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteEmployee(id) {
|
||||
return request({
|
||||
url: `/backend/erp/deleteEmployee/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
+47
-31
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getUserCate() {
|
||||
return request({
|
||||
url: `/admin/usercate`,
|
||||
url: `/backend/usercate`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function getUserCate() {
|
||||
*/
|
||||
export function getAllFiles() {
|
||||
return request({
|
||||
url: "/admin/allfiles",
|
||||
url: "/backend/allfiles",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function getAllFiles() {
|
||||
*/
|
||||
export function createFileCate(data) {
|
||||
return request({
|
||||
url: "/admin/createfilecate",
|
||||
url: "/backend/createfilecate",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
@@ -43,7 +43,7 @@ export function createFileCate(data) {
|
||||
*/
|
||||
export function renameFileCate(id, data) {
|
||||
return request({
|
||||
url: `/admin/renamefilecate/${id}`,
|
||||
url: `/backend/renamefilecate/${id}`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
@@ -56,7 +56,7 @@ export function renameFileCate(id, data) {
|
||||
*/
|
||||
export function deleteFileCate(id) {
|
||||
return request({
|
||||
url: `/admin/deletefilecate/${id}`,
|
||||
url: `/backend/deletefilecate/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
|
||||
params.keyword = keyword;
|
||||
}
|
||||
return request({
|
||||
url: `/admin/catefiles/${id}`,
|
||||
url: `/backend/catefiles/${id}`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
@@ -91,7 +91,7 @@ export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
|
||||
*/
|
||||
export function getFileById(id) {
|
||||
return request({
|
||||
url: `/admin/catefiles`,
|
||||
url: `/backend/catefiles`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -109,7 +109,7 @@ export function uploadFile(formData, options = {}) {
|
||||
}
|
||||
|
||||
return request({
|
||||
url: "/admin/uploadfile",
|
||||
url: "/backend/uploadfile",
|
||||
method: "post",
|
||||
data: formData,
|
||||
headers: {
|
||||
@@ -126,7 +126,7 @@ export function uploadFile(formData, options = {}) {
|
||||
*/
|
||||
export function updateFile(id, fileData) {
|
||||
return request({
|
||||
url: `/admin/updatefile/${id}`,
|
||||
url: `/backend/updatefile/${id}`,
|
||||
method: "post",
|
||||
data: fileData,
|
||||
});
|
||||
@@ -139,7 +139,19 @@ export function updateFile(id, fileData) {
|
||||
*/
|
||||
export function deleteFile(id) {
|
||||
return request({
|
||||
url: `/admin/deletefile/${id}`,
|
||||
url: `/backend/deletefile/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param {number|string} id 文件ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteFilePermanently(id) {
|
||||
return request({
|
||||
url: `/backend/deleteFilePermanently/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -152,44 +164,48 @@ export function deleteFile(id) {
|
||||
*/
|
||||
export function moveFile(id, cate) {
|
||||
return request({
|
||||
url: `/admin/movefile/${id}`,
|
||||
url: `/backend/movefile/${id}`,
|
||||
method: "get",
|
||||
params: { cate },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传头像
|
||||
* @param {FormData} formData 文件数据
|
||||
* @param {Object} options 额外选项
|
||||
* @param {string} [options.cate]
|
||||
* 批量删除文件
|
||||
* @param {Array} ids 文件ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function uploadAvatar(formData, options = {}) {
|
||||
if (options.cate) {
|
||||
formData.append('cate', options.cate);
|
||||
}
|
||||
|
||||
export function batchDeleteFiles(ids) {
|
||||
return request({
|
||||
url: "/admin/uploadavatar",
|
||||
url: "/backend/batchDeleteFiles",
|
||||
method: "post",
|
||||
data: formData,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data"
|
||||
}
|
||||
data: { ids },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新头像
|
||||
* @param {number|string} id 文件ID
|
||||
* @param {Object} fileData 更新头像
|
||||
* 批量彻底删除文件
|
||||
* @param {Array} ids 文件ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateAvatar(id, fileData) {
|
||||
export function batchDeleteFilesPermanently(ids) {
|
||||
return request({
|
||||
url: `/admin/uploadavatar/${id}`,
|
||||
url: "/backend/batchDeleteFilesPermanently",
|
||||
method: "post",
|
||||
data: fileData,
|
||||
data: { ids },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量移动文件
|
||||
* @param {Array} ids 文件ID数组
|
||||
* @param {number} cate 目标分类ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function batchMoveFiles(ids, cate) {
|
||||
return request({
|
||||
url: "/backend/batchMoveFiles",
|
||||
method: "post",
|
||||
data: { ids, cate },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取友情链接列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getFriendlinkList(params) {
|
||||
return request({
|
||||
url: '/backend/friendlinks',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有友情链接(下拉选择用)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getAllFriendlinks() {
|
||||
return request({
|
||||
url: '/backend/friendlinks/all',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加友情链接
|
||||
* @param {Object} data - 链接数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addFriendlink(data) {
|
||||
return request({
|
||||
url: '/backend/friendlinks',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新友情链接
|
||||
* @param {number} id - 链接ID
|
||||
* @param {Object} data - 链接数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateFriendlink(id, data) {
|
||||
return request({
|
||||
url: `/backend/friendlinks/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除友情链接
|
||||
* @param {number} id - 链接ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteFriendlink(id) {
|
||||
return request({
|
||||
url: `/backend/friendlinks/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除友情链接
|
||||
* @param {Array} ids - 链接ID数组
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function batchDeleteFriendlinks(ids) {
|
||||
return request({
|
||||
url: '/backend/friendlinks/batchdelete',
|
||||
method: 'post',
|
||||
data: { ids }
|
||||
})
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getFrontMenus() {
|
||||
return request({
|
||||
url: "/admin/frontmenus",
|
||||
url: "/backend/frontmenus",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function getFrontMenus() {
|
||||
*/
|
||||
export function createFrontMenu(formData, options = {}) {
|
||||
return request({
|
||||
url: "/admin/createfrontmenu",
|
||||
url: "/backend/createfrontmenu",
|
||||
method: "post",
|
||||
data: formData,
|
||||
headers: {
|
||||
@@ -35,7 +35,7 @@ export function createFrontMenu(formData, options = {}) {
|
||||
*/
|
||||
export function editFrontMenu(id, frontMenuData) {
|
||||
return request({
|
||||
url: `/admin/editfrontmenu/${id}`,
|
||||
url: `/backend/editfrontmenu/${id}`,
|
||||
method: "post",
|
||||
data: frontMenuData,
|
||||
});
|
||||
@@ -48,7 +48,7 @@ export function editFrontMenu(id, frontMenuData) {
|
||||
*/
|
||||
export function deleteFrontMenu(id) {
|
||||
return request({
|
||||
url: `/admin/deletefrontmenu/${id}`,
|
||||
url: `/backend/deletefrontmenu/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
+91
-7
@@ -1,14 +1,29 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
// 登录(使用租户名称)
|
||||
export function login(account, password) {
|
||||
export function login(data) {
|
||||
return request({
|
||||
url: `/admin/login`,
|
||||
url: `/backend/login`,
|
||||
method: "post",
|
||||
data: {
|
||||
account: account,
|
||||
password: password,
|
||||
},
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送登录验证码(手机号)
|
||||
export function sendLoginCode(data) {
|
||||
return request({
|
||||
url: "/backend/sendLoginCode",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 手机号验证码登录
|
||||
export function loginBySms(data) {
|
||||
return request({
|
||||
url: "/backend/loginBySms",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
// 登出
|
||||
@@ -26,8 +41,77 @@ export function logout(userInfo = null) {
|
||||
}
|
||||
|
||||
return request({
|
||||
url: `/admin/logout`,
|
||||
url: `/backend/logout`,
|
||||
method: "post",
|
||||
data: userInfo ? { userInfo: userInfo } : {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取极验3.0数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getGeetest3Infos() {
|
||||
return request({
|
||||
url: '/backend/login/getGeetest3Infos',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取极验4.0数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getGeetest4Infos() {
|
||||
return request({
|
||||
url: '/backend/login/getGeetest4Infos',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否开启验证
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getOpenVerify() {
|
||||
return request({
|
||||
url: '/backend/login/getOpenVerify',
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
// 注册
|
||||
export function register(data) {
|
||||
return request({
|
||||
url: "/backend/register",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送注册验证码
|
||||
export function sendRegisterCode(data) {
|
||||
return request({
|
||||
url: "/backend/sendRegisterCode",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 忘记密码重置
|
||||
export function resetPassword(data) {
|
||||
return request({
|
||||
url: "/backend/resetPassword",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送找回密码验证码
|
||||
export function sendResetCode(data) {
|
||||
return request({
|
||||
url: "/backend/sendResetCode",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
+6
-6
@@ -3,7 +3,7 @@ import request from "@/utils/request";
|
||||
// 获取所有菜单
|
||||
export function getAllMenus() {
|
||||
return request({
|
||||
url: `/admin/allmenu`,
|
||||
url: `/backend/allmenu`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export function getAllMenus() {
|
||||
//获取用户菜单
|
||||
export function getMenus(id){
|
||||
return request({
|
||||
url: `/admin/menu/${parseInt(id)}`,
|
||||
url: `/backend/menu/${parseInt(id)}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function getMenus(id){
|
||||
// 更新菜单状态
|
||||
export function updateMenuStatus(menuId, status) {
|
||||
return request({
|
||||
url: `/admin/menu/status/${menuId}`,
|
||||
url: `/backend/menu/status/${menuId}`,
|
||||
method: "patch",
|
||||
data: { status },
|
||||
});
|
||||
@@ -28,7 +28,7 @@ export function updateMenuStatus(menuId, status) {
|
||||
// 创建菜单
|
||||
export function createMenu(menuData) {
|
||||
return request({
|
||||
url: `/admin/createmenu`,
|
||||
url: `/backend/createmenu`,
|
||||
method: "post",
|
||||
data: menuData,
|
||||
});
|
||||
@@ -37,7 +37,7 @@ export function createMenu(menuData) {
|
||||
// 更新菜单
|
||||
export function updateMenu(menuId, menuData) {
|
||||
return request({
|
||||
url: `/admin/updatemenu/${menuId}`,
|
||||
url: `/backend/updatemenu/${menuId}`,
|
||||
method: "put",
|
||||
data: menuData,
|
||||
});
|
||||
@@ -46,7 +46,7 @@ export function updateMenu(menuId, menuData) {
|
||||
// 删除菜单
|
||||
export function deleteMenu(menuId) {
|
||||
return request({
|
||||
url: `/admin/deletemenu/${menuId}`,
|
||||
url: `/backend/deletemenu/${menuId}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getModuleCategory() {
|
||||
return request({
|
||||
url: "/admin/moduleCategory",
|
||||
url: "/backend/moduleCategory",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function getModuleCategory() {
|
||||
*/
|
||||
export function getModules(cid) {
|
||||
return request({
|
||||
url: "/admin/moduleCenter/modules",
|
||||
url: "/backend/moduleCenter/modules",
|
||||
method: "get",
|
||||
params: { cid }
|
||||
});
|
||||
@@ -34,7 +34,7 @@ export function getModules(cid) {
|
||||
*/
|
||||
export function editModuleCategory(data) {
|
||||
return request({
|
||||
url: "/admin/moduleCenter/editCategory",
|
||||
url: "/backend/moduleCenter/editCategory",
|
||||
method: "post",
|
||||
data
|
||||
});
|
||||
@@ -50,7 +50,7 @@ export function editModuleCategory(data) {
|
||||
*/
|
||||
export function editModules(data) {
|
||||
return request({
|
||||
url: "/admin/moduleCenter/editModules",
|
||||
url: "/backend/moduleCenter/editModules",
|
||||
method: "post",
|
||||
data
|
||||
});
|
||||
|
||||
+15
-8
@@ -2,21 +2,28 @@ import request from '@/utils/request';
|
||||
|
||||
export function getModulesList() {
|
||||
return request({
|
||||
url: '/admin/modules/list',
|
||||
url: '/backend/modules/list',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function getTenantList() {
|
||||
return request({
|
||||
url: '/backend/modules/getTenantList',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function getModuleDetail(id) {
|
||||
return request({
|
||||
url: `/admin/modules/${id}`,
|
||||
url: `/backend/modules/${id}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
export function addModule(data) {
|
||||
return request({
|
||||
url: '/admin/modules',
|
||||
url: '/backend/modules',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -24,7 +31,7 @@ export function addModule(data) {
|
||||
|
||||
export function editModule(id, data) {
|
||||
return request({
|
||||
url: `/admin/modules/${id}`,
|
||||
url: `/backend/modules/${id}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
@@ -32,14 +39,14 @@ export function editModule(id, data) {
|
||||
|
||||
export function deleteModule(id) {
|
||||
return request({
|
||||
url: `/admin/modules/${id}`,
|
||||
url: `/backend/modules/${id}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
export function batchDeleteModules(ids) {
|
||||
return request({
|
||||
url: '/admin/modules/batchDelete',
|
||||
url: '/backend/modules/batchDelete',
|
||||
method: 'post',
|
||||
data: { ids },
|
||||
});
|
||||
@@ -47,7 +54,7 @@ export function batchDeleteModules(ids) {
|
||||
|
||||
export function changeModuleStatus(id, status) {
|
||||
return request({
|
||||
url: '/admin/modules/status',
|
||||
url: '/backend/modules/status',
|
||||
method: 'post',
|
||||
data: { id, status },
|
||||
});
|
||||
@@ -55,7 +62,7 @@ export function changeModuleStatus(id, status) {
|
||||
|
||||
export function getModulesSelectList() {
|
||||
return request({
|
||||
url: '/admin/modules/select/list',
|
||||
url: '/backend/modules/select/list',
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
|
||||
+4
-4
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getOnePages() {
|
||||
return request({
|
||||
url: "/admin/allonepages",
|
||||
url: "/backend/allonepages",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -18,7 +18,7 @@ export function getOnePages() {
|
||||
*/
|
||||
export function createOnePage(formData) {
|
||||
return request({
|
||||
url: "/admin/createonepage",
|
||||
url: "/backend/createonepage",
|
||||
method: "post",
|
||||
data: formData,
|
||||
});
|
||||
@@ -32,7 +32,7 @@ export function createOnePage(formData) {
|
||||
*/
|
||||
export function editOnePage(id, onePageData) {
|
||||
return request({
|
||||
url: `/admin/editonepage/${id}`,
|
||||
url: `/backend/editonepage/${id}`,
|
||||
method: "post",
|
||||
data: onePageData,
|
||||
});
|
||||
@@ -45,7 +45,7 @@ export function editOnePage(id, onePageData) {
|
||||
*/
|
||||
export function deleteOnePage(id) {
|
||||
return request({
|
||||
url: `/admin/deleteonepage/${id}`,
|
||||
url: `/backend/deleteonepage/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import request from "@/utils/request";
|
||||
*/
|
||||
export function getOperationLogs(params) {
|
||||
return request({
|
||||
url: "/admin/operationLogs",
|
||||
url: "/backend/operationLogs",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
@@ -28,7 +28,7 @@ export function getOperationLogs(params) {
|
||||
*/
|
||||
export function getOperationLogDetail(id) {
|
||||
return request({
|
||||
url: `/admin/operationLogs/${id}`,
|
||||
url: `/backend/operationLogs/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export function getOperationLogDetail(id) {
|
||||
*/
|
||||
export function deleteOperationLog(id) {
|
||||
return request({
|
||||
url: `/admin/operationLogs/${id}`,
|
||||
url: `/backend/operationLogs/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export function deleteOperationLog(id) {
|
||||
*/
|
||||
export function batchDeleteOperationLogs(ids) {
|
||||
return request({
|
||||
url: "/admin/operationLogs/batchDelete",
|
||||
url: "/backend/operationLogs/batchDelete",
|
||||
method: "post",
|
||||
data: { ids },
|
||||
});
|
||||
@@ -64,7 +64,7 @@ export function batchDeleteOperationLogs(ids) {
|
||||
*/
|
||||
export function getOperationStatistics() {
|
||||
return request({
|
||||
url: "/admin/operationLogs/statistics",
|
||||
url: "/backend/operationLogs/statistics",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import request from '@/utils/request';
|
||||
|
||||
export function getAllMenuPermissions(params = {}) {
|
||||
return request({
|
||||
url: '/admin/allmenupermissions',
|
||||
url: '/backend/allmenupermissions',
|
||||
method: 'get',
|
||||
params
|
||||
});
|
||||
@@ -10,14 +10,14 @@ export function getAllMenuPermissions(params = {}) {
|
||||
|
||||
export function getRolePermissions(roleId) {
|
||||
return request({
|
||||
url: `/admin/rolepermissions/${roleId}`,
|
||||
url: `/backend/rolepermissions/${roleId}`,
|
||||
method: 'get'
|
||||
});
|
||||
}
|
||||
|
||||
export function assignRolePermissions(roleId, permissions) {
|
||||
return request({
|
||||
url: `/admin/assignrolepermissions/${roleId}`,
|
||||
url: `/backend/assignrolepermissions/${roleId}`,
|
||||
method: 'post',
|
||||
data: { permissions }
|
||||
});
|
||||
|
||||
+6
-6
@@ -3,7 +3,7 @@ import request from '@/utils/request';
|
||||
// 获取租户下的所有职位
|
||||
export function getTenantPositions(tenantId) {
|
||||
return request({
|
||||
url: `/admin/positions/tenant/${tenantId}`,
|
||||
url: `/backend/positions/tenant/${tenantId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export function getTenantPositions(tenantId) {
|
||||
// 根据部门ID获取职位列表
|
||||
export function getPositionsByDepartment(departmentId) {
|
||||
return request({
|
||||
url: `/admin/positions/department/${departmentId}`,
|
||||
url: `/backend/positions/department/${departmentId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -19,7 +19,7 @@ export function getPositionsByDepartment(departmentId) {
|
||||
// 获取职位详情
|
||||
export function getPositionInfo(positionId) {
|
||||
return request({
|
||||
url: `/admin/positions/${positionId}`,
|
||||
url: `/backend/positions/${positionId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export function getPositionInfo(positionId) {
|
||||
// 添加职位
|
||||
export function addPosition(data) {
|
||||
return request({
|
||||
url: '/admin/positions',
|
||||
url: '/backend/positions',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -36,7 +36,7 @@ export function addPosition(data) {
|
||||
// 更新职位信息
|
||||
export function editPosition(positionId, data) {
|
||||
return request({
|
||||
url: `/admin/positions/${positionId}`,
|
||||
url: `/backend/positions/${positionId}`,
|
||||
method: 'put',
|
||||
data,
|
||||
});
|
||||
@@ -45,7 +45,7 @@ export function editPosition(positionId, data) {
|
||||
// 删除职位
|
||||
export function deletePosition(positionId) {
|
||||
return request({
|
||||
url: `/admin/positions/${positionId}`,
|
||||
url: `/backend/positions/${positionId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取特色产品列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getProductsList(params) {
|
||||
return request({
|
||||
url: '/backend/productsList',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加特色产品
|
||||
* @param {Object} data - 产品数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addProducts(data) {
|
||||
return request({
|
||||
url: '/backend/addProducts',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新特色产品
|
||||
* @param {number} id - 产品ID
|
||||
* @param {Object} data - 产品数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateProducts(id, data) {
|
||||
return request({
|
||||
url: `/backend/editProducts/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除特色产品
|
||||
* @param {number} id - 产品ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteProducts(id) {
|
||||
return request({
|
||||
url: `/backend/deleteProducts/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品分类列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getProductsTypesList(params) {
|
||||
return request({
|
||||
url: '/backend/productsTypesList',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加产品分类
|
||||
* @param {Object} data - 分类数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addProductsTypes(data) {
|
||||
return request({
|
||||
url: '/backend/addProductsTypes',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新产品分类
|
||||
* @param {number} id - 分类ID
|
||||
* @param {Object} data - 分类数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateProductsTypes(id, data) {
|
||||
return request({
|
||||
url: `/backend/editProductsTypes/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除产品分类
|
||||
* @param {number} id - 分类ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteProductsTypes(id) {
|
||||
return request({
|
||||
url: `/backend/deleteProductsTypes/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
+5
-5
@@ -2,21 +2,21 @@ import request from '@/utils/request'
|
||||
|
||||
export function getAllRoles() {
|
||||
return request({
|
||||
url: '/admin/allRoles',
|
||||
url: '/backend/allRoles',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getRoleById(id) {
|
||||
return request({
|
||||
url: `/admin/roles/${id}`,
|
||||
url: `/backend/roles/${id}`,
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function createRole(data) {
|
||||
return request({
|
||||
url: '/admin/roles',
|
||||
url: '/backend/roles',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
@@ -24,7 +24,7 @@ export function createRole(data) {
|
||||
|
||||
export function updateRole(id, data) {
|
||||
return request({
|
||||
url: `/admin/roles/${id}`,
|
||||
url: `/backend/roles/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
@@ -32,7 +32,7 @@ export function updateRole(id, data) {
|
||||
|
||||
export function deleteRole(id) {
|
||||
return request({
|
||||
url: `/admin/roles/${id}`,
|
||||
url: `/backend/roles/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 获取特色服务列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getServiceList(params) {
|
||||
return request({
|
||||
url: '/backend/servicesList',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加特色服务
|
||||
* @param {Object} data - 服务数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function addService(data) {
|
||||
return request({
|
||||
url: '/backend/addServices',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新特色服务
|
||||
* @param {number} id - 服务ID
|
||||
* @param {Object} data - 服务数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function updateService(id, data) {
|
||||
return request({
|
||||
url: `/backend/editServices/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除特色服务
|
||||
* @param {number} id - 服务ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteService(id) {
|
||||
return request({
|
||||
url: `/backend/deleteServices/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
+88
-12
@@ -2,12 +2,14 @@ import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取基本信息
|
||||
* @param {number} tid 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getNormalInfos() {
|
||||
export function getNormalInfos(tid) {
|
||||
return request({
|
||||
url: "/admin/normalInfos",
|
||||
url: "/backend/normalInfos",
|
||||
method: "get",
|
||||
params: { tid }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,22 +20,46 @@ export function getNormalInfos() {
|
||||
*/
|
||||
export function saveNormalInfos(data) {
|
||||
return request({
|
||||
url: "/admin/saveNormalInfos",
|
||||
url: "/backend/saveNormalInfos",
|
||||
method: "post",
|
||||
data: {
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录验证数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getVerifyInfos() {
|
||||
return request({
|
||||
url: "/backend/loginVerifyInfos",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存登录验证数据
|
||||
* @param {Object} data 要保存的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function saveVerifyInfos(data) {
|
||||
return request({
|
||||
url: "/backend/saveloginVerifyInfos",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取法律声明和隐私条款
|
||||
* @param {number} tid 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getLegalInfos() {
|
||||
export function getLegalInfos(tid) {
|
||||
return request({
|
||||
url: "/admin/legalInfos",
|
||||
url: "/backend/legalInfos",
|
||||
method: "get",
|
||||
params: { tid }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,10 +70,60 @@ export function getLegalInfos() {
|
||||
*/
|
||||
export function saveLegalInfos(data) {
|
||||
return request({
|
||||
url: "/admin/saveLegalInfos",
|
||||
url: "/backend/saveLegalInfos",
|
||||
method: "post",
|
||||
data: {
|
||||
data: JSON.stringify(data)
|
||||
},
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业信息
|
||||
* @param {number} tid 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getCompanyInfos(tid) {
|
||||
return request({
|
||||
url: "/backend/companyInfos",
|
||||
method: "get",
|
||||
params: { tid }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存企业信息
|
||||
* @param {Object} data 要保存的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function saveCompanyInfos(data) {
|
||||
return request({
|
||||
url: "/backend/saveCompanyInfos",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业SEO
|
||||
* @param {number} tid 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getCompanySeo(tid) {
|
||||
return request({
|
||||
url: "/backend/companySeo",
|
||||
method: "get",
|
||||
params: { tid }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存企业SEO
|
||||
* @param {Object} data 要保存的数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function saveCompanySeo(data) {
|
||||
return request({
|
||||
url: "/backend/saveCompanySeo",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/**
|
||||
* 获取短信网关配置
|
||||
*/
|
||||
export function getSmsInfo() {
|
||||
return request({
|
||||
url: "/backend/sms/info",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑短信网关配置
|
||||
*/
|
||||
export function editSmsInfo(data: any) {
|
||||
return request({
|
||||
url: "/backend/sms/editinfo",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试短信(入队任务,等待网关发送)
|
||||
*/
|
||||
export function sendTestSms(data: any) {
|
||||
return request({
|
||||
url: "/backend/sms/sendtest",
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信任务列表(租户隔离)
|
||||
*/
|
||||
export function getSmsTaskList(params: { status?: string | number; phone?: string } = {}) {
|
||||
return request({
|
||||
url: "/backend/sms/taskList",
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑短信任务
|
||||
*/
|
||||
export function editSmsTask(id: number | string, data: any) {
|
||||
return request({
|
||||
url: `/backend/sms/taskEdit/${id}`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
/*************************************************
|
||||
****************** 租户相关接口 ******************
|
||||
*************************************************/
|
||||
|
||||
/**
|
||||
* 获取租户列表
|
||||
* @param {Object} params 包含 page 和 pageSize
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTenantList(params) {
|
||||
return request({
|
||||
url: "/backend/tenant/getTenant",
|
||||
method: "get",
|
||||
params: params,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户详情
|
||||
* @param {number} id 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTenantDetail(id) {
|
||||
return request({
|
||||
url: `/backend/tenant/getTenantDetail/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建租户数据
|
||||
* @param {Object} data 租户数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function createTenant(data) {
|
||||
return request({
|
||||
url: "/backend/tenant/createTenant",
|
||||
method: "post",
|
||||
data: data,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新租户数据
|
||||
* @param {Object} data 租户数据
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function editTenant(id, data) {
|
||||
return request({
|
||||
url: `/backend/tenant/editTenant/${id}`,
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除租户数据
|
||||
* @param {number} id 租户ID
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function deleteTenant(id) {
|
||||
return request({
|
||||
url: `/backend/tenant/deleteTenant/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验租户编码是否重复
|
||||
* @param {string} tenant_code 编码
|
||||
* @param {number} id 可选,当前编辑的租户ID
|
||||
*/
|
||||
export function checkTenantCode(tenant_code) {
|
||||
return request({
|
||||
url: '/backend/tenant/findTenantCode',
|
||||
method: 'get',
|
||||
params: { tenant_code }
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取模板列表
|
||||
export function getThemeList() {
|
||||
return request({
|
||||
url: '/backend/theme',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 切换模板
|
||||
export function switchTheme(data) {
|
||||
return request({
|
||||
url: '/backend/theme/switch',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取模板数据
|
||||
export function getThemeData(params) {
|
||||
return request({
|
||||
url: '/backend/theme/data',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 保存模板数据
|
||||
export function saveThemeData(data) {
|
||||
return request({
|
||||
url: '/backend/theme/data',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
+31
-7
@@ -1,9 +1,18 @@
|
||||
import request from '@/utils/request';
|
||||
|
||||
//获取所有用户信息
|
||||
export function getAllUsers() {
|
||||
export function getAllUsers(params) {
|
||||
return request({
|
||||
url: '/admin/getAllUsers',
|
||||
url: '/backend/getAllUsers',
|
||||
method: 'get',
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
//获取租户用户
|
||||
export function getTenantUsers(tenantId) {
|
||||
return request({
|
||||
url: `/backend/getTenantUsers/${tenantId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -11,7 +20,7 @@ export function getAllUsers() {
|
||||
// 获取用户信息
|
||||
export function getUserInfo(userId) {
|
||||
return request({
|
||||
url: `/admin/getUserInfo/${userId}`,
|
||||
url: `/backend/getUserInfo/${userId}`,
|
||||
method: 'get',
|
||||
});
|
||||
}
|
||||
@@ -19,7 +28,7 @@ export function getUserInfo(userId) {
|
||||
// 添加用户
|
||||
export function addUser(data) {
|
||||
return request({
|
||||
url: '/admin/addUser',
|
||||
url: '/backend/addUser',
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -28,7 +37,7 @@ export function addUser(data) {
|
||||
// 编辑用户信息
|
||||
export function editUser(userId, data) {
|
||||
return request({
|
||||
url: `/admin/editUser/${userId}`,
|
||||
url: `/backend/editUser/${userId}`,
|
||||
method: 'post',
|
||||
data,
|
||||
});
|
||||
@@ -39,10 +48,25 @@ export function updateUserInfo(userId, data) {
|
||||
return editUser(userId, data);
|
||||
}
|
||||
|
||||
// 更新个人资料
|
||||
export function updateUserProfile(userId, data) {
|
||||
return editUser(userId, data);
|
||||
}
|
||||
|
||||
// 绑定/更换手机号
|
||||
export function bindPhone(userId, phone) {
|
||||
return editUser(userId, { phone });
|
||||
}
|
||||
|
||||
// 绑定/更换邮箱
|
||||
export function bindEmail(userId, email) {
|
||||
return editUser(userId, { email });
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
export function deleteUser(userId) {
|
||||
return request({
|
||||
url: `/admin/deleteUser/${userId}`,
|
||||
url: `/backend/deleteUser/${userId}`,
|
||||
method: 'delete',
|
||||
});
|
||||
}
|
||||
@@ -50,7 +74,7 @@ export function deleteUser(userId) {
|
||||
// 修改密码
|
||||
export function changePassword(userId, data) {
|
||||
return request({
|
||||
url: '/admin/changePassword',
|
||||
url: '/backend/changePassword',
|
||||
method: 'post',
|
||||
data: {
|
||||
id: userId,
|
||||
|
||||
@@ -4,7 +4,7 @@ import request from "@/utils/request";
|
||||
// 获取文章列表
|
||||
export function GetCRMWorkbench(params) {
|
||||
return request({
|
||||
url: `/admin/workbench/crm`,
|
||||
url: `/backend/workbench/crm`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
"v0.5.0 Geetest Inc.";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error('Geetest requires browser environment');
|
||||
}
|
||||
|
||||
var document = window.document;
|
||||
var Math = window.Math;
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
|
||||
function _Object(obj) {
|
||||
this._obj = obj;
|
||||
}
|
||||
|
||||
_Object.prototype = {
|
||||
_each: function (process) {
|
||||
var _obj = this._obj;
|
||||
for (var k in _obj) {
|
||||
if (_obj.hasOwnProperty(k)) {
|
||||
process(k, _obj[k]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
function Config(config) {
|
||||
var self = this;
|
||||
new _Object(config)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
Config.prototype = {
|
||||
api_server: 'api.geetest.com',
|
||||
protocol: 'http://',
|
||||
typePath: '/gettype.php',
|
||||
fallback_config: {
|
||||
slide: {
|
||||
static_servers: ["static.geetest.com", "static.geevisit.com"],
|
||||
type: 'slide',
|
||||
slide: '/static/js/geetest.0.0.0.js'
|
||||
},
|
||||
fullpage: {
|
||||
static_servers: ["static.geetest.com", "static.geevisit.com"],
|
||||
type: 'fullpage',
|
||||
fullpage: '/static/js/fullpage.0.0.0.js'
|
||||
}
|
||||
},
|
||||
_get_fallback_config: function () {
|
||||
var self = this;
|
||||
if (isString(self.type)) {
|
||||
return self.fallback_config[self.type];
|
||||
} else if (self.new_captcha) {
|
||||
return self.fallback_config.fullpage;
|
||||
} else {
|
||||
return self.fallback_config.slide;
|
||||
}
|
||||
},
|
||||
_extend: function (obj) {
|
||||
var self = this;
|
||||
new _Object(obj)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
})
|
||||
}
|
||||
};
|
||||
var isNumber = function (value) {
|
||||
return (typeof value === 'number');
|
||||
};
|
||||
var isString = function (value) {
|
||||
return (typeof value === 'string');
|
||||
};
|
||||
var isBoolean = function (value) {
|
||||
return (typeof value === 'boolean');
|
||||
};
|
||||
var isObject = function (value) {
|
||||
return (typeof value === 'object' && value !== null);
|
||||
};
|
||||
var isFunction = function (value) {
|
||||
return (typeof value === 'function');
|
||||
};
|
||||
var MOBILE = /Mobi/i.test(navigator.userAgent);
|
||||
var pt = MOBILE ? 3 : 0;
|
||||
|
||||
var callbacks = {};
|
||||
var status = {};
|
||||
|
||||
var nowDate = function () {
|
||||
var date = new Date();
|
||||
var year = date.getFullYear();
|
||||
var month = date.getMonth() + 1;
|
||||
var day = date.getDate();
|
||||
var hours = date.getHours();
|
||||
var minutes = date.getMinutes();
|
||||
var seconds = date.getSeconds();
|
||||
|
||||
if (month >= 1 && month <= 9) {
|
||||
month = '0' + month;
|
||||
}
|
||||
if (day >= 0 && day <= 9) {
|
||||
day = '0' + day;
|
||||
}
|
||||
if (hours >= 0 && hours <= 9) {
|
||||
hours = '0' + hours;
|
||||
}
|
||||
if (minutes >= 0 && minutes <= 9) {
|
||||
minutes = '0' + minutes;
|
||||
}
|
||||
if (seconds >= 0 && seconds <= 9) {
|
||||
seconds = '0' + seconds;
|
||||
}
|
||||
var currentdate = year + '-' + month + '-' + day + " " + hours + ":" + minutes + ":" + seconds;
|
||||
return currentdate;
|
||||
}
|
||||
|
||||
var random = function () {
|
||||
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
|
||||
};
|
||||
|
||||
var loadScript = function (url, cb) {
|
||||
var script = document.createElement("script");
|
||||
script.charset = "UTF-8";
|
||||
script.async = true;
|
||||
|
||||
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
|
||||
if ( /static\.geetest\.com/g.test(url)) {
|
||||
script.crossOrigin = "anonymous";
|
||||
}
|
||||
|
||||
script.onerror = function () {
|
||||
cb(true);
|
||||
};
|
||||
var loaded = false;
|
||||
script.onload = script.onreadystatechange = function () {
|
||||
if (!loaded &&
|
||||
(!script.readyState ||
|
||||
"loaded" === script.readyState ||
|
||||
"complete" === script.readyState)) {
|
||||
|
||||
loaded = true;
|
||||
setTimeout(function () {
|
||||
cb(false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
script.src = url;
|
||||
head.appendChild(script);
|
||||
};
|
||||
|
||||
var normalizeDomain = function (domain) {
|
||||
// special domain: uems.sysu.edu.cn/jwxt/geetest/
|
||||
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
|
||||
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
|
||||
};
|
||||
var normalizePath = function (path) {
|
||||
path = path.replace(/\/+/g, '/');
|
||||
if (path.indexOf('/') !== 0) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
var normalizeQuery = function (query) {
|
||||
if (!query) {
|
||||
return '';
|
||||
}
|
||||
var q = '?';
|
||||
new _Object(query)._each(function (key, value) {
|
||||
if (isString(value) || isNumber(value) || isBoolean(value)) {
|
||||
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
|
||||
}
|
||||
});
|
||||
if (q === '?') {
|
||||
q = '';
|
||||
}
|
||||
return q.replace(/&$/, '');
|
||||
};
|
||||
var makeURL = function (protocol, domain, path, query) {
|
||||
domain = normalizeDomain(domain);
|
||||
|
||||
var url = normalizePath(path) + normalizeQuery(query);
|
||||
if (domain) {
|
||||
url = protocol + domain + url;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var load = function (config, send, protocol, domains, path, query, cb) {
|
||||
var tryRequest = function (at) {
|
||||
|
||||
var url = makeURL(protocol, domains[at], path, query);
|
||||
loadScript(url, function (err) {
|
||||
if (err) {
|
||||
if (at >= domains.length - 1) {
|
||||
cb(true);
|
||||
// report gettype error
|
||||
if (send) {
|
||||
config.error_code = 508;
|
||||
var url = protocol + domains[at] + path;
|
||||
reportError(config, url);
|
||||
}
|
||||
} else {
|
||||
tryRequest(at + 1);
|
||||
}
|
||||
} else {
|
||||
cb(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
tryRequest(0);
|
||||
};
|
||||
|
||||
|
||||
var jsonp = function (domains, path, config, callback) {
|
||||
if (isObject(config.getLib)) {
|
||||
config._extend(config.getLib);
|
||||
callback(config);
|
||||
return;
|
||||
}
|
||||
if (config.offline) {
|
||||
callback(config._get_fallback_config());
|
||||
return;
|
||||
}
|
||||
|
||||
var cb = "geetest_" + random();
|
||||
window[cb] = function (data) {
|
||||
if (data.status == 'success') {
|
||||
callback(data.data);
|
||||
} else if (!data.status) {
|
||||
callback(data);
|
||||
} else {
|
||||
callback(config._get_fallback_config());
|
||||
}
|
||||
window[cb] = undefined;
|
||||
try {
|
||||
delete window[cb];
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
load(config, true, config.protocol, domains, path, {
|
||||
gt: config.gt,
|
||||
callback: cb
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
callback(config._get_fallback_config());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var reportError = function (config, url) {
|
||||
load(config, false, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
|
||||
time: nowDate(),
|
||||
captcha_id: config.gt,
|
||||
challenge: config.challenge,
|
||||
pt: pt,
|
||||
exception_url: url,
|
||||
error_code: config.error_code
|
||||
}, function (err) {})
|
||||
}
|
||||
|
||||
var throwError = function (errorType, config) {
|
||||
var errors = {
|
||||
networkError: '网络错误',
|
||||
gtTypeError: 'gtå—æ®µä¸æ˜¯å—符串类型'
|
||||
};
|
||||
if (typeof config.onError === 'function') {
|
||||
config.onError(errors[errorType]);
|
||||
} else {
|
||||
throw new Error(errors[errorType]);
|
||||
}
|
||||
};
|
||||
|
||||
var detect = function () {
|
||||
return window.Geetest || document.getElementById("gt_lib");
|
||||
};
|
||||
|
||||
if (detect()) {
|
||||
status.slide = "loaded";
|
||||
}
|
||||
|
||||
window.initGeetest = function (userConfig, callback) {
|
||||
|
||||
var config = new Config(userConfig);
|
||||
|
||||
if (userConfig.https) {
|
||||
config.protocol = 'https://';
|
||||
} else if (!userConfig.protocol) {
|
||||
config.protocol = window.location.protocol + '//';
|
||||
}
|
||||
|
||||
// for KFC
|
||||
if (userConfig.gt === '050cffef4ae57b5d5e529fea9540b0d1' ||
|
||||
userConfig.gt === '3bd38408ae4af923ed36e13819b14d42') {
|
||||
config.apiserver = 'yumchina.geetest.com/'; // for old js
|
||||
config.api_server = 'yumchina.geetest.com';
|
||||
}
|
||||
|
||||
if(userConfig.gt){
|
||||
window.GeeGT = userConfig.gt
|
||||
}
|
||||
|
||||
if(userConfig.challenge){
|
||||
window.GeeChallenge = userConfig.challenge
|
||||
}
|
||||
|
||||
if (isObject(userConfig.getType)) {
|
||||
config._extend(userConfig.getType);
|
||||
}
|
||||
jsonp((config.api_server_v3 || [config.api_server || config.apiserver]), config.typePath, config, function (newConfig) {
|
||||
var type = newConfig.type;
|
||||
var init = function () {
|
||||
config._extend(newConfig);
|
||||
callback(new window.Geetest(config));
|
||||
};
|
||||
|
||||
callbacks[type] = callbacks[type] || [];
|
||||
var s = status[type] || 'init';
|
||||
if (s === 'init') {
|
||||
status[type] = 'loading';
|
||||
|
||||
callbacks[type].push(init);
|
||||
|
||||
load(config, true, config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function (err) {
|
||||
if (err) {
|
||||
status[type] = 'fail';
|
||||
throwError('networkError', config);
|
||||
} else {
|
||||
status[type] = 'loaded';
|
||||
var cbs = callbacks[type];
|
||||
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
|
||||
var cb = cbs[i];
|
||||
if (isFunction(cb)) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
callbacks[type] = [];
|
||||
}
|
||||
});
|
||||
} else if (s === "loaded") {
|
||||
init();
|
||||
} else if (s === "fail") {
|
||||
throwError('networkError', config);
|
||||
} else if (s === "loading") {
|
||||
callbacks[type].push(init);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
})(window);
|
||||
@@ -0,0 +1,487 @@
|
||||
"v4.2.0 Geetest Inc.";
|
||||
|
||||
(function (window) {
|
||||
"use strict";
|
||||
if (typeof window === 'undefined') {
|
||||
throw new Error('Geetest requires browser environment');
|
||||
}
|
||||
|
||||
var document = window.document;
|
||||
var Math = window.Math;
|
||||
var head = document.getElementsByTagName("head")[0];
|
||||
var TIMEOUT = 10000;
|
||||
|
||||
function _Object(obj) {
|
||||
this._obj = obj;
|
||||
}
|
||||
|
||||
_Object.prototype = {
|
||||
_each: function (process) {
|
||||
var _obj = this._obj;
|
||||
for (var k in _obj) {
|
||||
if (_obj.hasOwnProperty(k)) {
|
||||
process(k, _obj[k]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
},
|
||||
_extend: function (obj){
|
||||
var self = this;
|
||||
new _Object(obj)._each(function (key, value){
|
||||
self._obj[key] = value;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
var uuid = function () {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0;
|
||||
var v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
function Config(config) {
|
||||
var self = this;
|
||||
new _Object(config)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
});
|
||||
}
|
||||
|
||||
Config.prototype = {
|
||||
apiServers: ['gcaptcha4.geetest.com','gcaptcha4.geevisit.com','gcaptcha4.gsensebot.com'],
|
||||
staticServers: ["static.geetest.com",'static.geevisit.com'],
|
||||
protocol: 'http://',
|
||||
typePath: '/load',
|
||||
fallback_config: {
|
||||
bypass: {
|
||||
staticServers: ["static.geetest.com",'static.geevisit.com'],
|
||||
type: 'bypass',
|
||||
bypass: '/v4/bypass.js'
|
||||
}
|
||||
},
|
||||
_get_fallback_config: function () {
|
||||
var self = this;
|
||||
if (isString(self.type)) {
|
||||
return self.fallback_config[self.type];
|
||||
} else {
|
||||
return self.fallback_config.bypass;
|
||||
}
|
||||
},
|
||||
_extend: function (obj) {
|
||||
var self = this;
|
||||
new _Object(obj)._each(function (key, value) {
|
||||
self[key] = value;
|
||||
})
|
||||
}
|
||||
};
|
||||
var isNumber = function (value) {
|
||||
return (typeof value === 'number');
|
||||
};
|
||||
var isString = function (value) {
|
||||
return (typeof value === 'string');
|
||||
};
|
||||
var isBoolean = function (value) {
|
||||
return (typeof value === 'boolean');
|
||||
};
|
||||
var isObject = function (value) {
|
||||
return (typeof value === 'object' && value !== null);
|
||||
};
|
||||
var isFunction = function (value) {
|
||||
return (typeof value === 'function');
|
||||
};
|
||||
var MOBILE = /Mobi/i.test(navigator.userAgent);
|
||||
|
||||
var callbacks = {};
|
||||
var status = {};
|
||||
|
||||
var random = function () {
|
||||
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
|
||||
};
|
||||
|
||||
// bind 函数polify, ä¸å¸¦new功能的bind
|
||||
|
||||
var bind = function(target,context){
|
||||
if(typeof target !== 'function'){
|
||||
return;
|
||||
}
|
||||
var args = Array.prototype.slice.call(arguments,2);
|
||||
|
||||
if(Function.prototype.bind){
|
||||
return target.bind(context, args);
|
||||
}else {
|
||||
return function(){
|
||||
var _args = Array.prototype.slice.call(arguments);
|
||||
return target.apply(context,args.concat(_args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var toString = Object.prototype.toString;
|
||||
|
||||
var _isFunction = function(obj) {
|
||||
return typeof(obj) === 'function';
|
||||
};
|
||||
var _isObject = function(obj) {
|
||||
return obj === Object(obj);
|
||||
};
|
||||
var _isArray = function(obj) {
|
||||
return toString.call(obj) == '[object Array]';
|
||||
};
|
||||
var _isDate = function(obj) {
|
||||
return toString.call(obj) == '[object Date]';
|
||||
};
|
||||
var _isRegExp = function(obj) {
|
||||
return toString.call(obj) == '[object RegExp]';
|
||||
};
|
||||
var _isBoolean = function(obj) {
|
||||
return toString.call(obj) == '[object Boolean]';
|
||||
};
|
||||
|
||||
|
||||
function resolveKey(input){
|
||||
return input.replace(/(\S)(_([a-zA-Z]))/g, function(match, $1, $2, $3){
|
||||
return $1 + $3.toUpperCase() || "";
|
||||
})
|
||||
}
|
||||
|
||||
function camelizeKeys(input, convert){
|
||||
if(!_isObject(input) || _isDate(input) || _isRegExp(input) || _isBoolean(input) || _isFunction(input)){
|
||||
return convert ? resolveKey(input) : input;
|
||||
}
|
||||
|
||||
if(_isArray(input)){
|
||||
var temp = [];
|
||||
for(var i = 0; i < input.length; i++){
|
||||
temp.push(camelizeKeys(input[i]));
|
||||
}
|
||||
|
||||
}else {
|
||||
var temp = {};
|
||||
for(var prop in input){
|
||||
if(input.hasOwnProperty(prop)){
|
||||
temp[camelizeKeys(prop, true)] = camelizeKeys(input[prop]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
var loadScript = function (url, cb, timeout) {
|
||||
var script = document.createElement("script");
|
||||
script.charset = "UTF-8";
|
||||
script.async = true;
|
||||
|
||||
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
|
||||
if ( /static\.geetest\.com/g.test(url)) {
|
||||
script.crossOrigin = "anonymous";
|
||||
}
|
||||
|
||||
script.onerror = function () {
|
||||
cb(true);
|
||||
// 错误触å‘了,超时逻辑就ä¸ç”¨äº†
|
||||
loaded = true;
|
||||
};
|
||||
var loaded = false;
|
||||
script.onload = script.onreadystatechange = function () {
|
||||
if (!loaded &&
|
||||
(!script.readyState ||
|
||||
"loaded" === script.readyState ||
|
||||
"complete" === script.readyState)) {
|
||||
|
||||
loaded = true;
|
||||
setTimeout(function () {
|
||||
cb(false);
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
script.src = url;
|
||||
head.appendChild(script);
|
||||
|
||||
setTimeout(function () {
|
||||
if (!loaded) {
|
||||
script.onerror = script.onload = null;
|
||||
script.remove && script.remove();
|
||||
cb(true);
|
||||
}
|
||||
}, timeout || TIMEOUT);
|
||||
};
|
||||
|
||||
var normalizeDomain = function (domain) {
|
||||
// special domain: uems.sysu.edu.cn/jwxt/geetest/
|
||||
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
|
||||
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
|
||||
};
|
||||
var normalizePath = function (path) {
|
||||
|
||||
path = path && path.replace(/\/+/g, '/');
|
||||
if (path.indexOf('/') !== 0) {
|
||||
path = '/' + path;
|
||||
}
|
||||
return path;
|
||||
};
|
||||
var normalizeQuery = function (query) {
|
||||
if (!query) {
|
||||
return '';
|
||||
}
|
||||
var q = '?';
|
||||
new _Object(query)._each(function (key, value) {
|
||||
if (isString(value) || isNumber(value) || isBoolean(value)) {
|
||||
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
|
||||
}
|
||||
});
|
||||
if (q === '?') {
|
||||
q = '';
|
||||
}
|
||||
return q.replace(/&$/, '');
|
||||
};
|
||||
var makeURL = function (protocol, domain, path, query) {
|
||||
domain = normalizeDomain(domain);
|
||||
|
||||
var url = normalizePath(path) + normalizeQuery(query);
|
||||
if (domain) {
|
||||
url = protocol + domain + url;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var load = function (config, protocol, domains, path, query, cb, handleCb) {
|
||||
var tryRequest = function (at) {
|
||||
// 处ç†jsonp回调,这里为了ä¿è¯æ¯ä¸ªä¸åŒjsonp都有唯一的回调函数
|
||||
if(handleCb){
|
||||
var cbName = "geetest_" + random();
|
||||
// 需è¦ä¸Žé¢„先定义好cbname傿•°ï¼Œåˆ 除对象
|
||||
window[cbName] = bind(handleCb, null, cbName);
|
||||
query.callback = cbName;
|
||||
}
|
||||
var url = makeURL(protocol, domains[at], path, query);
|
||||
loadScript(url, function (err) {
|
||||
if (err) {
|
||||
// 超时或者出错的时候 移除回调
|
||||
if(cbName){
|
||||
try {
|
||||
window[cbName] = function(){
|
||||
window[cbName] = null;
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (at >= domains.length - 1) {
|
||||
cb(true);
|
||||
// report gettype error
|
||||
} else {
|
||||
tryRequest(at + 1);
|
||||
}
|
||||
} else {
|
||||
cb(false);
|
||||
}
|
||||
}, config.timeout);
|
||||
};
|
||||
tryRequest(0);
|
||||
};
|
||||
|
||||
|
||||
var jsonp = function (domains, path, config, callback) {
|
||||
|
||||
var handleCb = function (cbName, data) {
|
||||
|
||||
// ä¿è¯åªæ‰§è¡Œä¸€æ¬¡ï¼Œå…¨éƒ¨è¶…时的情况下ä¸ä¼šå†è§¦å‘;
|
||||
|
||||
if (data.status == 'success') {
|
||||
callback(data.data);
|
||||
} else if (!data.status) {
|
||||
callback(data);
|
||||
} else {
|
||||
//æŽ¥å£æœ‰è¿”回,但是返回了错误状æ€ï¼Œè¿›å…¥æŠ¥é”™é€»è¾‘
|
||||
callback(data);
|
||||
}
|
||||
window[cbName] = undefined;
|
||||
try {
|
||||
delete window[cbName];
|
||||
} catch (e) {
|
||||
}
|
||||
};
|
||||
load(config, config.protocol, domains, path, {
|
||||
callback: '',
|
||||
captcha_id: config.captchaId,
|
||||
challenge: config.challenge || uuid(),
|
||||
client_type: config.clientType ? config.clientType : (MOBILE? 'h5':'web'),
|
||||
risk_type: config.riskType,
|
||||
user_info: config.userInfo,
|
||||
call_type: config.callType,
|
||||
lang: config.language? config.language : navigator.appName === 'Netscape' ? navigator.language.toLowerCase() : navigator.userLanguage.toLowerCase()
|
||||
}, function (err) {
|
||||
// ç½‘ç»œé—®é¢˜æŽ¥å£æ²¡æœ‰è¿”回,直接使用本地验è¯ç ,走宕机模å¼
|
||||
// 这里å¯ä»¥æ·»åŠ ç”¨æˆ·çš„é€»è¾‘
|
||||
if(err && typeof config.offlineCb === 'function'){
|
||||
// 执行自己的宕机
|
||||
config.offlineCb();
|
||||
return;
|
||||
}
|
||||
if(err){
|
||||
callback(config._get_fallback_config());
|
||||
}
|
||||
}, handleCb);
|
||||
};
|
||||
|
||||
var reportError = function (config, url) {
|
||||
load(config, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
|
||||
time: Date.now().getTime(),
|
||||
captcha_id: config.gt,
|
||||
challenge: config.challenge,
|
||||
exception_url: url,
|
||||
error_code: config.error_code
|
||||
}, function (err) {})
|
||||
}
|
||||
|
||||
var throwError = function (errorType, config, errObj) {
|
||||
var errors = {
|
||||
networkError: '网络错误',
|
||||
gtTypeError: 'gtå—æ®µä¸æ˜¯å—符串类型'
|
||||
};
|
||||
if (typeof config.onError === 'function') {
|
||||
config.onError({
|
||||
desc: errObj.desc,
|
||||
msg: errObj.msg,
|
||||
code: errObj.code
|
||||
});
|
||||
} else {
|
||||
throw new Error(errors[errorType]);
|
||||
}
|
||||
};
|
||||
|
||||
var detect = function () {
|
||||
return window.Geetest || document.getElementById("gt_lib");
|
||||
};
|
||||
|
||||
if (detect()) {
|
||||
status.slide = "loaded";
|
||||
}
|
||||
var GeetestIsLoad = function (fname) {
|
||||
var GeetestIsLoad = false;
|
||||
var tags = { js: 'script', css: 'link' };
|
||||
var tagname = fname && tags[fname.split('.').pop()];
|
||||
if (tagname !== undefined) {
|
||||
var elts = document.getElementsByTagName(tagname);
|
||||
for (var i in elts) {
|
||||
if ((elts[i].href && elts[i].href.toString().indexOf(fname) > 0)
|
||||
|| (elts[i].src && elts[i].src.toString().indexOf(fname) > 0)) {
|
||||
GeetestIsLoad = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return GeetestIsLoad;
|
||||
};
|
||||
window.initGeetest4 = function (userConfig,callback) {
|
||||
|
||||
var config = new Config(userConfig);
|
||||
if (userConfig.https) {
|
||||
config.protocol = 'https://';
|
||||
} else if (!userConfig.protocol) {
|
||||
config.protocol = window.location.protocol + '//';
|
||||
}
|
||||
|
||||
|
||||
if (isObject(userConfig.getType)) {
|
||||
config._extend(userConfig.getType);
|
||||
}
|
||||
|
||||
jsonp(config.apiServers , config.typePath, config, function (newConfig) {
|
||||
//错误æ•获,第一个load请求å¯èƒ½ç›´æŽ¥æŠ¥é”™
|
||||
var newConfig = camelizeKeys(newConfig);
|
||||
|
||||
if(newConfig.status === 'error'){
|
||||
return throwError('networkError', config, newConfig);
|
||||
}
|
||||
|
||||
var type = newConfig.type;
|
||||
if(config.debug){
|
||||
new _Object(newConfig)._extend(config.debug)
|
||||
}
|
||||
var init = function () {
|
||||
config._extend(newConfig);
|
||||
callback(new window.Geetest4(config));
|
||||
};
|
||||
|
||||
callbacks[type] = callbacks[type] || [];
|
||||
|
||||
var s = status[type] || 'init';
|
||||
if (s === 'init') {
|
||||
status[type] = 'loading';
|
||||
|
||||
callbacks[type].push(init);
|
||||
|
||||
if(newConfig.gctPath){
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
|
||||
if(err){
|
||||
throwError('networkError', config, {
|
||||
code: '60205',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'gct resource load timeout'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers, newConfig.bypass || (newConfig.staticPath + newConfig.js), null, function (err) {
|
||||
if (err) {
|
||||
status[type] = 'fail';
|
||||
throwError('networkError', config, {
|
||||
code: '60204',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'js resource load timeout'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
||||
status[type] = 'loaded';
|
||||
var cbs = callbacks[type];
|
||||
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
|
||||
var cb = cbs[i];
|
||||
if (isFunction(cb)) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
callbacks[type] = [];
|
||||
status[type] = 'init';
|
||||
}
|
||||
});
|
||||
} else if (s === "loaded") {
|
||||
// 判æ–gct是å¦éœ€è¦é‡æ–°åŠ è½½
|
||||
if(newConfig.gctPath && !GeetestIsLoad(newConfig.gctPath)){
|
||||
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
|
||||
if(err){
|
||||
throwError('networkError', config, {
|
||||
code: '60205',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'gct resource load timeout'
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
return init();
|
||||
} else if (s === "fail") {
|
||||
throwError('networkError', config, {
|
||||
code: '60204',
|
||||
msg: 'Network failure',
|
||||
desc: {
|
||||
detail: 'js resource load timeout'
|
||||
}
|
||||
});
|
||||
} else if (s === "loading") {
|
||||
callbacks[type].push(init);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
})(window);
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0 0 400 400" fill="none"><path d="M77.08 90L15 90L89.64 239.51L89.64 310L145.07 310L145.07 239.51L196.406 137.69L275.435 137.69L203.574 276.92L203.574 310L386.005 310L386.005 262.31L272.464 262.31L345.085 123.08L345.085 90L157.64 90L116.99 180.63L77.08 90Z" fill-rule="evenodd" fill="#FFFFFF" ></path></svg>
|
||||
|
After Width: | Height: | Size: 425 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0 0 400 400" fill="none"><path d="M77.08 90L15 90L89.64 239.51L89.64 310L145.07 310L145.07 239.51L196.406 137.69L275.435 137.69L203.574 276.92L203.574 310L386.005 310L386.005 262.31L272.464 262.31L345.085 123.08L345.085 90L157.64 90L116.99 180.63L77.08 90Z" fill-rule="evenodd" fill="#000000" ></path></svg>
|
||||
|
After Width: | Height: | Size: 425 B |
@@ -1 +0,0 @@
|
||||
<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="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -27,7 +27,7 @@
|
||||
:default-active="route.path"
|
||||
>
|
||||
<!-- 菜单标题 -->
|
||||
<h3>{{ isCollapse ? "管理" : currentModule?.title || "子菜单" }}</h3>
|
||||
<h3>{{ isCollapse ? "管理" : asideTitle }}</h3>
|
||||
|
||||
<!-- 无模块时显示提示 -->
|
||||
<el-menu-item v-if="!currentModule" index="/home">
|
||||
@@ -248,12 +248,30 @@ const displayMenus = computed(() => {
|
||||
if (!currentModule.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentPath = route.path;
|
||||
// 访问“父级 index 页面”时不展开子菜单;只有进入“子页面”后才展示 children。
|
||||
// 例如:/apps/cms/products(父级)时不展示其 children(如 /apps/cms/products/types)。
|
||||
if (currentPath === currentModule.value.path) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return currentModule.value.children || [];
|
||||
});
|
||||
|
||||
const asideTitle = computed(() => {
|
||||
if (isCollapse.value) return "管理";
|
||||
if (!currentModule.value) return "子菜单";
|
||||
return displayMenus.value.length > 0 ? (currentModule.value.title || "子菜单") : "子菜单";
|
||||
});
|
||||
|
||||
const processMenus = (menus) => {
|
||||
return menus
|
||||
.filter((menu) => {
|
||||
// is_visible 控制“侧边栏是否展示”,不参与动态路由与缓存层面的过滤
|
||||
if (menu.is_visible !== undefined && Number(menu.is_visible) === 0) {
|
||||
return false;
|
||||
}
|
||||
if (menu.path && menu.path.trim() !== "") return true;
|
||||
if (menu.children && menu.children.length > 0) return true;
|
||||
return false;
|
||||
@@ -445,7 +463,7 @@ h3 {
|
||||
background-color: rgba(60, 60, 60, 0.8) !important;
|
||||
}
|
||||
color: #ffffff !important;
|
||||
border-left: 3px solid #4f84ff;
|
||||
/* border-left: 3px solid #4f84ff; */
|
||||
margin-left: -3px;
|
||||
|
||||
.menu-icon {
|
||||
@@ -486,7 +504,7 @@ h3 {
|
||||
.el-menu-item.is-active {
|
||||
background: rgba(219, 148, 148, 0.8) !important;
|
||||
color: var(--el-color-primary-light-3) !important;
|
||||
border-left-color: var(--el-color-primary);
|
||||
/* border-left-color: var(--el-color-primary); */
|
||||
|
||||
.menu-icon {
|
||||
color: var(--el-color-primary);
|
||||
|
||||
@@ -4,15 +4,6 @@
|
||||
<el-button size="small" @click="handleCollapse">
|
||||
<i class="fa fa-bars"></i>
|
||||
</el-button>
|
||||
<!-- <el-breadcrumb separator="/" class="bread">
|
||||
<el-breadcrumb-item :to="{ path: '/' }"
|
||||
style="position: relative !important; top: 1px !important;">首页</el-breadcrumb-item>
|
||||
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index"
|
||||
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null"
|
||||
style="position: relative !important; top: 1px !important;">
|
||||
{{ item.label }}
|
||||
</el-breadcrumb-item>
|
||||
</el-breadcrumb> -->
|
||||
</div>
|
||||
<div class="r-content">
|
||||
<!-- 返回首页按钮 -->
|
||||
@@ -223,6 +214,8 @@ const handleCommand = async (command) => {
|
||||
localStorage.removeItem('tabs_list');
|
||||
localStorage.removeItem('active_tab');
|
||||
sessionStorage.removeItem('tabs_list');
|
||||
|
||||
localStorage.removeItem('tabs_list');
|
||||
// 清除菜单缓存
|
||||
menuStore.resetMenus();
|
||||
|
||||
@@ -231,7 +224,7 @@ const handleCommand = async (command) => {
|
||||
// 遍历 localStorage
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith('menu_cache_')) {
|
||||
if (key && key.startsWith('menu_')) {
|
||||
menuCacheKeys.push(key);
|
||||
}
|
||||
}
|
||||
@@ -244,7 +237,7 @@ const handleCommand = async (command) => {
|
||||
const sessionMenuCacheKeys: string[] = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (key && key.startsWith('menu_cache_')) {
|
||||
if (key && key.startsWith('menu_')) {
|
||||
sessionMenuCacheKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+19
@@ -12,6 +12,25 @@ declare module '@/*' {
|
||||
export default component;
|
||||
}
|
||||
|
||||
declare module '@/api/erp' {
|
||||
export function getOrganizationList(): Promise<any>;
|
||||
export function getOrganizationDetail(id: number | string): Promise<any>;
|
||||
export function createOrganization(data: any): Promise<any>;
|
||||
export function editOrganization(id: number | string, data: any): Promise<any>;
|
||||
export function deleteOrganization(id: number | string): Promise<any>;
|
||||
export function getCompanys(): Promise<any>;
|
||||
export function getDepartments(parentId?: number | string): Promise<any>;
|
||||
export function getEmployeeList(tenantId?: number | string): Promise<any>;
|
||||
export function getEmployeeDetail(id: number | string): Promise<any>;
|
||||
export function createEmployee(data: any): Promise<any>;
|
||||
export function editEmployee(id: number | string, data: any): Promise<any>;
|
||||
export function deleteEmployee(id: number | string): Promise<any>;
|
||||
}
|
||||
|
||||
declare module '@/stores/auth' {
|
||||
export function useAuthStore(): any;
|
||||
}
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_API_BASE_URL: string;
|
||||
// 添加其他环境变量...
|
||||
|
||||
+13
-3
@@ -7,6 +7,7 @@ import 'element-plus/dist/index.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import '@/assets/less/index.less'
|
||||
import '@/assets/css/all.min.css'
|
||||
import '@/assets/js/all.min.js'
|
||||
import router from './router'
|
||||
import { loadAndAddDynamicRoutes } from './router'
|
||||
import { createPinia } from 'pinia'
|
||||
@@ -36,9 +37,18 @@ authStore.checkAuth()
|
||||
|
||||
// 如果用户已登录,在应用启动时加载动态路由
|
||||
if (authStore.isLoggedIn) {
|
||||
loadAndAddDynamicRoutes().catch(err => {
|
||||
console.error('应用启动时加载动态路由失败:', err);
|
||||
});
|
||||
loadAndAddDynamicRoutes()
|
||||
.catch(err => {
|
||||
console.error('应用启动时加载动态路由失败:', err);
|
||||
})
|
||||
.then(() => {
|
||||
// 检查是否因为 token 无效而导致路由加载失败
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
authStore.clearToken();
|
||||
window.location.href = '#/login';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
app.mount('#app')
|
||||
+148
-29
@@ -1,55 +1,174 @@
|
||||
import { createComponentLoader } from '@/utils/pathResolver';
|
||||
import { h, resolveComponent as resolveVueComponent } from 'vue';
|
||||
|
||||
function computeFullPath(menuPath, parentPath) {
|
||||
if (!menuPath) return parentPath || '';
|
||||
if (menuPath.startsWith('/')) {
|
||||
return menuPath.replace(/\/+/g, '/');
|
||||
}
|
||||
const base = (parentPath || '').replace(/\/$/, '');
|
||||
return `${base}/${menuPath}`.replace(/\/+/g, '/');
|
||||
}
|
||||
|
||||
/** 将子路由的绝对路径转为相对父布局的路径,供 Vue Router 嵌套使用 */
|
||||
function toRelativeChildPath(parentAbs, childAbs) {
|
||||
const base = (parentAbs || '').replace(/\/$/, '');
|
||||
const target = (childAbs || '').replace(/\/$/, '');
|
||||
if (!target) return '';
|
||||
if (target === base) return '';
|
||||
const prefix = `${base}/`;
|
||||
if (target.startsWith(prefix)) {
|
||||
return target.slice(prefix.length);
|
||||
}
|
||||
// 兜底:取最后一段(菜单 path 配置异常时)
|
||||
const parts = target.split('/').filter(Boolean);
|
||||
return parts.length ? parts[parts.length - 1] : '';
|
||||
}
|
||||
|
||||
function hasPageComponent(menu) {
|
||||
return menu.type === 4 || (menu.component_path && String(menu.component_path).trim() !== '');
|
||||
}
|
||||
|
||||
function resolvePageComponent(menu) {
|
||||
if (menu.type === 4) {
|
||||
return () => import('@/views/onepage/index.vue');
|
||||
}
|
||||
if (menu.component_path && String(menu.component_path).trim() !== '') {
|
||||
return createComponentLoader(menu.component_path);
|
||||
}
|
||||
return () => import('@/views/404/404.vue');
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单子节点 -> 嵌套路由(path 相对 layoutAbsPath)
|
||||
*/
|
||||
function convertNestedMenuChildren(children, layoutAbsPath) {
|
||||
if (!children || children.length === 0) return [];
|
||||
return children.map((child) => nestedMenuToRoute(child, layoutAbsPath));
|
||||
}
|
||||
|
||||
function nestedMenuToRoute(menu, layoutAbsPath) {
|
||||
const childAbs = computeFullPath(menu.path, layoutAbsPath);
|
||||
const relPath = toRelativeChildPath(layoutAbsPath, childAbs);
|
||||
const hasChildren = menu.children && menu.children.length > 0;
|
||||
const ownPage = hasPageComponent(menu);
|
||||
|
||||
const meta = {
|
||||
title: menu.title,
|
||||
icon: menu.icon,
|
||||
id: menu.id,
|
||||
componentPath: menu.component_path,
|
||||
};
|
||||
|
||||
// 既有自己的页面又有子菜单:套一层 EmptyLayout,避免父页面组件里没有 <router-view> 导致子路由无法渲染
|
||||
if (hasChildren && ownPage) {
|
||||
return {
|
||||
path: relPath,
|
||||
name: `menu_${menu.id}`,
|
||||
meta,
|
||||
component: () => import('@/views/layouts/EmptyLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: `menu_${menu.id}_index`,
|
||||
meta: { ...meta },
|
||||
component: resolvePageComponent(menu),
|
||||
},
|
||||
...convertNestedMenuChildren(menu.children, childAbs),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// 纯目录 + 子节点
|
||||
if (hasChildren && !ownPage) {
|
||||
const route = {
|
||||
path: relPath,
|
||||
name: `menu_${menu.id}`,
|
||||
meta,
|
||||
component: () => import('@/views/layouts/EmptyLayout.vue'),
|
||||
children: convertNestedMenuChildren(menu.children, childAbs),
|
||||
};
|
||||
const firstChild = menu.children[0];
|
||||
if (firstChild && firstChild.path) {
|
||||
const firstAbs = computeFullPath(firstChild.path, childAbs);
|
||||
const firstRel = toRelativeChildPath(childAbs, firstAbs);
|
||||
if (firstRel) {
|
||||
route.redirect = firstRel;
|
||||
}
|
||||
}
|
||||
return route;
|
||||
}
|
||||
|
||||
// 叶子页面
|
||||
return {
|
||||
path: relPath,
|
||||
name: `menu_${menu.id}`,
|
||||
meta,
|
||||
component: resolvePageComponent(menu),
|
||||
};
|
||||
}
|
||||
|
||||
// 递归转换嵌套菜单为嵌套路由
|
||||
export function convertMenusToRoutes(menus, parentPath = '') {
|
||||
if (!menus || menus.length === 0) return [];
|
||||
|
||||
return menus.map(menu => {
|
||||
// 拼接完整的路由路径(处理相对路径)
|
||||
const fullPath = menu.path ?
|
||||
(menu.path.startsWith('/') ? menu.path : `${parentPath}/${menu.path}`)
|
||||
return menus.map((menu) => {
|
||||
const fullPath = menu.path
|
||||
? menu.path.startsWith('/')
|
||||
? menu.path.replace(/\/+/g, '/')
|
||||
: `${(parentPath || '').replace(/\/$/, '')}/${menu.path}`.replace(/\/+/g, '/')
|
||||
: '';
|
||||
|
||||
const hasChildren = menu.children && menu.children.length > 0;
|
||||
const ownPage = hasPageComponent(menu);
|
||||
|
||||
const meta = {
|
||||
title: menu.title,
|
||||
icon: menu.icon,
|
||||
id: menu.id,
|
||||
componentPath: menu.component_path,
|
||||
};
|
||||
|
||||
// 顶层:有页面 + 有子菜单 -> EmptyLayout + 默认子路由 + 相对 path 子路由
|
||||
if (hasChildren && ownPage) {
|
||||
return {
|
||||
path: fullPath || menu.path || '',
|
||||
name: `menu_${menu.id}`,
|
||||
meta,
|
||||
component: () => import('@/views/layouts/EmptyLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: `menu_${menu.id}_index`,
|
||||
meta: { ...meta },
|
||||
component: resolvePageComponent(menu),
|
||||
},
|
||||
...convertNestedMenuChildren(menu.children, fullPath),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const route = {
|
||||
path: fullPath || menu.path || '',
|
||||
name: `menu_${menu.id}`,
|
||||
meta: {
|
||||
title: menu.title,
|
||||
icon: menu.icon,
|
||||
id: menu.id,
|
||||
componentPath: menu.component_path
|
||||
}
|
||||
meta,
|
||||
};
|
||||
|
||||
// 1. 处理组件加载
|
||||
if (menu.type === 4) {
|
||||
// 单页类型
|
||||
route.component = () => import('@/views/onepage/index.vue');
|
||||
} else if (menu.component_path && menu.component_path.trim() !== '') {
|
||||
// 正常页面
|
||||
route.component = createComponentLoader(menu.component_path);
|
||||
} else if (menu.children && menu.children.length > 0) {
|
||||
// 目录节点:必须给组件,否则父级无法渲染子级
|
||||
} else if (hasChildren) {
|
||||
route.component = () => import('@/views/layouts/EmptyLayout.vue');
|
||||
} else {
|
||||
// 异常:既没路径也没子菜单
|
||||
route.component = () => import('@/views/404/404.vue');
|
||||
}
|
||||
|
||||
// 2. 递归子路由(传递当前完整路径作为父路径)
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
route.children = convertMenusToRoutes(menu.children, fullPath);
|
||||
|
||||
// 目录节点添加重定向,防止点击父菜单页面空白
|
||||
const firstChild = menu.children[0];
|
||||
if (firstChild && firstChild.path) {
|
||||
// 计算第一个子路由的完整路径
|
||||
const childFullPath = firstChild.path.startsWith('/') ?
|
||||
firstChild.path :
|
||||
`${fullPath}/${firstChild.path}`;
|
||||
const childFullPath = firstChild.path.startsWith('/')
|
||||
? firstChild.path
|
||||
: `${fullPath}/${firstChild.path}`;
|
||||
route.redirect = childFullPath;
|
||||
}
|
||||
} else {
|
||||
route.component = () => import('@/views/404/404.vue');
|
||||
}
|
||||
|
||||
return route;
|
||||
|
||||
+23
-9
@@ -8,6 +8,11 @@ const staticMainChildren = [
|
||||
name: "userProfile",
|
||||
component: () => import("@/views/user/userProfile.vue"),
|
||||
meta: { requiresAuth: true, title: "用户中心" }
|
||||
},
|
||||
// 兼容拼写错误的路径重定向
|
||||
{
|
||||
path: "/apps/erp/dashborad",
|
||||
redirect: "/apps/erp/dashboard"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -19,19 +24,28 @@ const staticRoutes = [
|
||||
component: () => import("@/views/login/index.vue"),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: "/register",
|
||||
name: "Register",
|
||||
component: () => import("@/views/login/register.vue"),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: "/forget",
|
||||
name: "ForgetPassword",
|
||||
component: () => import("@/views/login/forget.vue"),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: "/home",
|
||||
name: "Home",
|
||||
component: () => import("@/views/home/index.vue"),
|
||||
meta: { requiresAuth: true, title: "系统导航", isStandalone: true }
|
||||
},
|
||||
// 兼容路径拼写错误:dashborad -> dashboard
|
||||
{
|
||||
path: "/",
|
||||
name: "Main",
|
||||
component: () => import("@/views/Main.vue"),
|
||||
redirect: "/dashboard",
|
||||
meta: { requiresAuth: true },
|
||||
children: staticMainChildren
|
||||
path: "/apps/erp/dashborad",
|
||||
redirect: "/apps/erp/dashboard"
|
||||
},
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
@@ -84,7 +98,7 @@ export async function loadAndAddDynamicRoutes() {
|
||||
console.error('加载动态路由失败:', error);
|
||||
dynamicRoutesAdded = true;
|
||||
routesLoadingPromise = null;
|
||||
return Promise.resolve();
|
||||
throw error;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -115,7 +129,6 @@ function addDynamicRoutes(menus) {
|
||||
});
|
||||
|
||||
dynamicRoutesAdded = true;
|
||||
// console.log('动态路由添加完成:', router.getRoutes().map(r => r.path));
|
||||
}
|
||||
|
||||
function findRouteByName(routes, routeName) {
|
||||
@@ -150,8 +163,9 @@ function findFirstValidRoute(routes) {
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const token = localStorage.getItem("token");
|
||||
const publicPaths = ["/login", "/register", "/forget"];
|
||||
|
||||
if (to.path === "/login") {
|
||||
if (publicPaths.includes(to.path)) {
|
||||
if (token) {
|
||||
if (!dynamicRoutesAdded) {
|
||||
await loadAndAddDynamicRoutes();
|
||||
|
||||
@@ -7,6 +7,7 @@ const defaultUser = {
|
||||
account: '',
|
||||
name: '',
|
||||
group_id: '',
|
||||
type: 'backend',
|
||||
avatar: ''
|
||||
}
|
||||
|
||||
@@ -40,6 +41,8 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
account: userInfo.account || '',
|
||||
name: userInfo.name || '',
|
||||
group_id: userInfo.group_id || '',
|
||||
type: 'backend',
|
||||
tid: userInfo.tid || '',
|
||||
avatar: userInfo.avatar || ''
|
||||
}
|
||||
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { getDictItemsByCode } from '@/api/dict'
|
||||
|
||||
/**
|
||||
* 字典 Store
|
||||
*
|
||||
* 用于全局管理系统字典数据
|
||||
* 缓存字典数据避免重复请求,提高性能
|
||||
*
|
||||
* 使用示例:
|
||||
* const dictStore = useDictStore()
|
||||
* const statusDict = await dictStore.getDictItems('user_status')
|
||||
* const roleDict = dictStore.getDictItemsSync('user_role') // 已加载则同步返回
|
||||
*/
|
||||
export const useDictStore = defineStore('dict', () => {
|
||||
// 字典缓存:{ dictCode: [...items] }
|
||||
const dictCache = ref({})
|
||||
|
||||
// 正在加载的字典代码集合
|
||||
const loadingCodes = ref(new Set())
|
||||
|
||||
/**
|
||||
* 获取字典项(异步)
|
||||
* @param {string} code - 字典编码,如 'user_status'
|
||||
* @returns {Promise<Array>} 字典项数组
|
||||
*/
|
||||
async function getDictItems(code) {
|
||||
// 如果缓存中已有,直接返回
|
||||
if (dictCache.value[code]) {
|
||||
return dictCache.value[code]
|
||||
}
|
||||
|
||||
// 避免重复请求:如果已在加载中,等待
|
||||
if (loadingCodes.value.has(code)) {
|
||||
// 等待加载完成(最多 5 秒)
|
||||
return await new Promise((resolve) => {
|
||||
let count = 0
|
||||
const timer = setInterval(() => {
|
||||
if (dictCache.value[code]) {
|
||||
clearInterval(timer)
|
||||
resolve(dictCache.value[code])
|
||||
}
|
||||
count++
|
||||
if (count > 50) {
|
||||
clearInterval(timer)
|
||||
resolve([])
|
||||
}
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
// 标记为正在加载
|
||||
loadingCodes.value.add(code)
|
||||
|
||||
try {
|
||||
const res = await getDictItemsByCode(code)
|
||||
let items = []
|
||||
|
||||
// 兼容不同的 API 响应格式
|
||||
if (res?.data && Array.isArray(res.data)) {
|
||||
items = res.data
|
||||
} else if (Array.isArray(res)) {
|
||||
items = res
|
||||
} else if (res?.data?.data && Array.isArray(res.data.data)) {
|
||||
items = res.data.data
|
||||
}
|
||||
|
||||
// 缓存字典项
|
||||
dictCache.value[code] = items
|
||||
// console.log(`✅ 字典 [${code}] 已加载,共 ${items.length} 项`)
|
||||
|
||||
return items
|
||||
} catch (error) {
|
||||
console.error(`❌ 加载字典 [${code}] 失败:`, error)
|
||||
dictCache.value[code] = []
|
||||
return []
|
||||
} finally {
|
||||
// 移除加载标记
|
||||
loadingCodes.value.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步获取字典项(如果已缓存)
|
||||
* @param {string} code - 字典编码
|
||||
* @returns {Array} 字典项数组,未缓存则返回空数组
|
||||
*/
|
||||
function getDictItemsSync(code) {
|
||||
return dictCache.value[code] || []
|
||||
}
|
||||
|
||||
/**
|
||||
* 预加载字典(在应用启动时调用)
|
||||
* @param {Array<string>} codes - 字典编码数组
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function preloadDicts(codes) {
|
||||
const promises = codes.map(code => getDictItems(code))
|
||||
await Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典编码和值获取标签
|
||||
* @param {string} code - 字典编码
|
||||
* @param {any} value - 字典值
|
||||
* @returns {string} 字典标签
|
||||
*/
|
||||
function getDictLabel(code, value) {
|
||||
const items = getDictItemsSync(code)
|
||||
if (!items.length) {
|
||||
console.warn(`⚠️ 字典 [${code}] 未加载,无法获取标签`)
|
||||
return String(value)
|
||||
}
|
||||
|
||||
const item = items.find(i => String(i.dict_value) === String(value) || i.dict_value === value)
|
||||
return item ? item.dict_label : String(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典编码和标签获取值
|
||||
* @param {string} code - 字典编码
|
||||
* @param {string} label - 字典标签
|
||||
* @returns {any} 字典值
|
||||
*/
|
||||
function getDictValue(code, label) {
|
||||
const items = getDictItemsSync(code)
|
||||
if (!items.length) {
|
||||
console.warn(`⚠️ 字典 [${code}] 未加载,无法获取值`)
|
||||
return null
|
||||
}
|
||||
|
||||
const item = items.find(i => i.dict_label === label)
|
||||
return item ? item.dict_value : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空缓存
|
||||
* @param {string} code - 字典编码,不指定则清空所有
|
||||
*/
|
||||
function clearCache(code) {
|
||||
if (code) {
|
||||
delete dictCache.value[code]
|
||||
// console.log(`✅ 已清除字典 [${code}] 缓存`)
|
||||
} else {
|
||||
dictCache.value = {}
|
||||
// console.log(`✅ 已清除所有字典缓存`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新字典
|
||||
* @param {string} code - 字典编码
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async function refreshDict(code) {
|
||||
clearCache(code)
|
||||
return getDictItems(code)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已缓存的字典
|
||||
* @returns {Object}
|
||||
*/
|
||||
const allDicts = computed(() => dictCache.value)
|
||||
|
||||
return {
|
||||
dictCache,
|
||||
loadingCodes,
|
||||
getDictItems,
|
||||
getDictItemsSync,
|
||||
preloadDicts,
|
||||
getDictLabel,
|
||||
getDictValue,
|
||||
clearCache,
|
||||
refreshDict,
|
||||
allDicts,
|
||||
}
|
||||
})
|
||||
+9
-6
@@ -21,7 +21,7 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||
const loginType = userInfo.type || 'user';
|
||||
const roleId = userInfo.role || 0;
|
||||
const roleId = userInfo.group_id || 0;
|
||||
return `menu_cache_${loginType}_${roleId}`;
|
||||
} catch (e) {
|
||||
return 'menu_cache_default';
|
||||
@@ -92,10 +92,6 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
const cachedMenus = loadFromCache();
|
||||
if (cachedMenus && cachedMenus.length > 0) {
|
||||
menus.value = cachedMenus;
|
||||
// 注释掉后台更新缓存的逻辑,避免重复请求
|
||||
// fetchMenus(true).catch(err => {
|
||||
// console.warn('后台更新菜单失败:', err);
|
||||
// });
|
||||
return Promise.resolve(cachedMenus);
|
||||
}
|
||||
}
|
||||
@@ -113,7 +109,7 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
try {
|
||||
const userInfo = getUserInfo();
|
||||
const loginType = userInfo.type || 'user';
|
||||
const roleId = userInfo.role || 0;
|
||||
const roleId = userInfo.group_id || 0;
|
||||
|
||||
let res;
|
||||
|
||||
@@ -175,6 +171,13 @@ export const useMenuStore = defineStore('menu', () => {
|
||||
stack: err.stack
|
||||
});
|
||||
|
||||
// 如果是 token 无效错误,不使用缓存,直接抛出
|
||||
if (err.message === 'token无效' || err.response?.status === 401) {
|
||||
clearCache();
|
||||
menus.value = [];
|
||||
throw err;
|
||||
}
|
||||
|
||||
// 如果出错,尝试使用缓存数据
|
||||
const cachedMenus = loadFromCache();
|
||||
if (cachedMenus && cachedMenus.length > 0) {
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module 'vue-cropper' {
|
||||
import { Component } from 'vue'
|
||||
export const VueCropper: Component
|
||||
}
|
||||
@@ -15,15 +15,20 @@ Object.keys(viewsModules).forEach(relativePath => {
|
||||
|
||||
// 统一去掉扩展名进行存储,方便各种格式匹配
|
||||
const baseNoExt = relativePath.replace('../views/', '').replace('.vue', '');
|
||||
const baseWithExt = relativePath.replace('../views/', '');
|
||||
|
||||
// 1. 存储标准路径
|
||||
pathMap.set(relativePath, viewsModules[relativePath]);
|
||||
// 2. 存储 @/views 路径
|
||||
pathMap.set(relativePath.replace('../views', '@/views'), viewsModules[relativePath]);
|
||||
// 3. 存储 /system/users 格式
|
||||
// 3. 存储 /system/users 格式(不带扩展名)
|
||||
pathMap.set(`/${baseNoExt}`, viewsModules[relativePath]);
|
||||
// 4. 存储 system/users 格式
|
||||
// 4. 存储 system/users 格式(不带扩展名)
|
||||
pathMap.set(baseNoExt, viewsModules[relativePath]);
|
||||
// 5. 存储 /system/users.vue 格式(带扩展名)
|
||||
pathMap.set(`/${baseWithExt}`, viewsModules[relativePath]);
|
||||
// 6. 存储 system/users.vue 格式(带扩展名)
|
||||
pathMap.set(baseWithExt, viewsModules[relativePath]);
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+6
-21
@@ -1,8 +1,11 @@
|
||||
import axios from 'axios';
|
||||
|
||||
// 获取API基础URL,添加调试信息
|
||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
baseURL: apiBaseURL,
|
||||
timeout: 10000,
|
||||
withCredentials: false // JWT 不需要 Cookie
|
||||
});
|
||||
@@ -15,22 +18,6 @@ service.interceptors.request.use(
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 禁止 GET 请求缓存:添加时间戳参数到 query string
|
||||
if (config.method === 'get') {
|
||||
config.params = {
|
||||
...config.params,
|
||||
_t: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// POST/PUT/PATCH 请求也添加时间戳到 query string 防止缓存
|
||||
if (['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
||||
config.params = {
|
||||
...config.params,
|
||||
_t: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// 对于有 body 的请求(POST、PUT、PATCH),确保设置 Content-Type
|
||||
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
||||
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
||||
@@ -53,16 +40,14 @@ service.interceptors.response.use(
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 处理未授权的错误 - 清除token并跳转登录页
|
||||
console.error('未授权,请重新登录');
|
||||
localStorage.removeItem('token');
|
||||
// 避免循环导入,直接使用路由
|
||||
localStorage.removeItem('userInfo');
|
||||
if (window.location.hash !== '#/login') {
|
||||
window.location.href = '#/login';
|
||||
}
|
||||
break;
|
||||
return Promise.reject(new Error('token无效'));
|
||||
case 404:
|
||||
// 处理资源不存在的错误
|
||||
console.error('请求的资源不存在');
|
||||
break;
|
||||
default:
|
||||
|
||||
+15
-6
@@ -4,13 +4,22 @@
|
||||
<div class="not-found-big">404</div>
|
||||
<div class="not-found-message">页面未找到</div>
|
||||
<div class="not-found-desc">很抱歉,您访问的页面不存在或已被删除。</div>
|
||||
<router-link to="/" class="back-home-btn">返回首页</router-link>
|
||||
<a href="javascript:void(0)" class="back-home-btn" @click.prevent="backToHome">
|
||||
返回首页
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 这里可以根据需要添加自定义逻辑
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const backToHome = () => {
|
||||
// 使用 /home 作为首页(Hash模式下的系统导航页)
|
||||
router.push("/home");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -26,9 +35,9 @@
|
||||
background: #fff;
|
||||
padding: 60px 48px 40px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.10);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
animation: float-in 0.8s cubic-bezier(.7,.13,.45,.81) both;
|
||||
animation: float-in 0.8s cubic-bezier(0.7, 0.13, 0.45, 0.81) both;
|
||||
}
|
||||
|
||||
.not-found-big {
|
||||
@@ -64,7 +73,7 @@
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(252,92,125,0.07);
|
||||
box-shadow: 0 2px 8px rgba(252, 92, 125, 0.07);
|
||||
}
|
||||
|
||||
.back-home-btn:hover {
|
||||
|
||||
@@ -132,7 +132,7 @@ onUnmounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped lang="less">
|
||||
.statistics-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
title="绑定父母"
|
||||
width="500px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="80px">
|
||||
|
||||
<el-form-item label="父亲">
|
||||
<div class="parent-selector">
|
||||
<el-select
|
||||
v-model="formData.father_id"
|
||||
placeholder="请输入手机号或者账号"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="(query) => handleSearchUser(query, 1)"
|
||||
:loading="searchLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #default>
|
||||
<div class="search-tip">支持账号/手机号搜索</div>
|
||||
<el-option
|
||||
v-for="user in maleUsers"
|
||||
:key="user.id"
|
||||
:label="user.name || user.account"
|
||||
:value="user.id"
|
||||
>
|
||||
<div class="user-option">
|
||||
<span class="user-name">{{ user.name || user.account }}</span>
|
||||
<span class="user-phone">{{ user.phone }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</template>
|
||||
</el-select>
|
||||
<el-button link type="primary" @click="handleSearchUser('', 1)">
|
||||
<i class="fa-solid fa-refresh"></i> 刷新列表
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="母亲">
|
||||
<div class="parent-selector">
|
||||
<el-select
|
||||
v-model="formData.mother_id"
|
||||
placeholder="请输入手机号或者账号"
|
||||
clearable
|
||||
filterable
|
||||
remote
|
||||
reserve-keyword
|
||||
:remote-method="(query) => handleSearchUser(query, 2)"
|
||||
:loading="searchLoading"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #default>
|
||||
<div class="search-tip">支持账号/手机号搜索</div>
|
||||
<el-option
|
||||
v-for="user in femaleUsers"
|
||||
:key="user.id"
|
||||
:label="user.name || user.account"
|
||||
:value="user.id"
|
||||
>
|
||||
<div class="user-option">
|
||||
<span class="user-name">{{ user.name || user.account }}</span>
|
||||
<span class="user-phone">{{ user.phone }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</template>
|
||||
</el-select>
|
||||
<el-button link type="primary" @click="handleSearchUser('', 2)">
|
||||
<i class="fa-solid fa-refresh"></i> 刷新列表
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch, computed } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { bindParent, getParents, getUserList } from '@/api/babyhealth';
|
||||
|
||||
interface Baby {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
name: string;
|
||||
account: string;
|
||||
phone: string;
|
||||
sex: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
babyData?: Baby | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const loading = ref(false);
|
||||
const searchLoading = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const users = ref<User[]>([]);
|
||||
const allUsers = ref<User[]>([]);
|
||||
const parents = ref<User[]>([]);
|
||||
|
||||
const formData = reactive<{
|
||||
father_id: number | null;
|
||||
mother_id: number | null;
|
||||
}>({
|
||||
father_id: null,
|
||||
mother_id: null,
|
||||
});
|
||||
|
||||
const formRules: FormRules = {};
|
||||
|
||||
// 过滤男性用户
|
||||
const maleUsers = computed(() => {
|
||||
return users.value.filter(user => user.sex === 1);
|
||||
});
|
||||
|
||||
// 过滤女性用户
|
||||
const femaleUsers = computed(() => {
|
||||
return users.value.filter(user => user.sex === 2);
|
||||
});
|
||||
|
||||
// 拼接接口路径
|
||||
const getEnvUrl = (path: string) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.father_id = null;
|
||||
formData.mother_id = null;
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (val) => {
|
||||
dialogVisible.value = val;
|
||||
if (val) {
|
||||
fetchUsers();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听对话框关闭,重置搜索结果
|
||||
watch(dialogVisible, (val) => {
|
||||
if (!val) {
|
||||
users.value = parents.value;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 dialogVisible 变化
|
||||
watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val);
|
||||
});
|
||||
|
||||
// 获取用户列表和已有父母数据
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
// 获取所有用户列表
|
||||
const res = await getUserList();
|
||||
if (res.code === 200) {
|
||||
allUsers.value = res.data || [];
|
||||
users.value = res.data || [];
|
||||
}
|
||||
|
||||
// 获取当前宝贝已绑定的父母数据
|
||||
if (props.babyData?.id) {
|
||||
const parentsRes = await getParents(props.babyData.id);
|
||||
if (parentsRes.code === 200 && parentsRes.data) {
|
||||
formData.father_id = parentsRes.data.father_id || null;
|
||||
formData.mother_id = parentsRes.data.mother_id || null;
|
||||
|
||||
// 如果已绑定父亲但不在用户列表中,添加进去以便显示名称
|
||||
if (parentsRes.data.father_id && parentsRes.data.father) {
|
||||
const fatherExists = allUsers.value.some(u => u.id === parentsRes.data.father_id);
|
||||
if (!fatherExists) {
|
||||
allUsers.value.push({
|
||||
id: parentsRes.data.father_id,
|
||||
name: parentsRes.data.father,
|
||||
account: '',
|
||||
phone: '',
|
||||
sex: 1
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已绑定母亲但不在用户列表中,添加进去以便显示名称
|
||||
if (parentsRes.data.mother_id && parentsRes.data.mother) {
|
||||
const motherExists = allUsers.value.some(u => u.id === parentsRes.data.mother_id);
|
||||
if (!motherExists) {
|
||||
allUsers.value.push({
|
||||
id: parentsRes.data.mother_id,
|
||||
name: parentsRes.data.mother,
|
||||
account: '',
|
||||
phone: '',
|
||||
sex: 2
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户列表
|
||||
users.value = allUsers.value;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
ElMessage.error('获取数据失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索用户
|
||||
const handleSearchUser = async (query: string, sex: number) => {
|
||||
searchLoading.value = true;
|
||||
try {
|
||||
if (!query) {
|
||||
// 如果查询为空,显示所有对应性别的用户
|
||||
users.value = allUsers.value.filter(user => user.sex === sex);
|
||||
} else {
|
||||
// 通过账号或手机号搜索
|
||||
const filteredUsers = allUsers.value.filter(user => {
|
||||
const isTargetSex = user.sex === sex;
|
||||
const matchAccount = user.account?.toLowerCase().includes(query.toLowerCase());
|
||||
const matchPhone = user.phone?.includes(query);
|
||||
const matchName = user.name?.toLowerCase().includes(query.toLowerCase());
|
||||
return isTargetSex && (matchAccount || matchPhone || matchName);
|
||||
});
|
||||
users.value = filteredUsers;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索用户失败:', error);
|
||||
ElMessage.error('搜索用户失败');
|
||||
} finally {
|
||||
searchLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
if (!props.babyData?.id) {
|
||||
ElMessage.error('宝贝信息不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const submitData: Record<string, any> = {};
|
||||
if (formData.father_id) {
|
||||
submitData.father_id = formData.father_id;
|
||||
}
|
||||
if (formData.mother_id) {
|
||||
submitData.mother_id = formData.mother_id;
|
||||
}
|
||||
|
||||
const res = await bindParent(props.babyData.id, submitData);
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('绑定成功');
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
emit('success');
|
||||
} else {
|
||||
ElMessage.error(res.msg || '绑定失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('绑定失败:', error);
|
||||
ElMessage.error('绑定失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.baby-info-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 8px;
|
||||
|
||||
.baby-avatar-mini {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.baby-info-text {
|
||||
flex: 1;
|
||||
|
||||
.baby-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.baby-nickname {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.parent-selector {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.el-select {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.search-tip {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
background: var(--el-fill-color-lighter);
|
||||
}
|
||||
|
||||
.user-option {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.user-name {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
margin-left: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,426 +0,0 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑宝贝' : '添加宝贝'"
|
||||
width="600px"
|
||||
:close-on-click-modal="false"
|
||||
@close="handleClose"
|
||||
>
|
||||
<div class="dialog-content">
|
||||
<!-- 头像上传区域 -->
|
||||
<div class="avatar-section">
|
||||
<el-upload
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:http-request="uploadFile"
|
||||
class="avatar-uploader"
|
||||
>
|
||||
<el-image
|
||||
v-if="formData.avatar"
|
||||
:src="getEnvUrl(formData.avatar)"
|
||||
class="avatar-preview"
|
||||
fit="cover"
|
||||
:preview-src-list="[getEnvUrl(formData.avatar)]"
|
||||
:initial-index="0"
|
||||
/>
|
||||
<div v-else class="avatar-placeholder">
|
||||
<el-icon class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
<span class="avatar-tip">点击上传头像</span>
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
|
||||
<!-- 表单区域 -->
|
||||
<el-form ref="editFormRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入姓名" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="小名" prop="nickname">
|
||||
<el-input v-model="formData.nickname" placeholder="请输入小名" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="性别" prop="sex">
|
||||
<el-radio-group v-model="formData.sex">
|
||||
<el-radio :value="1">男</el-radio>
|
||||
<el-radio :value="2">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="出生日期" prop="birth">
|
||||
<el-date-picker
|
||||
v-model="formData.birth"
|
||||
type="date"
|
||||
placeholder="选择出生日期"
|
||||
style="width: 100%"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="身高" prop="height">
|
||||
<el-input
|
||||
v-model="formData.height"
|
||||
:min="0"
|
||||
:max="200"
|
||||
:precision="1"
|
||||
:step="0.5"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #append>CM</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="体重" prop="weight">
|
||||
<el-input
|
||||
v-model="formData.weight"
|
||||
:min="0"
|
||||
:max="50"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
controls-position="right"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #append>KG</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="1">正常</el-radio>
|
||||
<el-radio :value="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import { createBaby, editBaby } from '@/api/babyhealth';
|
||||
import { uploadAvatar } from '@/api/file';
|
||||
|
||||
interface BabyFormData {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
sex: number;
|
||||
birth: string;
|
||||
height: number;
|
||||
weight: number;
|
||||
avatar: string;
|
||||
status: number;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean;
|
||||
editData?: BabyFormData;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:visible': [value: boolean];
|
||||
success: [];
|
||||
}>();
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
const loading = ref(false);
|
||||
const editFormRef = ref<FormInstance>();
|
||||
const fileList = ref<any[]>([]);
|
||||
|
||||
const formData = reactive<BabyFormData>({
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
sex: 1,
|
||||
birth: '',
|
||||
height: 0,
|
||||
weight: 0,
|
||||
avatar: '',
|
||||
status: 1
|
||||
});
|
||||
|
||||
const isEdit = ref(false);
|
||||
|
||||
const formRules: FormRules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
|
||||
birth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
|
||||
};
|
||||
|
||||
// 拼接接口路径
|
||||
const getEnvUrl = (path: string) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: 0,
|
||||
name: '',
|
||||
nickname: '',
|
||||
sex: 1,
|
||||
birth: '',
|
||||
height: 0,
|
||||
weight: 0,
|
||||
avatar: '',
|
||||
status: 1
|
||||
});
|
||||
fileList.value = [];
|
||||
};
|
||||
|
||||
// 关闭对话框
|
||||
const handleClose = () => {
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(() => props.visible, (val) => {
|
||||
dialogVisible.value = val;
|
||||
});
|
||||
|
||||
// 监听 dialogVisible 变化
|
||||
watch(dialogVisible, (val) => {
|
||||
emit('update:visible', val);
|
||||
});
|
||||
|
||||
// 监听 editData 变化
|
||||
watch(() => props.editData, (data) => {
|
||||
if (data) {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, data);
|
||||
if (data.avatar) {
|
||||
fileList.value = [
|
||||
{
|
||||
name: 'avatar',
|
||||
url: data.avatar
|
||||
}
|
||||
];
|
||||
} else {
|
||||
fileList.value = [];
|
||||
}
|
||||
} else {
|
||||
isEdit.value = false;
|
||||
resetForm();
|
||||
}
|
||||
}, { immediate: true });
|
||||
|
||||
// 头像上传前校验
|
||||
const beforeAvatarUpload = (file: any) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('仅支持图片格式');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('图片大小不能超过5MB');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 上传文件到服务器
|
||||
const uploadFile = async (options: any) => {
|
||||
const { file } = options;
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('file', file);
|
||||
|
||||
try {
|
||||
// 使用uploadAvatar接口上传文件
|
||||
const response = await uploadAvatar(uploadFormData, { cate: 'baby_avatar' });
|
||||
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
// 更新宝贝头像
|
||||
const avatarUrl = response.data.url || response.data.path || '';
|
||||
formData.avatar = avatarUrl;
|
||||
|
||||
if (response.code === 201) {
|
||||
ElMessage.info("文件已存在,使用已有图片");
|
||||
} else {
|
||||
ElMessage.success("头像上传成功");
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(response.msg || "上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
ElMessage.error("上传失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 上传相关
|
||||
const handleUploadChange = (file: any) => {
|
||||
if (file.raw) {
|
||||
const isImage = file.raw.type.startsWith('image/');
|
||||
const isLt5M = file.raw.size / 1024 / 1024 < 5;
|
||||
if (!isImage || !isLt5M) {
|
||||
fileList.value = [];
|
||||
ElMessage.error(isImage ? '图片大小不能超过5MB' : '仅支持图片格式');
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = (file: any) => {
|
||||
fileList.value = [];
|
||||
formData.avatar = '';
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!editFormRef.value) return;
|
||||
|
||||
await editFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 如果有新上传的图片,先上传
|
||||
if (fileList.value.length > 0 && fileList.value[0].raw) {
|
||||
const uploadFormData = new FormData();
|
||||
uploadFormData.append('file', fileList.value[0].raw);
|
||||
uploadFormData.append('cate', 'baby');
|
||||
|
||||
const uploadRes = await uploadAvatar(uploadFormData);
|
||||
if ((uploadRes.code === 200 || uploadRes.code === 201) && uploadRes.data?.url) {
|
||||
formData.avatar = uploadRes.data.url;
|
||||
} else {
|
||||
ElMessage.error('图片上传失败');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let res;
|
||||
if (formData.id) {
|
||||
// 编辑 - 使用普通对象,PUT请求
|
||||
const submitData: Record<string, any> = {};
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key !== 'id' && formData[key as keyof BabyFormData] !== undefined && formData[key as keyof BabyFormData] !== null) {
|
||||
submitData[key] = formData[key as keyof BabyFormData];
|
||||
}
|
||||
});
|
||||
res = await editBaby(formData.id, submitData);
|
||||
} else {
|
||||
// 新增 - 使用 FormData
|
||||
const submitData = new FormData();
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key !== 'id' && formData[key as keyof BabyFormData] !== undefined && formData[key as keyof BabyFormData] !== null) {
|
||||
submitData.append(key, String(formData[key as keyof BabyFormData]));
|
||||
}
|
||||
});
|
||||
res = await createBaby(submitData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(formData.id ? '编辑成功' : '添加成功');
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
emit('success'); // 通知父组件刷新列表
|
||||
} else {
|
||||
ElMessage.error(res.msg || (formData.id ? '编辑失败' : '添加失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
ElMessage.error('提交失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
:deep(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 2px dashed var(--el-border-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--el-fill-color-light);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 32px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.avatar-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-upload-list__item-thumbnail) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
@@ -1,400 +0,0 @@
|
||||
<template>
|
||||
<div class="babys-container">
|
||||
<div class="header-section">
|
||||
<h2 class="page-title">宝贝管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加宝贝
|
||||
</el-button>
|
||||
<el-button @click="handleRefresh">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div v-loading="loading" class="babys-grid">
|
||||
<div v-for="baby in babyList" :key="baby.id" class="baby-card">
|
||||
<div class="baby-avatar">
|
||||
<img v-if="baby.avatar" :src="getEnvUrl(baby.avatar)" :alt="baby.name" />
|
||||
<div v-else class="avatar-placeholder">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="baby-content">
|
||||
<div class="names">
|
||||
<h3 class="baby-name" :class="'sex-'+baby.sex">
|
||||
<i class="fa-solid fa-mars"></i> {{ baby.name }}
|
||||
</h3>
|
||||
<div class="birth">
|
||||
<el-tag><i class="fa-solid fa-cake-candles"></i> {{ formatDate(baby.birth) }}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="baby-info">
|
||||
<div class="info-left">
|
||||
<div class="info-item">
|
||||
<span><strong>父亲:</strong> {{ baby.father || '未绑定' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span><strong>母亲:</strong> {{ baby.mather || '未绑定' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-right">
|
||||
<div class="info-item">
|
||||
<span><strong>身高:</strong> {{ baby.height || 0 }}cm</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span><strong>体重:</strong> {{ baby.weight || 0 }}kg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="baby-footer">
|
||||
<el-tag :type="baby.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ baby.status === 1 ? '正常' : '禁用' }}
|
||||
</el-tag>
|
||||
<div class="baby-actions">
|
||||
<el-button size="small" type="primary" link @click="handleBindParents(baby)">
|
||||
<i class="fa-solid fa-link"></i>
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleView(baby)">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</el-button>
|
||||
<el-button size="small" type="primary" link @click="handleEdit(baby)">
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" link @click="handleDelete(baby)">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="babyList.length === 0 && !loading" class="empty-state">
|
||||
<el-empty description="暂无宝贝数据" :image-size="120" />
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<BabyEdit
|
||||
v-model:visible="editDialogVisible"
|
||||
:editData="currentBaby"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
|
||||
<!-- 绑定父母对话框 -->
|
||||
<BindParents
|
||||
v-model:visible="bindParentsVisible"
|
||||
:babyData="currentBaby"
|
||||
@success="handleBindParentsSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import BabyEdit from './components/edit.vue';
|
||||
import BindParents from './components/bindParents.vue';
|
||||
import { getBabyList, deleteBaby } from '@/api/babyhealth';
|
||||
|
||||
interface Baby {
|
||||
id: number;
|
||||
name: string;
|
||||
nickname: string;
|
||||
sex: number;
|
||||
birth: string;
|
||||
height: number;
|
||||
weight: number;
|
||||
avatar: string;
|
||||
status: number;
|
||||
create_time: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const babyList = ref<Baby[]>([]);
|
||||
const editDialogVisible = ref(false);
|
||||
const bindParentsVisible = ref(false);
|
||||
const currentBaby = ref<Baby | null>(null);
|
||||
|
||||
// 拼接接口路径
|
||||
const getEnvUrl = (path: string) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-';
|
||||
return date.substring(0, 10);
|
||||
};
|
||||
|
||||
// 格式化性别
|
||||
const formatGender = (gender: number) => {
|
||||
const map: Record<number, string> = {
|
||||
1: '男',
|
||||
2: '女'
|
||||
};
|
||||
return map[gender] || '未知';
|
||||
};
|
||||
|
||||
// 刷新列表
|
||||
const handleRefresh = () => {
|
||||
fetchBabyList();
|
||||
};
|
||||
|
||||
// 添加宝贝
|
||||
const handleAdd = () => {
|
||||
currentBaby.value = null;
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleView = (row: Baby) => {
|
||||
ElMessage.info('详情功能待实现');
|
||||
};
|
||||
|
||||
// 编辑宝贝
|
||||
const handleEdit = (row: Baby) => {
|
||||
currentBaby.value = row;
|
||||
editDialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除宝贝
|
||||
const handleDelete = (row: Baby) => {
|
||||
ElMessageBox.confirm(`确定要删除宝贝「${row.name}」吗?删除后不可恢复。`, '警告', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const res = await deleteBaby(row.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功');
|
||||
fetchBabyList();
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
ElMessage.error('删除失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
// 绑定父母
|
||||
const handleBindParents = (row: Baby) => {
|
||||
currentBaby.value = row;
|
||||
bindParentsVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑/添加成功回调
|
||||
const handleEditSuccess = () => {
|
||||
fetchBabyList();
|
||||
};
|
||||
|
||||
// 绑定父母成功回调
|
||||
const handleBindParentsSuccess = () => {
|
||||
fetchBabyList();
|
||||
};
|
||||
|
||||
// 获取宝贝列表
|
||||
const fetchBabyList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getBabyList();
|
||||
// console.log('获取宝贝列表响应:', res);
|
||||
if (res.code === 200) {
|
||||
babyList.value = res.data || [];
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取宝贝列表失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取宝贝列表失败:', error);
|
||||
ElMessage.error('获取宝贝列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchBabyList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.babys-container {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
min-height: 100vh;
|
||||
|
||||
.header-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.babys-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
|
||||
.baby-card {
|
||||
background: var(--el-bg-color);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.3s;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
transform: translateY(-4px);
|
||||
border-color: var(--el-color-primary-light-7);
|
||||
}
|
||||
|
||||
.baby-avatar {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.fa-solid {
|
||||
font-size: 40px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.baby-content {
|
||||
padding: 20px;
|
||||
|
||||
.names{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.baby-name {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
// color: var(--el-text-color-primary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.baby-nickname {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.baby-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.info-left,
|
||||
.info-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
.fa-solid {
|
||||
font-size: 16px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.baby-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.baby-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 80px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sex-0,.sex-1,.sex-2{
|
||||
|
||||
}
|
||||
|
||||
.sex-0{
|
||||
|
||||
}
|
||||
.sex-1{
|
||||
|
||||
color: #409eff;
|
||||
}
|
||||
.sex-2{
|
||||
color: #ff69b4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,196 +0,0 @@
|
||||
<template>
|
||||
<div class="statistics-container">
|
||||
<el-row :gutter="20" class="data-overview">
|
||||
<el-col :span="6" v-for="item in summaryData" :key="item.title">
|
||||
<el-card shadow="hover" class="data-card">
|
||||
<div class="card-content">
|
||||
<div class="icon-box" :style="{ backgroundColor: item.color }">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div class="text-box">
|
||||
<div class="title">{{ item.title }}</div>
|
||||
<div class="value">{{ item.value.toLocaleString() }}</div>
|
||||
<div class="trend" :class="item.isUp ? 'up' : 'down'">
|
||||
{{ item.isUp ? '↑' : '↓' }} {{ item.percentage }}%
|
||||
<span>较上月</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" class="charts-row">
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" header="用户增长趋势">
|
||||
<div ref="lineChartRef" class="chart-box"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" header="用户等级分布">
|
||||
<div ref="pieChartRef" class="chart-box"></div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, shallowRef } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { User, Pointer, Connection, Histogram } from '@element-plus/icons-vue';
|
||||
|
||||
// --- 类型定义 ---
|
||||
interface SummaryItem {
|
||||
title: string;
|
||||
value: number;
|
||||
icon: any;
|
||||
color: string;
|
||||
percentage: number;
|
||||
isUp: boolean;
|
||||
}
|
||||
|
||||
// --- 响应式数据 ---
|
||||
const lineChartRef = ref<HTMLElement | null>(null);
|
||||
const pieChartRef = ref<HTMLElement | null>(null);
|
||||
const lineChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const pieChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
|
||||
const summaryData = ref<SummaryItem[]>([
|
||||
{ title: '总用户数', value: 12840, icon: User, color: '#3973FF', percentage: 12, isUp: true },
|
||||
{ title: '今日新增', value: 156, icon: Pointer, color: '#67C23A', percentage: 5, isUp: true },
|
||||
{ title: '活跃用户', value: 3420, icon: Connection, color: '#E6A23C', percentage: 2, isUp: false },
|
||||
{ title: '留存率', value: 85, icon: Histogram, color: '#F56C6C', percentage: 1, isUp: true },
|
||||
]);
|
||||
|
||||
// --- 初始化图表 ---
|
||||
const initCharts = () => {
|
||||
// 折线图配置
|
||||
if (lineChartRef.value) {
|
||||
lineChartInstance.value = echarts.init(lineChartRef.value);
|
||||
lineChartInstance.value.setOption({
|
||||
tooltip: { trigger: 'axis' },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
},
|
||||
yAxis: { type: 'value' },
|
||||
series: [
|
||||
{
|
||||
name: '新增用户',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: [120, 132, 101, 134, 90, 230, 210],
|
||||
areaStyle: { opacity: 0.3 },
|
||||
itemStyle: { color: '#3973FF' }
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// 饼图配置
|
||||
if (pieChartRef.value) {
|
||||
pieChartInstance.value = echarts.init(pieChartRef.value);
|
||||
pieChartInstance.value.setOption({
|
||||
tooltip: { trigger: 'item' },
|
||||
legend: { bottom: '0%', left: 'center' },
|
||||
series: [
|
||||
{
|
||||
name: '等级分布',
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false },
|
||||
data: [
|
||||
{ value: 1048, name: '普通用户' },
|
||||
{ value: 735, name: 'VIP会员' },
|
||||
{ value: 580, name: '超级管理员' },
|
||||
{ value: 484, name: '运营人员' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// --- 生命周期与自适应 ---
|
||||
const handleResize = () => {
|
||||
lineChartInstance.value?.resize();
|
||||
pieChartInstance.value?.resize();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initCharts();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.statistics-container {
|
||||
padding: 20px;
|
||||
min-height: 100vh;
|
||||
|
||||
.data-overview {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.data-card {
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.icon-box {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 15px;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.text-box {
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
.value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 4px 0;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
.trend {
|
||||
font-size: 12px;
|
||||
&.up { color: #67c23a; }
|
||||
&.down { color: #f56c6c; }
|
||||
span { color: #909399; margin-left: 4px; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charts-row {
|
||||
.chart-box {
|
||||
height: 350px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深度修改 Element Plus 卡片头部样式
|
||||
:deep(.el-card__header) {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,198 +0,0 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="400px" @close="handleClose">
|
||||
<el-form :model="form">
|
||||
<!-- 用户账号(只读) -->
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="form.username" disabled />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 新密码 -->
|
||||
<el-form-item label="新密码">
|
||||
<el-input
|
||||
v-model="passwordForm.newPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
placeholder="请输入新密码(6-16位)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码">
|
||||
<el-input
|
||||
v-model="passwordForm.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
placeholder="请再次输入新密码"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<el-form-item v-if="passwordError">
|
||||
<el-alert :title="passwordError" type="error" :closable="false" style="color: #f56c6c;" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定修改</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { changePassword } from "@/api/user";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
userId: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit', 'close']);
|
||||
|
||||
const visible = ref(false);
|
||||
const passwordError = ref("");
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
username: "",
|
||||
});
|
||||
|
||||
const passwordForm = ref<any>({
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
// 监听 modelValue
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal;
|
||||
if (newVal && props.userId) {
|
||||
form.value.id = props.userId;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 userId 变化
|
||||
watch(() => props.userId, (newVal) => {
|
||||
if (newVal) {
|
||||
form.value.id = newVal;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit('update:modelValue', false);
|
||||
}
|
||||
});
|
||||
|
||||
// 校验密码
|
||||
const validatePassword = (password: string) => {
|
||||
if (!password) {
|
||||
return "请输入密码";
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return "密码长度不能小于6位";
|
||||
}
|
||||
if (password.length > 16) {
|
||||
return "密码长度不能大于16位";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 校验确认密码
|
||||
const validateConfirmPassword = (password: string, confirmPassword: string) => {
|
||||
if (!confirmPassword) {
|
||||
return "请再次输入密码";
|
||||
}
|
||||
if (confirmPassword !== password) {
|
||||
return "两次输入的密码不一致";
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
passwordForm.value = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 清除之前的错误
|
||||
passwordError.value = "";
|
||||
|
||||
try {
|
||||
if (!form.value.id) {
|
||||
passwordError.value = "用户ID不能为空";
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验新密码格式
|
||||
const passwordCheck = validatePassword(passwordForm.value.newPassword);
|
||||
if (passwordCheck !== true) {
|
||||
passwordError.value = passwordCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 校验确认密码
|
||||
const confirmCheck = validateConfirmPassword(
|
||||
passwordForm.value.newPassword,
|
||||
passwordForm.value.confirmPassword
|
||||
);
|
||||
if (confirmCheck !== true) {
|
||||
passwordError.value = confirmCheck;
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用接口修改密码
|
||||
const res = await changePassword(form.value.id, passwordForm.value);
|
||||
|
||||
if (res.code === 200 || res.msg === '修改成功') {
|
||||
ElMessage.success("密码修改成功");
|
||||
visible.value = false;
|
||||
emit('update:modelValue', false);
|
||||
emit('submit');
|
||||
} else {
|
||||
passwordError.value = res.msg || "密码修改失败";
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.msg || e?.response?.data?.message || e?.message || "操作失败";
|
||||
passwordError.value = errorMsg;
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
open: (userId: number, username: string) => {
|
||||
form.value = {
|
||||
id: userId,
|
||||
username: username,
|
||||
};
|
||||
passwordForm.value = {
|
||||
newPassword: "",
|
||||
confirmPassword: "",
|
||||
};
|
||||
passwordError.value = "";
|
||||
visible.value = true;
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
</style>
|
||||
@@ -1,190 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="用户信息预览" size="50%">
|
||||
<div class="user-preview" v-if="user">
|
||||
<div class="user-header">
|
||||
<div class="user-avatar">
|
||||
<el-avatar :size="80" :icon="UserFilled" />
|
||||
</div>
|
||||
<h2 class="user-name">{{ user.name || "未知用户" }}</h2>
|
||||
<el-tag :type="user.status === 1 ? 'success' : 'danger'" size="large">
|
||||
{{ user.status === 1 ? "启用" : "禁用" }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="user-info">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="ID">
|
||||
{{ user.id }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="账号">
|
||||
{{ user.account || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">
|
||||
{{ user.name || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">
|
||||
{{ user.sex === 1 ? "男" : "女" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">
|
||||
{{ user.phone || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="QQ">
|
||||
{{ user.qq || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ user.email || "未设置" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="角色">
|
||||
<el-tag :type="getRoleTagType(user.group_id)" size="small">
|
||||
{{ getRoleName(user.group_id) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录IP">
|
||||
{{ user.last_login_ip || "未登录" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="登录次数">
|
||||
{{ user.login_count || 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ user.create_time || "未知" }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ user.update_time || "未知" }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="no-user">
|
||||
<el-empty description="暂无用户信息" />
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { UserFilled } from "@element-plus/icons-vue";
|
||||
import { getAllRoles } from "@/api/role";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: string;
|
||||
email: string;
|
||||
sex: number;
|
||||
group_id: number;
|
||||
status: number;
|
||||
last_login_ip: string;
|
||||
login_count: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
user: {
|
||||
type: Object as () => User | undefined,
|
||||
default: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const roles = ref<Role[]>([]);
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听visible变化,同步给父组件
|
||||
watch(visible, (newVal) => {
|
||||
emit("update:modelValue", newVal);
|
||||
});
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
roles.value = res.data || [];
|
||||
} catch (e) {
|
||||
roles.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取角色tag状态
|
||||
function getRoleTagType(group_id: number): string {
|
||||
const typeMap: Record<number, string> = {
|
||||
1: "primary",
|
||||
2: "success",
|
||||
3: "warning",
|
||||
4: "danger",
|
||||
};
|
||||
return typeMap[group_id] || "primary";
|
||||
}
|
||||
|
||||
// 获取角色名称
|
||||
function getRoleName(group_id: number): string {
|
||||
const role = roles.value.find((r) => r.id === group_id);
|
||||
return role?.name || "未知";
|
||||
}
|
||||
|
||||
// 暴露open方法供父组件调用
|
||||
const open = (userData?: User) => {
|
||||
visible.value = true;
|
||||
fetchRoles();
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
});
|
||||
|
||||
// 初始化时获取角色列表
|
||||
fetchRoles();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-preview {
|
||||
padding: 20px;
|
||||
|
||||
.user-header {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
|
||||
.user-avatar {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
margin: 15px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.no-user {
|
||||
padding: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,539 +0,0 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<div class="form-title">账号信息</div>
|
||||
<!-- 账号 -->
|
||||
<el-form-item label="账号">
|
||||
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-form-item label="密码" prop="password" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider></el-divider>
|
||||
<div class="form-title">个人信息</div>
|
||||
<!-- 头像 -->
|
||||
<el-form-item label="头像">
|
||||
<el-upload
|
||||
class="avatar-uploader"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeAvatarUpload"
|
||||
:http-request="uploadFile"
|
||||
>
|
||||
<img v-if="form.avatar" :src="getAvatarUrl(form.avatar)" class="avatar" />
|
||||
<el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 姓名 -->
|
||||
<el-form-item label="姓名">
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 性别 -->
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
||||
<el-radio-button label="未知" :value="0" />
|
||||
<el-radio-button label="男" :value="1" />
|
||||
<el-radio-button label="女" :value="2" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 生日 -->
|
||||
<el-form-item label="生日">
|
||||
<el-date-picker
|
||||
v-model="form.birth"
|
||||
type="date"
|
||||
placeholder="请选择日期"
|
||||
style="width: 100%"
|
||||
value-format="YYYY-MM-DD"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 电话 -->
|
||||
<el-form-item label="电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<el-form-item label="邮箱">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="form.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from '@element-plus/icons-vue';
|
||||
import type { UploadProps, UploadRequestOptions } from 'element-plus';
|
||||
import { createUser, updateUser, getUserDetail } from "@/api/babyhealth";
|
||||
import { uploadAvatar } from '@/api/upload';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "submit", "close"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: "",
|
||||
sex: 0,
|
||||
birth: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
avatar: "",
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return isAdd.value ? "添加用户" : "编辑用户";
|
||||
});
|
||||
|
||||
// 密码验证规则
|
||||
const validatePassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须符合规则
|
||||
if (value && value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 确认密码验证规则
|
||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,确认密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则确认密码必须一致
|
||||
if (form.value.password && value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
// 如果填写了确认密码但没填密码,提示错误
|
||||
if (value && !form.value.password) {
|
||||
callback(new Error("请先输入密码"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
account: [
|
||||
{ required: true, message: "请输入账号", trigger: "blur" },
|
||||
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
password: [{ validator: validatePassword, trigger: "blur" }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
|
||||
};
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 statusDict 变化,用于调试
|
||||
watch(
|
||||
() => props.statusDict,
|
||||
(newVal) => {},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = import.meta.env.VITE_API_BASE_URL + "/admin/upload";
|
||||
const uploadHeaders = {
|
||||
Authorization: "Bearer " + localStorage.getItem("token"),
|
||||
};
|
||||
|
||||
// 拼接头像完整URL
|
||||
const getAvatarUrl = (url: string) => {
|
||||
if (!url) return '';
|
||||
// 如果已经是完整URL,直接返回
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
// 拼接API地址
|
||||
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
|
||||
};
|
||||
|
||||
// 头像上传前校验
|
||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
||||
const isImage = rawFile.type.startsWith("image/");
|
||||
const isLt5M = rawFile.size / 1024 / 1024 < 5;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("只能上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isLt5M) {
|
||||
ElMessage.error("图片大小不能超过5MB!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// 上传文件到服务器
|
||||
const uploadFile = async (options: any) => {
|
||||
const { file } = options;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
try {
|
||||
// 使用uploadAvatar接口上传文件
|
||||
const response = await uploadAvatar(formData, { cate: 'user_avatar' });
|
||||
|
||||
if (response.code === 200) {
|
||||
// 更新用户头像
|
||||
form.value.avatar = response.data.url || response.data.path || '';
|
||||
// 调用更新用户信息接口保存头像URL
|
||||
await updateUser(form.value.id, { avatar: form.value.avatar });
|
||||
ElMessage.success("头像上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
ElMessage.error("上传失败,请重试");
|
||||
}
|
||||
};
|
||||
|
||||
// 头像上传成功
|
||||
const handleAvatarSuccess: UploadProps['onSuccess'] = (response, uploadFile) => {
|
||||
if (response.code === 200) {
|
||||
form.value.avatar = response.data.avatar || '';
|
||||
ElMessage.success("头像上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
// 处理两种调用方式:传递用户对象或用户 ID
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getUserDetail(userId);
|
||||
|
||||
const data = res.data || res;
|
||||
|
||||
// 确保 sex 和 status 都是数字类型
|
||||
const sexValue =
|
||||
data.sex !== undefined && data.sex !== null
|
||||
? Number(data.sex)
|
||||
: 1;
|
||||
|
||||
const statusValue =
|
||||
data.status !== undefined && data.status !== null
|
||||
? Number(data.status)
|
||||
: 1;
|
||||
|
||||
form.value = {
|
||||
id: data.id,
|
||||
account: data.account,
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: data.name,
|
||||
sex: sexValue,
|
||||
birth: data.birth,
|
||||
phone: data.phone,
|
||||
email: data.email,
|
||||
status: statusValue,
|
||||
avatar: data.avatar || "",
|
||||
};
|
||||
} catch (e: any) {
|
||||
console.error("Failed to load user data:", e);
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (error) {
|
||||
ElMessage.warning("请检查表单填写是否正确");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码一致性
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!form.value.password) {
|
||||
ElMessage.error("请输入密码");
|
||||
return;
|
||||
}
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须填写确认密码且一致
|
||||
if (form.value.password) {
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isAdd.value) {
|
||||
// 新增用户
|
||||
const submitData: any = {
|
||||
account: form.value.account,
|
||||
password: form.value.password || '',
|
||||
name: form.value.name,
|
||||
sex: form.value.sex,
|
||||
birth: form.value.birth,
|
||||
phone: form.value.phone,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
avatar: form.value.avatar,
|
||||
};
|
||||
|
||||
await createUser(submitData);
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
// 编辑用户
|
||||
if (!form.value.id || form.value.id === 0) {
|
||||
ElMessage.error("用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData: any = {
|
||||
id: form.value.id,
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
sex: form.value.sex,
|
||||
birth: form.value.birth,
|
||||
phone: form.value.phone,
|
||||
email: form.value.email,
|
||||
status: form.value.status,
|
||||
avatar: form.value.avatar,
|
||||
};
|
||||
|
||||
// 只有在填写了密码时才添加到提交数据中
|
||||
if (form.value.password) {
|
||||
submitData.password = form.value.password;
|
||||
}
|
||||
|
||||
await updateUser(form.value.id, submitData);
|
||||
ElMessage.success("更新成功");
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
emit("submit");
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
ElMessage.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openAdd: () => {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: "",
|
||||
sex: 0,
|
||||
birth: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
avatar: "",
|
||||
};
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
openEdit: (user: any) => {
|
||||
isAdd.value = false;
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
// 异步加载用户详细信息
|
||||
loadUserData(user);
|
||||
},
|
||||
open: (user?: any) => {
|
||||
if (user) {
|
||||
isAdd.value = false;
|
||||
loadUserData(user);
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
account: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
name: "",
|
||||
sex: 0,
|
||||
birth: "",
|
||||
phone: "",
|
||||
email: "",
|
||||
status: 1,
|
||||
avatar: "",
|
||||
};
|
||||
}
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatar-uploader {
|
||||
.avatar {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
text-align: center;
|
||||
line-height: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,328 +0,0 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column
|
||||
prop="id"
|
||||
label="ID"
|
||||
align="center"
|
||||
fixed="left"
|
||||
/>
|
||||
<el-table-column prop="account" label="账号" align="center" />
|
||||
<el-table-column
|
||||
prop="name"
|
||||
label="姓名"
|
||||
align="center"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sex" label="性别" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag
|
||||
:type="getSexTagType(scope.row.sex)"
|
||||
size="small"
|
||||
>
|
||||
{{ getSexTagText(scope.row.sex) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="phone"
|
||||
label="手机号"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="email"
|
||||
label="email"
|
||||
width="180"
|
||||
align="center"
|
||||
/>
|
||||
<el-table-column prop="status" label="状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
||||
scope.row.status === 1 ? "启用" : "禁用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="warning"
|
||||
@click="handleChangePassword(scope.row)"
|
||||
>
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
@current-change="handlePageChange"
|
||||
layout="total, prev, pager, next"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 编辑用户对话框组件 -->
|
||||
<UserEditDialog
|
||||
ref="userEditRef"
|
||||
:modelValue="editDialogVisible"
|
||||
@update:modelValue="editDialogVisible = $event"
|
||||
:is-edit="isEdit"
|
||||
@submit="handleEditSuccess"
|
||||
@close="editDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 修改密码对话框组件 -->
|
||||
<ChangePasswordDialog
|
||||
ref="changePasswordRef"
|
||||
:modelValue="passwordDialogVisible"
|
||||
@update:modelValue="passwordDialogVisible = $event"
|
||||
:user-id="currentUserId"
|
||||
@submit="handlePasswordChangeSuccess"
|
||||
@close="passwordDialogVisible = false"
|
||||
/>
|
||||
|
||||
<!-- 预览用户对话框组件 -->
|
||||
<PreviewDialog
|
||||
ref="previewDialogRef"
|
||||
:modelValue="previewDialogVisible"
|
||||
@update:modelValue="previewDialogVisible = $event"
|
||||
:user="currentUser"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getUserList, deleteUser } from "@/api/babyhealth";
|
||||
import UserEditDialog from "./components/userEdit.vue";
|
||||
import ChangePasswordDialog from "./components/changePassword.vue";
|
||||
import PreviewDialog from "./components/preview.vue";
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
account: string;
|
||||
name: string;
|
||||
phone: string;
|
||||
qq: string;
|
||||
sex: number;
|
||||
group_id: number;
|
||||
status: number;
|
||||
last_login_ip: string;
|
||||
email: number;
|
||||
create_time: string;
|
||||
update_time: string;
|
||||
}
|
||||
|
||||
interface Role {
|
||||
id: number;
|
||||
name: string;
|
||||
status?: number;
|
||||
rights?: string;
|
||||
create_time?: string;
|
||||
update_time?: string;
|
||||
}
|
||||
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
|
||||
const users = ref<User[]>([]);
|
||||
const roles = ref<Role[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
// 组件引用
|
||||
const userEditRef = ref();
|
||||
const changePasswordRef = ref();
|
||||
const previewDialogRef = ref();
|
||||
|
||||
// 编辑/密码对话框状态
|
||||
const editDialogVisible = ref(false);
|
||||
const passwordDialogVisible = ref(false);
|
||||
const previewDialogVisible = ref(false);
|
||||
const editDialogTitle = ref("添加用户");
|
||||
const isEdit = ref(false);
|
||||
const currentUserId = ref<number | undefined>(undefined);
|
||||
const currentUser = ref<User | undefined>(undefined);
|
||||
|
||||
//刷新
|
||||
const refresh = async () => {
|
||||
await fetchUsers();
|
||||
};
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
const res = await getAllRoles();
|
||||
roles.value = res.data || [];
|
||||
} catch (e) {
|
||||
roles.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
// 获取性别tag类型
|
||||
function getSexTagType(sex: number): string {
|
||||
if (sex === 1) return 'primary'; // 男
|
||||
if (sex === 2) return 'danger'; // 女
|
||||
return 'info'; // 未知
|
||||
}
|
||||
|
||||
// 获取性别tag文本
|
||||
function getSexTagText(sex: number): string {
|
||||
if (sex === 1) return '男';
|
||||
if (sex === 2) return '女';
|
||||
return '未知';
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getUserList();
|
||||
users.value = res.data || [];
|
||||
} catch (e) {
|
||||
users.value = [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
isEdit.value = false;
|
||||
editDialogVisible.value = true;
|
||||
if (userEditRef.value) {
|
||||
userEditRef.value.open();
|
||||
}
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (user: User) => {
|
||||
isEdit.value = true;
|
||||
editDialogVisible.value = true;
|
||||
if (userEditRef.value) {
|
||||
userEditRef.value.open(user);
|
||||
}
|
||||
};
|
||||
|
||||
// 预览用户
|
||||
const handlePreview = (user: User) => {
|
||||
currentUser.value = user;
|
||||
previewDialogVisible.value = true;
|
||||
if (previewDialogRef.value) {
|
||||
previewDialogRef.value.open(user);
|
||||
}
|
||||
};
|
||||
|
||||
//修改密码
|
||||
const handleChangePassword = async (user: User) => {
|
||||
changePasswordRef.value.open(user.id, user.account);
|
||||
passwordDialogVisible.value = true;
|
||||
currentUserId.value = user.id;
|
||||
};
|
||||
|
||||
// 编辑成功回调
|
||||
const handleEditSuccess = () => {
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success(isEdit.value ? "编辑成功" : "添加成功");
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
// 密码修改成功回调
|
||||
const handlePasswordChangeSuccess = () => {
|
||||
passwordDialogVisible.value = false;
|
||||
ElMessage.success("密码修改成功");
|
||||
};
|
||||
|
||||
// 分页改变
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchUsers();
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (user: User) => {
|
||||
ElMessageBox.confirm("确认删除该用户?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
await deleteUser(user.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchUsers();
|
||||
} catch (e) {
|
||||
ElMessage.error("删除失败");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
fetchUsers();
|
||||
fetchRoles();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -185,7 +185,7 @@ onUnmounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style scoped lang="less">
|
||||
.content-stats-container {
|
||||
min-height: 100vh;
|
||||
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
<template>
|
||||
<div class="category-manager">
|
||||
<!-- 页面头部 -->
|
||||
<div class="page-header">
|
||||
<div class="page-title">
|
||||
<h3>文章分类</h3>
|
||||
<p>共 {{ totalCount }} 个分类</p>
|
||||
</div>
|
||||
<div class="page-actions">
|
||||
<el-button type="primary" :icon="Plus" @click="handleAdd">
|
||||
新增分类
|
||||
</el-button>
|
||||
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="category-list" v-loading="loading">
|
||||
<div v-if="filteredCategories.length === 0" class="empty-state"></div>
|
||||
<div v-if="filteredCategories.length === 0" class="empty-state">
|
||||
<el-empty description="暂无文章分类" />
|
||||
</div>
|
||||
|
||||
<div v-else class="category-tree">
|
||||
<category-node
|
||||
@@ -53,6 +69,13 @@ const currentEdit = ref(null);
|
||||
const searchText = ref("");
|
||||
const categories = ref([]);
|
||||
|
||||
// 新增分类
|
||||
const addCategory = ref(null);
|
||||
const handleAdd = () => {
|
||||
addCategory.value = { parentId: 0 };
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const totalCount = computed(() => categories.value.length);
|
||||
|
||||
@@ -173,9 +196,11 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="less" scoped>
|
||||
.category-manager {
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
// 页面头部样式
|
||||
.page-header {
|
||||
|
||||
@@ -99,7 +99,7 @@ const handleRowClick = () => {
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="less" scoped>
|
||||
.category-node-wrapper {
|
||||
.category-item {
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
@@ -205,7 +205,7 @@ const cateOptions = ref([]);
|
||||
|
||||
const form = reactive({
|
||||
title: "",
|
||||
author: "云泽网",
|
||||
author: "",
|
||||
cate: "",
|
||||
content: "",
|
||||
image: "",
|
||||
@@ -503,7 +503,7 @@ function resetForm() {
|
||||
// 重置表单数据
|
||||
Object.assign(form, {
|
||||
title: "",
|
||||
author: "云泽网",
|
||||
author: "",
|
||||
cate: "",
|
||||
content: "",
|
||||
image: "",
|
||||
@@ -525,7 +525,7 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="less" scoped>
|
||||
.editor-container {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -118,7 +118,7 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="less" scoped>
|
||||
.article-preview {
|
||||
padding: 20px;
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索文章标题/作者"
|
||||
placeholder="搜索文章标题"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
>
|
||||
@@ -630,7 +630,7 @@ onMounted(async () => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="less" scoped>
|
||||
.cms-articles {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="需求标题" required>
|
||||
<el-input v-model="form.title" placeholder="请输入需求标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="需求描述" required>
|
||||
<el-input
|
||||
v-model="form.desc"
|
||||
type="textarea"
|
||||
rows="4"
|
||||
placeholder="请输入需求描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请人">
|
||||
<el-input v-model="form.applicant" placeholder="请输入申请人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话">
|
||||
<el-input v-model="form.phone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
<el-form-item label="需求状态">
|
||||
<el-select v-model="form.status" placeholder="请选择状态">
|
||||
<el-option label="待处理" :value="1" />
|
||||
<el-option label="处理中" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已拒绝" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { addDemand, editDemand } from "@/api/demand";
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "新增需求",
|
||||
},
|
||||
demand: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
id: "",
|
||||
title: "",
|
||||
desc: "",
|
||||
applicant: "",
|
||||
status: 1,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(["close", "submit"]);
|
||||
|
||||
// 状态
|
||||
const dialogVisible = ref(props.visible);
|
||||
const dialogTitle = ref(props.title);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: "",
|
||||
title: "",
|
||||
desc: "",
|
||||
applicant: "",
|
||||
status: 1,
|
||||
});
|
||||
|
||||
// 监听props变化
|
||||
watch(
|
||||
() => props.visible,
|
||||
(newVal) => {
|
||||
dialogVisible.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.title,
|
||||
(newVal) => {
|
||||
dialogTitle.value = newVal;
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.demand,
|
||||
(newVal) => {
|
||||
Object.assign(form, newVal);
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// 处理关闭
|
||||
const handleClose = () => {
|
||||
emit("close");
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!form.title || !form.desc) {
|
||||
ElMessage.warning("请填写必填项");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (form.id) {
|
||||
// 编辑
|
||||
result = await editDemand(form.id, form);
|
||||
} else {
|
||||
// 新增
|
||||
result = await addDemand(form);
|
||||
}
|
||||
|
||||
if (result.code === 200) {
|
||||
ElMessage.success(form.id ? "编辑成功" : "新增成功");
|
||||
emit("submit", { ...form });
|
||||
} else {
|
||||
ElMessage.error(result.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("网络错误,请稍后重试");
|
||||
console.error("提交表单失败:", error);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>需求管理</h2>
|
||||
<div>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
新增需求
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleRefush">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选 -->
|
||||
<div class="search-bar">
|
||||
<el-form :inline="true" :model="searchForm" class="mb-4">
|
||||
<el-form-item label="需求标题">
|
||||
<el-input
|
||||
v-model="searchForm.title"
|
||||
placeholder="请输入需求标题"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="需求状态">
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="全部" value="" />
|
||||
<el-option label="待处理" :value="1" />
|
||||
<el-option label="处理中" :value="2" />
|
||||
<el-option label="已完成" :value="3" />
|
||||
<el-option label="已拒绝" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 需求列表 -->
|
||||
<el-table v-loading="loading" :data="demandList" style="width: 100%" border>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="title" label="标题" min-width="100" />
|
||||
<el-table-column
|
||||
prop="desc"
|
||||
label="描述"
|
||||
min-width="300"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column prop="applicant" label="申请人" width="120" />
|
||||
<el-table-column prop="phone" label="电话" width="180" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusType(scope.row.status)">
|
||||
{{ getStatusText(scope.row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button size="small" @click="handleEdit(scope.row)"
|
||||
>编辑</el-button
|
||||
>
|
||||
<el-button
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
>删除</el-button
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination" v-if="total > 0">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="total"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 需求表单组件 -->
|
||||
<Edit
|
||||
:visible="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:demand="form"
|
||||
@close="handleFormClose"
|
||||
@submit="handleFormSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import Edit from "./components/edit.vue";
|
||||
import { getDemandList, deleteDemand } from "@/api/demand";
|
||||
|
||||
// 状态管理
|
||||
const loading = ref(false);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogTitle = ref("新增需求");
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
title: "",
|
||||
status: "",
|
||||
});
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
size: 10,
|
||||
});
|
||||
|
||||
// 总数据量
|
||||
const total = ref(0);
|
||||
|
||||
// 需求列表
|
||||
const demandList = ref<any[]>([]);
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: "",
|
||||
title: "",
|
||||
desc: "",
|
||||
applicant: "",
|
||||
status: 1,
|
||||
});
|
||||
|
||||
// 状态类型映射
|
||||
const getStatusType = (status: string | number) => {
|
||||
const typeMap: Record<string | number, string> = {
|
||||
1: "info",
|
||||
2: "warning",
|
||||
3: "success",
|
||||
4: "danger",
|
||||
};
|
||||
return typeMap[status] || "info";
|
||||
};
|
||||
|
||||
// 状态文本映射
|
||||
const getStatusText = (status: string | number) => {
|
||||
const textMap: Record<string | number, string> = {
|
||||
1: "待处理",
|
||||
2: "处理中",
|
||||
3: "已完成",
|
||||
4: "已拒绝",
|
||||
};
|
||||
return textMap[status] || String(status);
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
fetchDemandList();
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.title = "";
|
||||
searchForm.status = "";
|
||||
pagination.current = 1;
|
||||
fetchDemandList();
|
||||
};
|
||||
|
||||
// 获取需求列表
|
||||
const fetchDemandList = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const data = await getDemandList();
|
||||
|
||||
if (data.code === 200) {
|
||||
let filteredList = [...data.list];
|
||||
|
||||
// 根据搜索条件过滤数据
|
||||
if (searchForm.title) {
|
||||
filteredList = filteredList.filter((item) =>
|
||||
item.title.includes(searchForm.title),
|
||||
);
|
||||
}
|
||||
if (searchForm.status) {
|
||||
filteredList = filteredList.filter(
|
||||
(item) => item.status === searchForm.status,
|
||||
);
|
||||
}
|
||||
|
||||
// 模拟分页
|
||||
total.value = filteredList.length;
|
||||
const start = (pagination.current - 1) * pagination.size;
|
||||
const end = start + pagination.size;
|
||||
demandList.value = filteredList.slice(start, end);
|
||||
} else {
|
||||
ElMessage.error("获取需求列表失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("网络错误,请稍后重试");
|
||||
console.error("获取需求列表失败:", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.size = size;
|
||||
fetchDemandList();
|
||||
};
|
||||
|
||||
// 页码变化
|
||||
const handleCurrentChange = (current: number) => {
|
||||
pagination.current = current;
|
||||
fetchDemandList();
|
||||
};
|
||||
|
||||
// 新增需求
|
||||
const handleAdd = () => {
|
||||
dialogTitle.value = "新增需求";
|
||||
Object.assign(form, {
|
||||
id: "",
|
||||
title: "",
|
||||
desc: "",
|
||||
applicant: "",
|
||||
status: 1,
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 编辑需求
|
||||
const handleEdit = (row: any) => {
|
||||
dialogTitle.value = "编辑需求";
|
||||
Object.assign(form, row);
|
||||
dialogVisible.value = true;
|
||||
};
|
||||
|
||||
// 删除需求
|
||||
const handleDelete = async (id: number) => {
|
||||
ElMessageBox.confirm("确定要删除这个需求吗?", "删除确认", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
const result = await deleteDemand(id);
|
||||
|
||||
if (result.code === 200) {
|
||||
ElMessage.success("删除成功");
|
||||
fetchDemandList();
|
||||
} else {
|
||||
ElMessage.error(result.msg || "删除失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error("网络错误,请稍后重试");
|
||||
console.error("删除需求失败:", error);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
});
|
||||
};
|
||||
|
||||
// 刷新需求
|
||||
const handleRefush = () => {
|
||||
fetchDemandList();
|
||||
};
|
||||
|
||||
// 处理表单关闭
|
||||
const handleFormClose = () => {
|
||||
dialogVisible.value = false;
|
||||
};
|
||||
|
||||
// 处理表单提交
|
||||
const handleFormSubmit = () => {
|
||||
dialogVisible.value = false;
|
||||
fetchDemandList();
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchDemandList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>租户域名审核</h2>
|
||||
<el-button @click="fetchData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="searchForm.sub_domain"
|
||||
placeholder="搜索二级域名"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width: 120px; margin-right: 10px;">
|
||||
<el-option label="审核中" :value="0" />
|
||||
<el-option label="已生效" :value="1" />
|
||||
<el-option label="已禁用" :value="2" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="tenant_name" label="租户名称" min-width="150" />
|
||||
<el-table-column prop="sub_domain" label="二级域名前缀" width="150" />
|
||||
<el-table-column prop="main_domain" label="主域名" min-width="150" />
|
||||
<el-table-column prop="full_domain" label="完整域名" min-width="200" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === 0" type="warning">审核中</el-tag>
|
||||
<el-tag v-else-if="scope.row.status === 1" type="success">已生效</el-tag>
|
||||
<el-tag v-else type="danger">已禁用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="申请时间" width="180" />
|
||||
<el-table-column label="操作" width="200" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.status === 0">
|
||||
<el-button size="small" type="success" @click="handleAudit(scope.row, 'approve')">
|
||||
通过
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" @click="handleAudit(scope.row, 'reject')">
|
||||
拒绝
|
||||
</el-button>
|
||||
</template>
|
||||
<template v-else-if="scope.row.status === 1">
|
||||
<el-button size="small" text type="danger" @click="handleDisable(scope.row)">
|
||||
禁用
|
||||
</el-button>
|
||||
</template>
|
||||
<span v-else style="color: #999;">-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Refresh } from '@element-plus/icons-vue'
|
||||
import { getTenantDomainList, auditTenantDomain, toggleTenantDomainStatus } from '@/api/domain'
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const searchForm = reactive({
|
||||
sub_domain: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getTenantDomainList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchForm
|
||||
})
|
||||
if (res.code === 200) {
|
||||
tableData.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
const handleAudit = async (row: any, action: string) => {
|
||||
const msg = action === 'approve' ? '确定要通过该域名申请吗?' : '确定要拒绝该域名申请吗?'
|
||||
await ElMessageBox.confirm(msg, '提示', { type: 'warning' })
|
||||
|
||||
const res = await auditTenantDomain({ id: row.id, action })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(res.msg)
|
||||
fetchData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisable = async (row: any) => {
|
||||
await ElMessageBox.confirm('确定要禁用该域名吗?', '提示', { type: 'warning' })
|
||||
|
||||
const res = await toggleTenantDomainStatus(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(res.msg)
|
||||
fetchData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
<template></template>
|
||||
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,287 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>主域名池管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加主域名
|
||||
</el-button>
|
||||
<el-button @click="fetchData" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="searchForm.main_domain"
|
||||
placeholder="搜索主域名"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px;"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width: 120px; margin-right: 10px;">
|
||||
<el-option label="禁用" :value="0" />
|
||||
<el-option label="启用" :value="1" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 表格 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
style="width: 100%"
|
||||
border
|
||||
v-loading="loading"
|
||||
element-loading-text="正在加载..."
|
||||
>
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="main_domain" label="主域名" min-width="200" />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
|
||||
{{ scope.row.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="240" fixed="right" align="center">
|
||||
<template #default="scope">
|
||||
<el-button size="small" text @click="handleEdit(scope.row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
<span>编辑</span>
|
||||
</el-button>
|
||||
<el-button size="small" text @click="handleToggleStatus(scope.row)">
|
||||
<el-icon><Switch /></el-icon>
|
||||
<span>{{ scope.row.status === 1 ? '禁用' : '启用' }}</span>
|
||||
</el-button>
|
||||
<el-button size="small" text type="danger" @click="handleDelete(scope.row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span>删除</span>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-wrap">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:page-sizes="[10, 20, 50]"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="fetchData"
|
||||
@current-change="fetchData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑弹窗 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑主域名' : '添加主域名'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<el-form-item label="主域名" prop="main_domain">
|
||||
<el-input v-model="form.main_domain" placeholder="请输入主域名,如: example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete, Refresh, Switch } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getDomainPoolList,
|
||||
createDomainPool,
|
||||
updateDomainPool,
|
||||
deleteDomainPool,
|
||||
toggleDomainPoolStatus
|
||||
} from '@/api/domain'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
main_domain: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref<any[]>([])
|
||||
|
||||
// 表单数据
|
||||
const form = reactive({
|
||||
id: 0,
|
||||
main_domain: '',
|
||||
status: 1
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
main_domain: [
|
||||
{ required: true, message: '请输入主域名', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getDomainPoolList({
|
||||
page: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchForm
|
||||
})
|
||||
if (res.code === 200) {
|
||||
tableData.value = res.data.list || []
|
||||
pagination.total = res.data.total || 0
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
form.id = 0
|
||||
form.main_domain = ''
|
||||
form.status = 1
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row: any) => {
|
||||
isEdit.value = true
|
||||
form.id = row.id
|
||||
form.main_domain = row.main_domain
|
||||
form.status = row.status
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
const res = await updateDomainPool(form)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('更新成功')
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '更新失败')
|
||||
}
|
||||
} else {
|
||||
const res = await createDomainPool(form)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('创建成功')
|
||||
dialogVisible.value = false
|
||||
fetchData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '创建失败')
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (row: any) => {
|
||||
ElMessageBox.confirm('确定要删除该主域名吗?', '提示', {
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
const res = await deleteDomainPool(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 切换状态
|
||||
const handleToggleStatus = async (row: any) => {
|
||||
const res = await toggleDomainPoolStatus(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('状态更新成功')
|
||||
fetchData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,302 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="链接名称" prop="link_name">
|
||||
<el-input v-model="formData.link_name" placeholder="请输入链接名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="链接地址" prop="link_url">
|
||||
<el-input v-model="formData.link_url" placeholder="请输入链接地址,如:https://www.example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Logo" prop="link_logo">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img
|
||||
v-if="formData.link_logo"
|
||||
:src="getImageUrl(formData.link_logo)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传Logo</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: var(--el-text-color-secondary);">
|
||||
建议尺寸:200x200,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
<el-button
|
||||
v-if="formData.link_logo"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px;"
|
||||
>
|
||||
删除图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { addFriendlink, updateFriendlink } from '@/api/friendlink'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '添加友情链接'
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith('image/');
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('请上传图片文件!');
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error('图片大小不能超过 2MB!');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
formData.link_logo = response.data.url || response.data.path;
|
||||
ElMessage.success(response.code === 201 ? "文件已存在,直接使用" : "图片上传成功");
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.link_logo = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
const formData = reactive({
|
||||
link_name: '',
|
||||
link_url: '',
|
||||
link_logo: '',
|
||||
description: '',
|
||||
sort: 0,
|
||||
status: 1
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
link_name: [
|
||||
{ required: true, message: '请输入链接名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
],
|
||||
link_url: [
|
||||
{ required: true, message: '请输入链接地址', trigger: 'blur' },
|
||||
{ type: 'url', message: '请输入正确的URL地址', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.link_name = ''
|
||||
formData.link_url = ''
|
||||
formData.link_logo = ''
|
||||
formData.description = ''
|
||||
formData.sort = 0
|
||||
formData.status = 1
|
||||
}
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
if (props.isEdit) {
|
||||
res = await updateFriendlink(props.rowData.id, formData)
|
||||
} else {
|
||||
res = await addFriendlink(formData)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新成功' : '添加成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.image-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>友情链接管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加链接
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入链接名称搜索"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-select
|
||||
v-model="searchForm.status"
|
||||
placeholder="状态筛选"
|
||||
clearable
|
||||
style="width: 120px; margin-right: 10px"
|
||||
>
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
:data="friendlinkList"
|
||||
style="width: 100%"
|
||||
v-loading="loading"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="link_name" label="链接名称" min-width="150" align="center" />
|
||||
<el-table-column prop="link_url" label="链接地址" min-width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.link_url" target="_blank" type="primary">
|
||||
{{ row.link_url }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="link_logo" label="Logo" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
v-if="row.link_logo"
|
||||
:src="getImageUrl(row.link_logo)"
|
||||
style="width: 50px; height: 50px; object-fit: contain"
|
||||
preview-teleported
|
||||
/>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="描述" min-width="150" align="center" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="(val) => handleStatusChange(row, val)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.page"
|
||||
v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:is-edit="isEdit"
|
||||
:row-data="currentRow"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getFriendlinkList,
|
||||
updateFriendlink,
|
||||
deleteFriendlink
|
||||
} from '@/api/friendlink'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const friendlinkList = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getFriendlinkList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
friendlinkList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取友情链接列表失败:', error)
|
||||
ElMessage.error('获取列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(item => item.id)
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加友情链接'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑友情链接'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该友情链接吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteFriendlink(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变化
|
||||
const handleStatusChange = async (row, val) => {
|
||||
try {
|
||||
const res = await updateFriendlink(row.id, { status: val })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('状态更新成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '状态更新失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('状态更新失败:', error)
|
||||
ElMessage.error('状态更新失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,7 @@
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,321 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="产品名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入产品名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入产品描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品内容" prop="content">
|
||||
<div class="editor-container">
|
||||
<WangEditor v-model="formData.content" style="height: 450px;" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品图片" prop="thumb">
|
||||
<div class="flex-direction">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img
|
||||
v-if="formData.thumb"
|
||||
:src="getImageUrl(formData.thumb)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传产品图片</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
style="
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
"
|
||||
>
|
||||
建议尺寸:200x200,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="formData.thumb"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
删除产品图片
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { addProducts, updateProducts } from "@/api/products";
|
||||
import WangEditor from "@/views/components/WangEditor.vue";
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "添加产品",
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("请上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error("图片大小不能超过 2MB!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
formData.thumb = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.thumb = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
const formData = reactive({
|
||||
title: "",
|
||||
desc: "",
|
||||
thumb: "",
|
||||
url: "",
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: "请输入产品名称", trigger: "blur" },
|
||||
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
|
||||
],
|
||||
desc: [
|
||||
{ required: true, message: "请输入产品描述", trigger: "blur" },
|
||||
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.title = "";
|
||||
formData.desc = "";
|
||||
formData.thumb = "";
|
||||
formData.url = "";
|
||||
formData.sort = 0;
|
||||
formData.status = 1;
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let res;
|
||||
if (props.isEdit) {
|
||||
res = await updateProducts(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addProducts(formData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? "更新成功" : "添加成功");
|
||||
handleClose();
|
||||
emit("success");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
ElMessage.error("操作失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.image-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.flex-direction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,295 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleTypes">
|
||||
<i class="fa-solid fa-layer-group"></i>
|
||||
分类管理
|
||||
</el-button>
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加产品
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.keyword" placeholder="请输入产品名称搜索" clearable style="width: 200px; margin-right: 10px"
|
||||
@keyup.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态筛选" clearable style="width: 120px; margin-right: 10px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="productsList" style="width: 100%" v-loading="loading" @selection-change="handleSelectionChange">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="thumb" label="产品图片" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-image v-if="row.thumb" :src="getImageUrl(row.thumb)" :preview-src-list="[getImageUrl(row.thumb)]"
|
||||
fit="cover" class="product-thumb" :preview-teleported="true" lazy>
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<el-icon>
|
||||
<Picture />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
|
||||
<span v-else class="text-gray">暂无图片</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="产品名称" min-width="150" align="center" />
|
||||
<el-table-column prop="desc" label="产品描述" min-width="150" align="center" />
|
||||
<el-table-column prop="url" label="跳转地址" min-width="150" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
getProductsList,
|
||||
updateProducts,
|
||||
deleteProducts
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 获取图片完整URL
|
||||
// 接口可能返回:
|
||||
// 1) 绝对地址:http(s)://...
|
||||
// 2) 以 / 开头的相对路径:/storage/uploads/xxx.png
|
||||
// 3) 不以 / 开头的相对路径:storage/uploads/xxx.png
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (typeof imagePath !== 'string') imagePath = String(imagePath);
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
|
||||
const base = (API_BASE_URL ?? '').replace(/\/$/, '');
|
||||
const path = imagePath.startsWith('/') ? imagePath : `/${imagePath}`;
|
||||
return base + path;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const productsList = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductsList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
productsList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取企业产品列表失败:', error)
|
||||
ElMessage.error('获取企业产品列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(item => item.id)
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加企业产品'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑企业产品'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该企业产品吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteProducts(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除企业产品失败:', error)
|
||||
ElMessage.error('删除企业产品失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 跳转到产品分类管理页
|
||||
const handleTypes = () => {
|
||||
router.push('/apps/cms/products/types')
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="父级分类" prop="pid">
|
||||
<el-select
|
||||
v-model="formData.pid"
|
||||
placeholder="请选择父级分类"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in typeOptions"
|
||||
:key="item.id"
|
||||
:label="item.title"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="分类描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入分类描述(可选)"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
addProductsTypes,
|
||||
updateProductsTypes,
|
||||
getProductsTypesList
|
||||
} from '@/api/products'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '添加分类'
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'success'])
|
||||
|
||||
const visible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const typeOptions = ref([])
|
||||
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
pid: 0,
|
||||
desc: '',
|
||||
sort: 0
|
||||
})
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ max: 100, message: '长度不超过100个字符', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.title = ''
|
||||
formData.pid = 0
|
||||
formData.desc = ''
|
||||
formData.sort = 0
|
||||
}
|
||||
|
||||
const loadTypeOptions = async () => {
|
||||
try {
|
||||
const res = await getProductsTypesList({ page: 1, limit: 1000, keyword: '' })
|
||||
if (res.code === 200 && Array.isArray(res.data?.list)) {
|
||||
const curId = props.isEdit ? Number(props.rowData?.id ?? 0) : 0
|
||||
typeOptions.value = curId
|
||||
? res.data.list.filter((item) => Number(item.id) !== curId)
|
||||
: res.data.list
|
||||
} else {
|
||||
typeOptions.value = []
|
||||
}
|
||||
} catch (e) {
|
||||
// 下拉选项失败不影响弹窗可用性
|
||||
console.error('加载父级分类失败:', e)
|
||||
typeOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 打开/关闭
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
async (val) => {
|
||||
visible.value = val
|
||||
if (val) {
|
||||
resetForm()
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData)
|
||||
formData.pid = Number(props.rowData?.pid ?? 0)
|
||||
formData.sort = Number(props.rowData?.sort ?? 0)
|
||||
}
|
||||
await loadTypeOptions()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(visible, (val) => {
|
||||
emit('update:modelValue', val)
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
let res
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
pid: Number(formData.pid ?? 0),
|
||||
desc: formData.desc ?? '',
|
||||
sort: Number(formData.sort ?? 0)
|
||||
}
|
||||
|
||||
if (props.isEdit) {
|
||||
res = await updateProductsTypes(Number(props.rowData?.id), payload)
|
||||
} else {
|
||||
res = await addProductsTypes(payload)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? '更新分类成功' : '添加分类成功')
|
||||
handleClose()
|
||||
emit('success')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('提交分类失败:', e)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>企业产品分类管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
添加分类
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<i class="fa-solid fa-refresh"></i>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchForm.keyword"
|
||||
placeholder="请输入分类名称搜索"
|
||||
clearable
|
||||
style="width: 200px; margin-right: 10px"
|
||||
@keyup.enter="handleSearch" />
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="typesList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="title" label="分类名称" min-width="180" align="center" />
|
||||
<el-table-column prop="desc" label="分类描述" min-width="200" align="center" />
|
||||
<el-table-column prop="pid" label="父级ID" width="110" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="90" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" min-width="180" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getProductsTypesList,
|
||||
deleteProductsTypes
|
||||
} from '@/api/products'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const typesList = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getProductsTypesList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword
|
||||
})
|
||||
if (res.code === 200) {
|
||||
typesList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取企业产品分类列表失败:', error)
|
||||
ElMessage.error('获取企业产品分类列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加企业产品分类'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑企业产品分类'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该企业产品分类吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteProductsTypes(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除分类成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除企业产品分类失败:', error)
|
||||
ElMessage.error('删除企业产品分类失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<template>这是cate</template>
|
||||
<script lang="ts" setup></script>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -1,3 +1,3 @@
|
||||
<template>这是list</template>
|
||||
<script lang="ts" setup></script>
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,315 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="title"
|
||||
width="600px"
|
||||
destroy-on-close
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="服务名称" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入服务名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="服务描述" prop="desc">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入服务描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="Logo" prop="thumb">
|
||||
<div class="flex-direction">
|
||||
<el-upload
|
||||
class="image-uploader"
|
||||
:action="uploadUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="handleImageSuccess"
|
||||
:on-error="handleImageError"
|
||||
:before-upload="beforeImageUpload"
|
||||
accept="image/*"
|
||||
>
|
||||
<img
|
||||
v-if="formData.thumb"
|
||||
:src="getImageUrl(formData.thumb)"
|
||||
class="image-preview"
|
||||
/>
|
||||
<div v-else class="upload-placeholder">
|
||||
<el-icon class="image-uploader-icon"><Plus /></el-icon>
|
||||
<div class="el-upload__text">点击上传服务图标</div>
|
||||
</div>
|
||||
</el-upload>
|
||||
<div
|
||||
style="
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
"
|
||||
>
|
||||
建议尺寸:200x200,支持 jpg、png、gif 格式,大小不超过 2MB
|
||||
</div>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="formData.thumb"
|
||||
size="small"
|
||||
type="danger"
|
||||
text
|
||||
@click="handleRemoveImage"
|
||||
style="margin-top: 8px"
|
||||
>
|
||||
删除图标
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleSubmit"
|
||||
:loading="submitLoading"
|
||||
>
|
||||
确定
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { addService, updateService } from "@/api/services";
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "添加服务",
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
rowData: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "success"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
// 上传配置
|
||||
const uploadUrl = ref(API_BASE_URL + "/admin/uploadfiles");
|
||||
const uploadHeaders = ref({
|
||||
Authorization: "Bearer " + (localStorage.getItem("token") || ""),
|
||||
});
|
||||
|
||||
// 图片上传前校验
|
||||
const beforeImageUpload = (file) => {
|
||||
const isImage = file.type.startsWith("image/");
|
||||
const isLt2M = file.size / 1024 / 1024 < 2;
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error("请上传图片文件!");
|
||||
return false;
|
||||
}
|
||||
if (!isLt2M) {
|
||||
ElMessage.error("图片大小不能超过 2MB!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 图片上传成功
|
||||
const handleImageSuccess = (response) => {
|
||||
if (response.code === 200 || response.code === 201) {
|
||||
formData.link_logo = response.data.url || response.data.path;
|
||||
ElMessage.success(
|
||||
response.code === 201 ? "文件已存在,直接使用" : "图片上传成功",
|
||||
);
|
||||
} else {
|
||||
ElMessage.error(response.msg || "图片上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 图片上传失败
|
||||
const handleImageError = () => {
|
||||
ElMessage.error("图片上传失败,请重试");
|
||||
};
|
||||
|
||||
// 删除图片
|
||||
const handleRemoveImage = () => {
|
||||
formData.link_logo = "";
|
||||
ElMessage.success("图片已删除");
|
||||
};
|
||||
|
||||
// 获取图片完整URL(用于预览)
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
const formData = reactive({
|
||||
title: "",
|
||||
desc: "",
|
||||
thumb: "",
|
||||
url: "",
|
||||
sort: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
title: [
|
||||
{ required: true, message: "请输入服务名称", trigger: "blur" },
|
||||
{ max: 100, message: "长度不超过100个字符", trigger: "blur" },
|
||||
],
|
||||
desc: [
|
||||
{ required: true, message: "请输入服务描述", trigger: "blur" },
|
||||
{ max: 200, message: "描述长度不超过200个字符", trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
visible.value = val;
|
||||
if (val) {
|
||||
resetForm();
|
||||
if (props.isEdit && props.rowData) {
|
||||
Object.assign(formData, props.rowData);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (val) => {
|
||||
emit("update:modelValue", val);
|
||||
});
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.title = "";
|
||||
formData.desc = "";
|
||||
formData.thumb = "";
|
||||
formData.url = "";
|
||||
formData.sort = 0;
|
||||
formData.status = 1;
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return;
|
||||
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
let res;
|
||||
if (props.isEdit) {
|
||||
res = await updateService(props.rowData.id, formData);
|
||||
} else {
|
||||
res = await addService(formData);
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(props.isEdit ? "更新成功" : "添加成功");
|
||||
handleClose();
|
||||
emit("success");
|
||||
} else {
|
||||
ElMessage.error(res.msg || "操作失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("提交失败:", error);
|
||||
ElMessage.error("操作失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* 图片上传样式 */
|
||||
.image-uploader {
|
||||
border: 1px dashed var(--el-border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: var(--el-transition-duration-fast);
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.image-uploader:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.upload-placeholder {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.image-uploader-icon {
|
||||
font-size: 28px;
|
||||
color: #8c939d;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.el-upload__text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
.flex-direction {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>特色服务管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAdd">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加内容
|
||||
</el-button>
|
||||
<el-button @click="refresh" :loading="loading">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<el-input v-model="searchForm.keyword" placeholder="请输入内容搜索" clearable style="width: 200px; margin-right: 10px"
|
||||
@keyup.enter="handleSearch" />
|
||||
<el-select v-model="searchForm.status" placeholder="状态筛选" clearable style="width: 120px; margin-right: 10px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon>
|
||||
<Search />
|
||||
</el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table :data="servicesList" style="width: 100%" v-loading="loading" @selection-change="handleSelectionChange">
|
||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||
<el-table-column prop="thumb" label="服务图片" min-width="150" align="center" />
|
||||
<el-table-column prop="title" label="服务名称" min-width="150" align="center" />
|
||||
<el-table-column prop="desc" label="服务描述" min-width="150" align="center" />
|
||||
<el-table-column prop="url" label="跳转地址" min-width="150" align="center" />
|
||||
<el-table-column prop="sort" label="排序" width="80" align="center" />
|
||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center" />
|
||||
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEdit(row)">
|
||||
<el-icon>
|
||||
<Edit />
|
||||
</el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-container">
|
||||
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
|
||||
:page-sizes="[10, 20, 50, 100]" :total="pagination.total" layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange" @current-change="handlePageChange" />
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑组件 -->
|
||||
<EditDialog v-model="dialogVisible" :title="dialogTitle" :is-edit="isEdit" :row-data="currentRow"
|
||||
@success="handleSuccess" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Refresh, Search, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import {
|
||||
getServiceList,
|
||||
updateService,
|
||||
deleteService
|
||||
} from '@/api/services'
|
||||
import EditDialog from './components/edit.vue'
|
||||
|
||||
// @ts-ignore
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
|
||||
// 获取图片完整URL
|
||||
const getImageUrl = (imagePath) => {
|
||||
if (!imagePath) return "";
|
||||
if (imagePath.startsWith("http")) return imagePath;
|
||||
return API_BASE_URL + imagePath;
|
||||
};
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
keyword: '',
|
||||
status: ''
|
||||
})
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0
|
||||
})
|
||||
|
||||
// 数据列表
|
||||
const servicesList = ref([])
|
||||
const selectedIds = ref([])
|
||||
|
||||
// 对话框
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('')
|
||||
const isEdit = ref(false)
|
||||
const currentRow = ref({})
|
||||
|
||||
// 获取列表
|
||||
const fetchList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getServiceList({
|
||||
page: pagination.page,
|
||||
limit: pagination.limit,
|
||||
keyword: searchForm.keyword,
|
||||
status: searchForm.status
|
||||
})
|
||||
if (res.code === 200) {
|
||||
servicesList.value = res.data.list
|
||||
pagination.total = res.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取特色服务列表失败:', error)
|
||||
ElMessage.error('获取特色服务列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchForm.keyword = ''
|
||||
searchForm.status = ''
|
||||
pagination.page = 1
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 分页变化
|
||||
const handleSizeChange = (val) => {
|
||||
pagination.limit = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
const handlePageChange = (val) => {
|
||||
pagination.page = val
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(item => item.id)
|
||||
}
|
||||
|
||||
// 添加
|
||||
const handleAdd = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加特色服务'
|
||||
currentRow.value = {}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑特色服务'
|
||||
currentRow.value = row
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该特色服务吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
|
||||
const res = await deleteService(row.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除特色服务失败:', error)
|
||||
ElMessage.error('删除特色服务失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 状态变化
|
||||
const handleStatusChange = async (row, val) => {
|
||||
try {
|
||||
const res = await updateService(row.id, { status: val })
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('状态更新成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '状态更新失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新特色服务状态失败:', error)
|
||||
ElMessage.error('更新特色服务状态失败')
|
||||
row.status = val === 1 ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑成功回调
|
||||
const handleSuccess = () => {
|
||||
fetchList()
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container-box {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header-bar h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pagination-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div class="template-layout">
|
||||
<aside class="category-sidebar">
|
||||
<div class="sidebar-title">模板管理</div>
|
||||
<ul class="category-list">
|
||||
<li class="current-theme">
|
||||
<span>当前使用:</span>
|
||||
<el-tag type="success">{{ currentTheme }}</el-tag>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<main class="content-area">
|
||||
<header class="toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="result-count">共 {{ templateList.length }} 个模板</span>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<el-button type="primary" @click="fetchTemplates" :loading="loading">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新模板
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<span>加载模板中...</span>
|
||||
</div>
|
||||
|
||||
<!-- 模板列表 -->
|
||||
<div v-else class="template-grid">
|
||||
<div v-for="item in templateList" :key="item.key" class="template-card" :class="{ active: item.key === currentTheme }">
|
||||
<div class="card-preview">
|
||||
<img :src="getPreviewUrl(item.preview)" alt="preview" @error="handleImageError($event)" />
|
||||
<div v-if="item.key === currentTheme" class="current-tag">
|
||||
<el-tag type="success" size="small">使用中</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-info">
|
||||
<h4 class="title">{{ item.name }}</h4>
|
||||
<p class="description">{{ item.description }}</p>
|
||||
<div class="meta">
|
||||
<span class="version">v{{ item.version }}</span>
|
||||
<span v-if="item.author" class="author">{{ item.author }}</span>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<el-button
|
||||
v-if="item.key !== currentTheme"
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleUse(item)"
|
||||
:loading="switching === item.key"
|
||||
>
|
||||
启用
|
||||
</el-button>
|
||||
<el-button v-else type="info" size="small" disabled>
|
||||
当前使用
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Refresh, Loading } from '@element-plus/icons-vue'
|
||||
import { getThemeList, switchTheme } from '@/api/theme'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
|
||||
const authStore = useAuthStore()
|
||||
|
||||
// 状态
|
||||
const loading = ref(false)
|
||||
const switching = ref('')
|
||||
const templateList = ref<any[]>([])
|
||||
const currentTheme = ref('default')
|
||||
const currentTid = ref<number>(0)
|
||||
|
||||
// 获取完整预览图URL
|
||||
const getPreviewUrl = (path: string) => {
|
||||
if (!path) return ''
|
||||
if (path.startsWith('http')) return path
|
||||
return API_BASE_URL + path
|
||||
}
|
||||
|
||||
// 获取模板列表
|
||||
const fetchTemplates = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
currentTid.value = (authStore.user as any)?.tid || 0
|
||||
const res = await getThemeList({ tid: currentTid.value })
|
||||
if (res.code === 200) {
|
||||
templateList.value = res.data.list || []
|
||||
currentTheme.value = res.data.currentTheme || 'default'
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取模板列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取模板列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 切换模板
|
||||
const handleUse = async (item: any) => {
|
||||
if (!currentTid.value) {
|
||||
ElMessage.error('请先选择租户')
|
||||
return
|
||||
}
|
||||
switching.value = item.key
|
||||
try {
|
||||
const res = await switchTheme({ tid: currentTid.value, theme_key: item.key })
|
||||
if (res.code === 200) {
|
||||
currentTheme.value = item.key
|
||||
ElMessage.success('切换成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '切换失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('切换失败')
|
||||
} finally {
|
||||
switching.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
// 图片加载失败处理
|
||||
const handleImageError = (event: Event) => {
|
||||
const img = event.target as HTMLImageElement
|
||||
img.src = 'https://picsum.photos/300/200?random=1'
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
fetchTemplates()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.template-layout {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
background-color: #f0f2f5;
|
||||
|
||||
.category-sidebar {
|
||||
width: 240px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
padding: 20px 0;
|
||||
|
||||
.sidebar-title {
|
||||
padding: 0 24px 16px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.current-theme {
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex: 1;
|
||||
padding: 0 24px 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
|
||||
.result-count {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px;
|
||||
color: #999;
|
||||
gap: 10px;
|
||||
|
||||
.el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.template-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.template-card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 2px solid #f0f0f0;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.active {
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.card-preview {
|
||||
height: 200px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #f5f5f5;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.current-tag {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-info {
|
||||
padding: 16px;
|
||||
|
||||
.title {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin: 0 0 12px 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -164,7 +164,7 @@ onMounted(() => {
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
<style lang="less" scoped>
|
||||
.exam-workbench {
|
||||
padding: 20px;
|
||||
|
||||
|
||||
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="erp-dashboard">
|
||||
<!-- 第一行统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row">
|
||||
<el-col :span="4" v-for="(item, index) in firstRowStats" :key="'first-' + index">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
<el-icon class="stat-icon"><Info-Filled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
<span class="currency">¥</span>
|
||||
<span class="number">{{ formatMoney(item.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 第二行统计卡片 -->
|
||||
<el-row :gutter="16" class="stat-row" style="margin-top: 16px;">
|
||||
<el-col :span="4" v-for="(item, index) in secondRowStats" :key="'second-' + index">
|
||||
<div class="stat-card">
|
||||
<div class="stat-header">
|
||||
<span class="stat-label">{{ item.label }}</span>
|
||||
<el-icon class="stat-icon"><Info-Filled /></el-icon>
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
<span class="currency">¥</span>
|
||||
<span class="number">{{ formatMoney(item.value) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 图表区域 -->
|
||||
<el-row :gutter="16" class="chart-row" style="margin-top: 16px;">
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">销售统计</div>
|
||||
<div ref="salesChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">零售统计</div>
|
||||
<div ref="retailChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="chart-card">
|
||||
<div class="chart-title">采购统计</div>
|
||||
<div ref="purchaseChartRef" class="chart-container"></div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import { InfoFilled } from '@element-plus/icons-vue';
|
||||
|
||||
// 统计卡片数据 - 第一行
|
||||
const firstRowStats = ref([
|
||||
{ label: '今日销售', value: 0 },
|
||||
{ label: '今日零售', value: 0 },
|
||||
{ label: '今日采购', value: 0 },
|
||||
{ label: '本月累计销售', value: 0 },
|
||||
{ label: '本月累计零售', value: 0 },
|
||||
{ label: '本月累计采购', value: 0 },
|
||||
]);
|
||||
|
||||
// 统计卡片数据 - 第二行
|
||||
const secondRowStats = ref([
|
||||
{ label: '昨日销售', value: 0 },
|
||||
{ label: '昨日零售', value: 0 },
|
||||
{ label: '昨日采购', value: 0 },
|
||||
{ label: '今年累计销售', value: 0 },
|
||||
{ label: '今年累计零售', value: 0 },
|
||||
{ label: '今年累计采购', value: 0 },
|
||||
]);
|
||||
|
||||
// 图表实例
|
||||
const salesChartRef = ref<HTMLElement | null>(null);
|
||||
const retailChartRef = ref<HTMLElement | null>(null);
|
||||
const purchaseChartRef = ref<HTMLElement | null>(null);
|
||||
const salesChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const retailChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
const purchaseChartInstance = shallowRef<echarts.ECharts | null>(null);
|
||||
|
||||
// 格式化金额
|
||||
const formatMoney = (value: number): string => {
|
||||
return value.toLocaleString();
|
||||
};
|
||||
|
||||
// 生成最近6个月的月份标签
|
||||
const getMonthLabels = (): string[] => {
|
||||
const labels: string[] = [];
|
||||
const now = new Date();
|
||||
for (let i = 5; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
labels.push(`${year}-${month}`);
|
||||
}
|
||||
return labels;
|
||||
};
|
||||
|
||||
// 初始化图表通用配置
|
||||
const initChart = (refEl: HTMLElement, title: string, color: string) => {
|
||||
const chart = echarts.init(refEl);
|
||||
const months = getMonthLabels();
|
||||
|
||||
chart.setOption({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross' }
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
top: '15%',
|
||||
containLabel: true
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: months,
|
||||
axisLine: { lineStyle: { color: '#E0E6ED' } },
|
||||
axisLabel: { color: '#606266', fontSize: 11 }
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: '#F2F6FC' } },
|
||||
axisLabel: { color: '#606266', fontSize: 11 }
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: title,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
symbol: 'circle',
|
||||
symbolSize: 6,
|
||||
lineStyle: { color: color, width: 2 },
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: color + '40' },
|
||||
{ offset: 1, color: color + '10' }
|
||||
])
|
||||
},
|
||||
data: [0, 0, 0, 0, 0, 0]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return chart;
|
||||
};
|
||||
|
||||
// 初始化所有图表
|
||||
const initCharts = () => {
|
||||
if (salesChartRef.value) {
|
||||
salesChartInstance.value = initChart(salesChartRef.value, '销售', '#409EFF');
|
||||
}
|
||||
if (retailChartRef.value) {
|
||||
retailChartInstance.value = initChart(retailChartRef.value, '零售', '#67C23A');
|
||||
}
|
||||
if (purchaseChartRef.value) {
|
||||
purchaseChartInstance.value = initChart(purchaseChartRef.value, '采购', '#E6A23C');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载数据(模拟接口调用)
|
||||
const loadData = async () => {
|
||||
// TODO: 调用接口获取真实数据
|
||||
// const res = await getErpDashboard();
|
||||
|
||||
// 模拟数据更新
|
||||
firstRowStats.value = [
|
||||
{ label: '今日销售', value: 12580 },
|
||||
{ label: '今日零售', value: 8560 },
|
||||
{ label: '今日采购', value: 23400 },
|
||||
{ label: '本月累计销售', value: 358600 },
|
||||
{ label: '本月累计零售', value: 245800 },
|
||||
{ label: '本月累计采购', value: 568900 },
|
||||
];
|
||||
|
||||
secondRowStats.value = [
|
||||
{ label: '昨日销售', value: 15230 },
|
||||
{ label: '昨日零售', value: 9870 },
|
||||
{ label: '昨日采购', value: 18900 },
|
||||
{ label: '今年累计销售', value: 2856000 },
|
||||
{ label: '今年累计零售', value: 1985000 },
|
||||
{ label: '今年累计采购', value: 4568000 },
|
||||
];
|
||||
|
||||
// 更新图表数据
|
||||
updateCharts();
|
||||
};
|
||||
|
||||
// 更新图表数据
|
||||
const updateCharts = () => {
|
||||
const salesData = [28000, 32000, 35000, 38000, 42000, 358600];
|
||||
const retailData = [18000, 21000, 22000, 25000, 28000, 245800];
|
||||
const purchaseData = [45000, 48000, 52000, 55000, 58000, 568900];
|
||||
|
||||
if (salesChartInstance.value) {
|
||||
salesChartInstance.value.setOption({ series: [{ data: salesData }] });
|
||||
}
|
||||
if (retailChartInstance.value) {
|
||||
retailChartInstance.value.setOption({ series: [{ data: retailData }] });
|
||||
}
|
||||
if (purchaseChartInstance.value) {
|
||||
purchaseChartInstance.value.setOption({ series: [{ data: purchaseData }] });
|
||||
}
|
||||
};
|
||||
|
||||
// 自适应窗口大小
|
||||
const handleResize = () => {
|
||||
salesChartInstance.value?.resize();
|
||||
retailChartInstance.value?.resize();
|
||||
purchaseChartInstance.value?.resize();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
initCharts();
|
||||
await nextTick();
|
||||
await loadData();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
salesChartInstance.value?.dispose();
|
||||
retailChartInstance.value?.dispose();
|
||||
purchaseChartInstance.value?.dispose();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.erp-dashboard {
|
||||
background-color: #f5f7fa;
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.stat-row {
|
||||
.el-col {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.stat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 14px;
|
||||
color: #c0c4cc;
|
||||
cursor: help;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
|
||||
.currency {
|
||||
font-size: 14px;
|
||||
color: #303133;
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.number {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-row {
|
||||
.el-col {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
||||
height: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.chart-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="修改密码" width="420px" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
|
||||
<el-form-item label="用户">
|
||||
<span>{{ currentUser.name || currentUser.account || "-" }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="新密码" prop="password">
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
placeholder="请输入新密码"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
placeholder="请再次输入新密码"
|
||||
show-password
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from "vue";
|
||||
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
|
||||
import { changePassword } from "@/api/user";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "submit"): void;
|
||||
}>();
|
||||
|
||||
const visible = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const formRef = ref<FormInstance>();
|
||||
const currentUser = ref<any>({});
|
||||
|
||||
const form = reactive({
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
|
||||
const validateConfirmPassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入新密码"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (value !== form.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
const rules: FormRules = {
|
||||
password: [
|
||||
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||||
{ min: 6, message: "密码长度不能少于 6 位", trigger: "blur" },
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: "请再次输入新密码", trigger: "blur" },
|
||||
{ validator: validateConfirmPassword, trigger: "blur" },
|
||||
],
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.password = "";
|
||||
form.confirmPassword = "";
|
||||
formRef.value?.clearValidate();
|
||||
};
|
||||
|
||||
const openChangePass = (row: any) => {
|
||||
currentUser.value = row || {};
|
||||
resetForm();
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!currentUser.value?.id) {
|
||||
ElMessage.error("用户 ID 不存在");
|
||||
return;
|
||||
}
|
||||
|
||||
await formRef.value?.validate();
|
||||
|
||||
submitLoading.value = true;
|
||||
try {
|
||||
await changePassword(currentUser.value.id, {
|
||||
newPassword: form.password,
|
||||
});
|
||||
|
||||
ElMessage.success("密码修改成功");
|
||||
visible.value = false;
|
||||
emit("submit");
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.response?.data?.message || e?.message || "密码修改失败");
|
||||
} finally {
|
||||
submitLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
openChangePass,
|
||||
open: openChangePass,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,583 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
|
||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||
<div class="form-title">账号信息</div>
|
||||
<!-- 账号 -->
|
||||
<el-form-item label="账号" required>
|
||||
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 密码 -->
|
||||
<el-form-item label="密码" prop="password" v-if="isAdd" required>
|
||||
<el-input
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 确认密码 -->
|
||||
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd" required>
|
||||
<el-input
|
||||
v-model="form.confirmPassword"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
show-password
|
||||
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<div class="form-title">个人信息</div>
|
||||
<!-- 姓名 -->
|
||||
<el-form-item label="姓名" required>
|
||||
<el-input v-model="form.name" placeholder="请输入姓名" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 性别 -->
|
||||
<el-form-item label="性别" required>
|
||||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
||||
<el-radio-button label="未知" :value="0" />
|
||||
<el-radio-button label="男" :value="1" />
|
||||
<el-radio-button label="女" :value="2" />
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 生日 -->
|
||||
<el-form-item label="生日" required>
|
||||
<el-date-picker
|
||||
v-model="form.birthday"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择生日"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 隶属单位 -->
|
||||
<el-form-item label="隶属单位" required>
|
||||
<el-select v-model="form.affiliate_unit" placeholder="请选择隶属单位" clearable style="width: 100%">
|
||||
<el-option v-for="item in companyList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 部门 -->
|
||||
<el-form-item label="部门" required>
|
||||
<el-select v-model="form.department" placeholder="请先选择隶属单位" clearable style="width: 100%" :disabled="!form.affiliate_unit">
|
||||
<el-option v-for="item in departmentList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 职位 -->
|
||||
<el-form-item label="职位" required>
|
||||
<el-input v-model="form.position" placeholder="请输入职位" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 学历 -->
|
||||
<el-form-item label="学历">
|
||||
<el-input v-model="form.education" placeholder="请输入学历" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 民族 -->
|
||||
<el-form-item label="民族" required>
|
||||
<el-input v-model="form.nation" placeholder="请输入民族" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 手机 -->
|
||||
<el-form-item label="手机" required>
|
||||
<el-input v-model="form.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 微信 -->
|
||||
<el-form-item label="微信">
|
||||
<el-input v-model="form.wechat" placeholder="请输入微信" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 邮箱 -->
|
||||
<el-form-item label="邮箱" required>
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 家庭住址 -->
|
||||
<el-form-item label="家庭住址" required>
|
||||
<el-input v-model="form.home_address" placeholder="请输入家庭住址" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 状态 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select
|
||||
v-model="form.account_status"
|
||||
placeholder="请选择状态"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option label="离职" :value="2" v-if="!isAdd" />
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 对话框脚部 -->
|
||||
<template #footer>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { createEmployee, editEmployee, getEmployeeDetail, getCompanys, getDepartments } from "@/api/erp";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "submit", "close"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const formRef = ref<any>(null);
|
||||
const isAdd = ref(false);
|
||||
const companyList = ref<any[]>([]);
|
||||
const departmentList = ref<any[]>([]);
|
||||
const isHydratingUserData = ref(false);
|
||||
|
||||
// 获取公司列表
|
||||
const loadCompanyList = async () => {
|
||||
try {
|
||||
const res = await getCompanys();
|
||||
companyList.value = res.data || res || [];
|
||||
} catch (e) {
|
||||
console.error("获取公司列表失败", e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 获取部门列表
|
||||
const loadDepartments = async (parentId?: number) => {
|
||||
try {
|
||||
const res = await getDepartments(parentId);
|
||||
departmentList.value = res.data || res || [];
|
||||
} catch (e) {
|
||||
console.error("获取部门列表失败", e);
|
||||
}
|
||||
};
|
||||
|
||||
const form = ref<any>({
|
||||
id: null,
|
||||
tid: 1,
|
||||
tenant_id: 1,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 0,
|
||||
birthday: "",
|
||||
affiliate_unit: "",
|
||||
department: "",
|
||||
position: "",
|
||||
education: "",
|
||||
nation: "",
|
||||
wechat: "",
|
||||
home_address: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
account_status: 1,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
// 监听隶属单位变化,加载对应部门
|
||||
watch(() => form.value.affiliate_unit, (newVal) => {
|
||||
if (isHydratingUserData.value) {
|
||||
return;
|
||||
}
|
||||
form.value.department = "";
|
||||
departmentList.value = [];
|
||||
if (newVal) {
|
||||
loadDepartments(Number(newVal));
|
||||
}
|
||||
});
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
return isAdd.value ? "添加用户" : "编辑用户";
|
||||
});
|
||||
|
||||
// 密码验证规则
|
||||
const validatePassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须符合规则
|
||||
if (value && value.length < 6) {
|
||||
callback(new Error("密码长度不能少于6位"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 确认密码验证规则
|
||||
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,确认密码必填
|
||||
if (!value) {
|
||||
callback(new Error("请再次输入密码"));
|
||||
return;
|
||||
}
|
||||
if (value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则确认密码必须一致
|
||||
if (form.value.password && value !== form.value.password) {
|
||||
callback(new Error("两次输入的密码不一致"));
|
||||
return;
|
||||
}
|
||||
// 如果填写了确认密码但没填密码,提示错误
|
||||
if (value && !form.value.password) {
|
||||
callback(new Error("请先输入密码"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
account: [
|
||||
{ required: true, message: "请输入账号", trigger: "blur" },
|
||||
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
|
||||
],
|
||||
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
|
||||
password: [{ validator: validatePassword, trigger: "blur" }],
|
||||
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
|
||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
|
||||
};
|
||||
|
||||
// 监听 modelValue
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
visible.value = newVal;
|
||||
}
|
||||
);
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
if (!newVal) {
|
||||
emit("update:modelValue", false);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 statusDict 变化,用于调试
|
||||
watch(
|
||||
() => props.statusDict,
|
||||
(newVal) => {},
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
// 处理两种调用方式:传递用户对象或用户 ID
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getEmployeeDetail(userId);
|
||||
|
||||
const data = res.data || res;
|
||||
|
||||
// 确保 sex 和 status 都是数字类型
|
||||
const sexValue =
|
||||
data.sex !== undefined && data.sex !== null
|
||||
? Number(data.sex)
|
||||
: data.gender !== undefined && data.gender !== null
|
||||
? Number(data.gender)
|
||||
: 0;
|
||||
|
||||
const statusValue =
|
||||
data.account_status !== undefined && data.account_status !== null
|
||||
? Number(data.account_status)
|
||||
: data.status !== undefined && data.status !== null
|
||||
? Number(data.status)
|
||||
: 1;
|
||||
|
||||
const affiliateUnitValue =
|
||||
data.affiliate_unit !== undefined && data.affiliate_unit !== null
|
||||
? String(data.affiliate_unit)
|
||||
: "";
|
||||
const departmentValue =
|
||||
data.department !== undefined && data.department !== null
|
||||
? String(data.department)
|
||||
: "";
|
||||
|
||||
isHydratingUserData.value = true;
|
||||
form.value = {
|
||||
id: data.id,
|
||||
tid: data.tid || data.tenant_id || 1,
|
||||
tenant_id: data.tenant_id || data.tid || 1,
|
||||
account: data.account || "",
|
||||
name: data.name || "",
|
||||
phone: data.phone || "",
|
||||
qq: data.qq || "",
|
||||
sex: sexValue,
|
||||
birthday: data.birthday || "",
|
||||
affiliate_unit: affiliateUnitValue,
|
||||
department: departmentValue,
|
||||
position: data.position || "",
|
||||
education: data.education || "",
|
||||
nation: data.nation || data.nationality || "",
|
||||
wechat: data.wechat || "",
|
||||
home_address: data.home_address || "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: data.email || "",
|
||||
account_status: statusValue,
|
||||
status: statusValue,
|
||||
};
|
||||
|
||||
await nextTick();
|
||||
|
||||
departmentList.value = [];
|
||||
if (form.value.affiliate_unit) {
|
||||
await loadDepartments(Number(form.value.affiliate_unit));
|
||||
}
|
||||
form.value.department = departmentValue;
|
||||
isHydratingUserData.value = false;
|
||||
} catch (e: any) {
|
||||
console.error("Failed to load user data:", e);
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
// 表单验证
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await formRef.value.validate();
|
||||
} catch (error) {
|
||||
ElMessage.warning("请检查表单填写是否正确");
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证密码一致性
|
||||
if (isAdd.value) {
|
||||
// 新增用户时,密码必填
|
||||
if (!form.value.password) {
|
||||
ElMessage.error("请输入密码");
|
||||
return;
|
||||
}
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 编辑用户时,如果填写了密码,则必须填写确认密码且一致
|
||||
if (form.value.password) {
|
||||
if (form.value.password !== form.value.confirmPassword) {
|
||||
ElMessage.error("两次输入的密码不一致");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (isAdd.value) {
|
||||
// 新增用户
|
||||
const submitData: any = {
|
||||
tid: form.value.tid,
|
||||
tenant_id: form.value.tenant_id,
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
qq: form.value.qq,
|
||||
sex: form.value.sex,
|
||||
birthday: form.value.birthday,
|
||||
affiliate_unit: form.value.affiliate_unit,
|
||||
department: form.value.department,
|
||||
position: form.value.position,
|
||||
education: form.value.education,
|
||||
nation: form.value.nation,
|
||||
wechat: form.value.wechat,
|
||||
email: form.value.email,
|
||||
home_address: form.value.home_address,
|
||||
account_status: form.value.account_status,
|
||||
status: form.value.account_status,
|
||||
password: form.value.password,
|
||||
};
|
||||
|
||||
await createEmployee(submitData);
|
||||
ElMessage.success("添加成功");
|
||||
} else {
|
||||
// 编辑用户
|
||||
if (!form.value.id || form.value.id === 0) {
|
||||
ElMessage.error("用户ID不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const submitData: any = {
|
||||
id: form.value.id,
|
||||
tid: form.value.tid,
|
||||
tenant_id: form.value.tenant_id,
|
||||
account: form.value.account,
|
||||
name: form.value.name,
|
||||
phone: form.value.phone,
|
||||
qq: form.value.qq,
|
||||
sex: form.value.sex,
|
||||
birthday: form.value.birthday,
|
||||
affiliate_unit: form.value.affiliate_unit,
|
||||
department: form.value.department,
|
||||
position: form.value.position,
|
||||
education: form.value.education,
|
||||
nation: form.value.nation,
|
||||
wechat: form.value.wechat,
|
||||
email: form.value.email,
|
||||
home_address: form.value.home_address,
|
||||
account_status: form.value.account_status,
|
||||
status: form.value.account_status,
|
||||
};
|
||||
|
||||
// 只有在填写了密码时才添加到提交数据中
|
||||
if (form.value.password) {
|
||||
submitData.password = form.value.password;
|
||||
}
|
||||
|
||||
await editEmployee(form.value.id, submitData);
|
||||
ElMessage.success("更新成功");
|
||||
}
|
||||
|
||||
visible.value = false;
|
||||
emit("submit");
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
|
||||
ElMessage.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openAdd: () => {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
tid: 1,
|
||||
tenant_id: 1,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 0,
|
||||
birthday: "",
|
||||
affiliate_unit: "",
|
||||
department: "",
|
||||
position: "",
|
||||
education: "",
|
||||
nation: "",
|
||||
wechat: "",
|
||||
home_address: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
account_status: 1,
|
||||
status: 1,
|
||||
};
|
||||
loadCompanyList();
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
openEdit: (user: any) => {
|
||||
isAdd.value = false;
|
||||
loadCompanyList();
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
// 异步加载用户详细信息
|
||||
loadUserData(user);
|
||||
},
|
||||
open: (user?: any) => {
|
||||
loadCompanyList();
|
||||
if (user) {
|
||||
isAdd.value = false;
|
||||
loadUserData(user);
|
||||
} else {
|
||||
isAdd.value = true;
|
||||
form.value = {
|
||||
id: 0,
|
||||
tid: 1,
|
||||
tenant_id: 1,
|
||||
account: "",
|
||||
name: "",
|
||||
phone: "",
|
||||
qq: "",
|
||||
sex: 0,
|
||||
birthday: "",
|
||||
affiliate_unit: "",
|
||||
department: "",
|
||||
position: "",
|
||||
education: "",
|
||||
nation: "",
|
||||
wechat: "",
|
||||
home_address: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
email: "",
|
||||
account_status: 1,
|
||||
status: 1,
|
||||
};
|
||||
}
|
||||
visible.value = true;
|
||||
// 清除表单验证
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<el-drawer v-model="visible" title="查看用户" width="500px">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="ID">{{ detail.id || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号">{{ detail.account || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="姓名">{{ detail.name || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="性别">{{ genderText }}</el-descriptions-item>
|
||||
<el-descriptions-item label="生日">{{ detail.birthday || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="隶属单位">{{ detail.affiliate_unit_name || detail.affiliate_unit || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="部门">{{ detail.department_name || detail.department || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="职位">{{ detail.position || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="学历">{{ detail.education || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="民族">{{ detail.nation || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">{{ detail.phone || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="微信">{{ detail.wechat || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ detail.email || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="家庭住址">{{ detail.home_address || "-" }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号状态">
|
||||
<el-tag :type="statusValue === 1 ? 'success' : 'danger'">
|
||||
{{ statusText }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { getEmployeeDetail } from "@/api/erp";
|
||||
|
||||
const visible = ref(false);
|
||||
const detail = ref<any>({});
|
||||
|
||||
const genderValue = computed(() => {
|
||||
const value = detail.value.sex ?? detail.value.gender ?? 0;
|
||||
return Number(value);
|
||||
});
|
||||
|
||||
const genderText = computed(() => {
|
||||
if (genderValue.value === 1) {
|
||||
return "男";
|
||||
}
|
||||
if (genderValue.value === 2) {
|
||||
return "女";
|
||||
}
|
||||
return "未知";
|
||||
});
|
||||
|
||||
const statusValue = computed(() => {
|
||||
const value = detail.value.account_status ?? detail.value.status ?? 0;
|
||||
return Number(value);
|
||||
});
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (statusValue.value === 1) {
|
||||
return "启用";
|
||||
}
|
||||
if (statusValue.value === 2) {
|
||||
return "离职";
|
||||
}
|
||||
return "禁用";
|
||||
});
|
||||
|
||||
const loadUserData = async (user: any) => {
|
||||
try {
|
||||
const userId = typeof user === "number" ? user : user?.id || user?.userId;
|
||||
if (!userId) {
|
||||
throw new Error("未提供有效的用户 ID");
|
||||
}
|
||||
|
||||
const res = await getEmployeeDetail(userId);
|
||||
detail.value = res.data || res || {};
|
||||
} catch (e: any) {
|
||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||
ElMessage.error(errorMsg);
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const openView = async (user: any) => {
|
||||
visible.value = true;
|
||||
await loadUserData(user);
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
loadUserData,
|
||||
openView,
|
||||
open: openView,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<div class="container-box">
|
||||
<div class="header-bar">
|
||||
<h2>用户管理</h2>
|
||||
<div class="header-actions">
|
||||
<el-button type="primary" @click="handleAddUser">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
添加用户
|
||||
</el-button>
|
||||
<el-button @click="refresh">
|
||||
<el-icon>
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider></el-divider>
|
||||
|
||||
<!-- 用户列表表格 -->
|
||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" align="center" fixed="left" width="80" />
|
||||
<el-table-column prop="account" label="账号" align="center" width="120" />
|
||||
<el-table-column prop="name" label="姓名" align="center" width="120">
|
||||
<template #default="scope">
|
||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="隶属单位" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.affiliate_unit_name || scope.row.affiliate_unit || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="部门" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.department_name || scope.row.department || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="position" label="职位" align="center" />
|
||||
<el-table-column prop="account_status" label="账号状态" width="80" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="getStatusValue(scope.row) === 1 ? 'success' : 'danger'">{{
|
||||
getStatusValue(scope.row) === 1 ? "启用" : getStatusValue(scope.row) === 2 ? "离职" : "禁用"
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||
<template #default="scope"><!-- -->
|
||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
||||
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
|
||||
修改密码
|
||||
</el-button>
|
||||
<el-button v-if="scope.row.username !== 'admin' && scope.row.id !== 1" size="small" type="danger"
|
||||
@click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-bar">
|
||||
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
|
||||
layout="total, prev, pager, next" />
|
||||
</div>
|
||||
|
||||
<!-- 查看弹窗 -->
|
||||
<View ref="viewRef" />
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<ChangePass ref="changePassRef" @submit="fetchData" />
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<Edit ref="editRef" @submit="fetchData" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||
import { getEmployeeList, deleteEmployee } from "@/api/erp";
|
||||
import Edit from "./components/edit.vue";
|
||||
import View from "./components/view.vue";
|
||||
import ChangePass from "./components/changepass.vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const users = ref<any[]>([]);
|
||||
const loading = ref(false);
|
||||
const page = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const total = ref(0);
|
||||
const editRef = ref<any>(null);
|
||||
const viewRef = ref<any>(null);
|
||||
const changePassRef = ref<any>(null);
|
||||
|
||||
const getStatusValue = (row: any) => {
|
||||
const status = row.account_status ?? row.status ?? 0;
|
||||
return Number(status);
|
||||
};
|
||||
|
||||
const authStore = useAuthStore();
|
||||
const tenantId = (authStore.user as any)?.tid;
|
||||
|
||||
// console.log(tenantId);
|
||||
|
||||
// 获取列表数据
|
||||
const fetchData = async (currentTenantId = tenantId) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getEmployeeList(currentTenantId);
|
||||
users.value = res.data || res || [];
|
||||
total.value = users.value.length;
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e?.message || "获取列表失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 刷新
|
||||
const refresh = () => {
|
||||
fetchData(tenantId);
|
||||
};
|
||||
|
||||
// 添加用户
|
||||
const handleAddUser = () => {
|
||||
editRef.value?.openAdd();
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (row: any) => {
|
||||
editRef.value?.openEdit(row);
|
||||
};
|
||||
|
||||
const handleChangePassword = (row: any) => {
|
||||
changePassRef.value?.openChangePass(row);
|
||||
};
|
||||
|
||||
// 查看
|
||||
const handlePreview = (row: any) => {
|
||||
viewRef.value?.openView(row);
|
||||
};
|
||||
|
||||
// 删除用户
|
||||
const handleDelete = async (row: any) => {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除用户 "${row.name}" 吗?`, "提示", {
|
||||
type: "warning",
|
||||
});
|
||||
await deleteEmployee(row.id);
|
||||
ElMessage.success("删除成功");
|
||||
fetchData(tenantId);
|
||||
} catch (e: any) {
|
||||
if (e !== "cancel") {
|
||||
ElMessage.error(e?.message || "删除失败");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 分页变化
|
||||
const handlePageChange = (val: number) => {
|
||||
page.value = val;
|
||||
fetchData(tenantId);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchData(tenantId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-alert__title) {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
|
||||
.name-link {
|
||||
color: #3973ff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: #66b1ff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,5 +4,5 @@
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="less" scoped></style>
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" :title="isEdit ? '编辑组织' : '添加组织'" width="500px" :close-on-click-modal="false">
|
||||
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
|
||||
|
||||
<el-form-item label="上级组织" prop="parent_id">
|
||||
<el-tree-select v-model="formData.parent_id" :data="extendedTreeData"
|
||||
:props="props.treeProps || defaultTreeProps" check-strictly placeholder="请选择上级组织" clearable
|
||||
style="width: 100%" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="组织名称" prop="org_name">
|
||||
<el-input v-model="formData.org_name" placeholder="请输入组织名称,例如:连云港云泽集团" />
|
||||
</el-form-item>
|
||||
<el-form-item label="组织编码" prop="org_code">
|
||||
<el-input v-model="formData.org_code" placeholder="请输入组织编码,例如:LYG-YZ-GROUP" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="leader_id">
|
||||
<el-select v-model="formData.leader_id" placeholder="请选择负责人" filterable clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in leaderList"
|
||||
:key="item.id"
|
||||
:label="item.name || item.account || String(item.id)"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否是公司" prop="is_company">
|
||||
<el-radio-group v-model="formData.is_company">
|
||||
<el-radio :label="1">是</el-radio>
|
||||
<el-radio :label="0">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="handleClose">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||||
import { createOrganization, editOrganization } from '@/api/erp'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { getTenantUsers } from '@/api/user'
|
||||
|
||||
interface TreeNode {
|
||||
id: number
|
||||
org_name: string
|
||||
org_code?: string
|
||||
parent_id?: number
|
||||
parent_name?: string
|
||||
leader_id?: number
|
||||
sort?: number
|
||||
status?: number
|
||||
is_company?: number
|
||||
remark?: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
treeData: TreeNode[]
|
||||
treeProps?: {
|
||||
label: string
|
||||
children: string
|
||||
value: string
|
||||
}
|
||||
}>()
|
||||
|
||||
// 默认 treeProps
|
||||
const defaultTreeProps = {
|
||||
label: 'org_name',
|
||||
children: 'children',
|
||||
value: 'id'
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
success: []
|
||||
}>()
|
||||
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
const isEdit = ref(false)
|
||||
const authStore = useAuthStore()
|
||||
const tenantId = (authStore.user as any)?.tid
|
||||
const leaderList = ref<Array<{ id: number; name?: string; account?: string }>>([])
|
||||
const formData = reactive({
|
||||
id: 0,
|
||||
parent_id: undefined as number | undefined,
|
||||
org_name: '',
|
||||
org_code: '',
|
||||
leader_id: undefined as number | undefined,
|
||||
sort: 0,
|
||||
is_company: 0,
|
||||
status: 1,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const formRules: FormRules = {
|
||||
org_name: [{ required: true, message: '请输入组织名称', trigger: 'blur' }],
|
||||
org_code: [{ required: true, message: '请输入组织编码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const treeOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
id: 0,
|
||||
org_name: '顶级组织', // 显示名称
|
||||
children: props.treeData // 将原有的树结构作为其子节点(或者并列,取决于你的业务)
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 如果你希望“顶级组织”是作为一个独立的根选项,而不是所有节点的父级:
|
||||
const extendedTreeData = computed(() => {
|
||||
const rootNode = { id: 0, org_name: '顶级组织' }
|
||||
return [rootNode, ...props.treeData]
|
||||
})
|
||||
|
||||
const getLeaderList = async () => {
|
||||
if (!tenantId) {
|
||||
leaderList.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const res: any = await getTenantUsers(tenantId)
|
||||
const responseData = res?.data?.code !== undefined ? res.data : res
|
||||
|
||||
if (responseData?.code === 200) {
|
||||
const list = responseData.data?.list || responseData.data || []
|
||||
leaderList.value = list.map((item: any) => ({
|
||||
...item,
|
||||
id: Number(item.id)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
const open = (data?: TreeNode, parentId?: number) => {
|
||||
getLeaderList()
|
||||
if (data) {
|
||||
// 编辑:只拷贝需要的字段,避免把 children 等多余字段带到表单
|
||||
isEdit.value = true
|
||||
formData.id = data.id
|
||||
formData.parent_id = data.parent_id ?? 0
|
||||
formData.org_name = data.org_name
|
||||
formData.org_code = data.org_code || ''
|
||||
formData.leader_id = data.leader_id === undefined || data.leader_id === null ? undefined : Number(data.leader_id)
|
||||
formData.is_company = data.is_company ?? 0
|
||||
formData.sort = data.sort ?? 0
|
||||
formData.status = data.status ?? 1
|
||||
formData.remark = data.remark || ''
|
||||
} else {
|
||||
// 新增
|
||||
isEdit.value = false
|
||||
formData.id = 0
|
||||
formData.parent_id = parentId ?? 0
|
||||
formData.org_name = ''
|
||||
formData.org_code = ''
|
||||
formData.leader_id = undefined
|
||||
formData.is_company = 0
|
||||
formData.sort = 0
|
||||
formData.status = 1
|
||||
formData.remark = ''
|
||||
}
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 提交
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
|
||||
await formRef.value.validate()
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
let res
|
||||
|
||||
|
||||
// 只允许提交到后台的字段白名单
|
||||
const allowedKeys = ['parent_id', 'org_name', 'org_code', 'leader_id', 'sort', 'is_company', 'status', 'remark']
|
||||
|
||||
if (isEdit.value && formData.id) {
|
||||
// 编辑:使用普通对象提交
|
||||
const submitData: Record<string, any> = {}
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData[key] = value
|
||||
}
|
||||
})
|
||||
if (tenantId) {
|
||||
submitData.tid = tenantId
|
||||
}
|
||||
res = await editOrganization(formData.id, submitData)
|
||||
} else {
|
||||
// 新增:使用 FormData 以兼容后端 multipart/form-data
|
||||
const submitData = new FormData()
|
||||
allowedKeys.forEach(key => {
|
||||
const value = (formData as any)[key]
|
||||
if (value !== undefined && value !== null) {
|
||||
submitData.append(key, String(value))
|
||||
}
|
||||
})
|
||||
if (tenantId) {
|
||||
submitData.append('tid', String(tenantId))
|
||||
}
|
||||
res = await createOrganization(submitData)
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '添加成功')
|
||||
emit('success')
|
||||
handleClose()
|
||||
} else {
|
||||
ElMessage.error(res.msg || (isEdit.value ? '编辑失败' : '添加失败'))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
ElMessage.error('提交失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getLeaderList()
|
||||
})
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="organization-container">
|
||||
<el-row :gutter="16">
|
||||
<!-- 左侧:组织架构树 -->
|
||||
<el-col :span="8">
|
||||
<el-card shadow="hover" class="tree-card" v-loading="loading">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>组织架构</span>
|
||||
<el-button type="primary" link @click="handleAddRoot">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="treeProps"
|
||||
node-key="id"
|
||||
default-expand-all
|
||||
:expand-on-click-node="false"
|
||||
@node-click="handleNodeClick"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="tree-node">
|
||||
<span class="node-label">{{ node.label }}</span>
|
||||
<div class="node-actions" @click.stop>
|
||||
<el-button link type="primary" size="small" @click="handleAdd(data)">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="primary" size="small" @click="handleEdit(data)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button link type="danger" size="small" @click="handleDelete(data)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 右侧:详情面板 -->
|
||||
<el-col :span="16">
|
||||
<el-card shadow="hover" class="detail-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>{{ detailTitle }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="selectedNode" class="detail-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="组织名称">
|
||||
{{ selectedNode.org_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="组织编码">
|
||||
{{ selectedNode.org_code || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="上级组织">
|
||||
{{ selectedNode.parent_name || '顶级组织' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ selectedNode.leader_name || '-' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否是公司">
|
||||
<el-tag :type="selectedNode.is_company === 1 ? 'primary' : 'info'">
|
||||
{{ selectedNode.is_company === 1 ? '是' : '否' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="selectedNode.status === 1 ? 'success' : 'danger'">
|
||||
{{ selectedNode.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注" :span="2">
|
||||
{{ selectedNode.remark || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div class="detail-actions">
|
||||
<el-button type="primary" @click="handleEdit(selectedNode)">
|
||||
<el-icon><Edit /></el-icon>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button type="danger" @click="handleDelete(selectedNode)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty v-else description="请选择左侧组织节点查看详情" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 添加/编辑对话框 -->
|
||||
<OrganizationEdit
|
||||
ref="editRef"
|
||||
:tree-data="treeData"
|
||||
:tree-props="treeProps"
|
||||
@success="handleSuccess"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
|
||||
import OrganizationEdit from './components/edit.vue'
|
||||
import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '@/api/erp'
|
||||
|
||||
// 树形结构数据(与后台字段对应)
|
||||
interface TreeNode {
|
||||
id: number
|
||||
tid?: number
|
||||
org_name: string
|
||||
org_code?: string
|
||||
parent_id?: number
|
||||
parent_name?: string
|
||||
leader_id?: number
|
||||
leader_name?: string
|
||||
is_company?: number
|
||||
sort?: number
|
||||
status?: number
|
||||
remark?: string
|
||||
children?: TreeNode[]
|
||||
}
|
||||
|
||||
// 树形数据
|
||||
const treeData = ref<TreeNode[]>([])
|
||||
|
||||
const treeProps = {
|
||||
label: 'org_name',
|
||||
children: 'children',
|
||||
value: 'id'
|
||||
}
|
||||
|
||||
const treeRef = ref()
|
||||
const editRef = ref()
|
||||
const selectedNode = ref<TreeNode | null>(null)
|
||||
const loading = ref(false)
|
||||
|
||||
// 将扁平列表构建为树形结构
|
||||
const buildTree = (list: any[]): TreeNode[] => {
|
||||
const nodeMap = new Map<number, TreeNode>()
|
||||
const roots: TreeNode[] = []
|
||||
|
||||
list.forEach(item => {
|
||||
const node: TreeNode = {
|
||||
id: item.id,
|
||||
tid: item.tid,
|
||||
org_name: item.org_name,
|
||||
org_code: item.org_code,
|
||||
parent_id: item.parent_id,
|
||||
parent_name: item.parent_name,
|
||||
leader_id: item.leader_id,
|
||||
leader_name: item.leader_name,
|
||||
is_company: item.is_company,
|
||||
sort: item.sort,
|
||||
status: item.status,
|
||||
remark: item.remark,
|
||||
children: []
|
||||
}
|
||||
nodeMap.set(node.id, node)
|
||||
})
|
||||
|
||||
nodeMap.forEach(node => {
|
||||
if (node.parent_id && nodeMap.has(node.parent_id)) {
|
||||
const parent = nodeMap.get(node.parent_id)!
|
||||
if (!parent.children) {
|
||||
parent.children = []
|
||||
}
|
||||
parent.children.push(node)
|
||||
} else {
|
||||
roots.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
return roots
|
||||
}
|
||||
|
||||
// 获取组织机构详情
|
||||
const loadOrganizationDetail = async (id: number) => {
|
||||
try {
|
||||
const res = await getOrganizationDetail(id)
|
||||
if (res.code === 200) {
|
||||
const data = res.data || null
|
||||
if (data) {
|
||||
selectedNode.value = {
|
||||
id: data.id,
|
||||
tid: data.tid,
|
||||
org_name: data.org_name,
|
||||
org_code: data.org_code,
|
||||
parent_id: data.parent_id,
|
||||
parent_name: data.parent_name,
|
||||
leader_id: data.leader_id,
|
||||
leader_name: data.leader_name,
|
||||
is_company: data.is_company,
|
||||
sort: data.sort,
|
||||
status: data.status,
|
||||
remark: data.remark,
|
||||
children: []
|
||||
}
|
||||
} else {
|
||||
selectedNode.value = null
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取组织机构详情失败:', error)
|
||||
ElMessage.error('获取组织机构详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 获取组织机构列表
|
||||
const fetchOrganizationList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getOrganizationList()
|
||||
if (res.code === 200) {
|
||||
const list = res.data || []
|
||||
treeData.value = buildTree(list)
|
||||
|
||||
// 如果当前没有选中的节点且有数据,默认加载第一个节点详情
|
||||
if (!selectedNode.value && list.length) {
|
||||
await loadOrganizationDetail(list[0].id)
|
||||
}
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取组织机构列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取组织机构列表失败:', error)
|
||||
ElMessage.error('获取组织机构列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 详情标题
|
||||
const detailTitle = computed(() => {
|
||||
return selectedNode.value ? `部门详情 - ${selectedNode.value.org_name}` : '部门详情'
|
||||
})
|
||||
|
||||
// 点击节点,加载详情
|
||||
const handleNodeClick = (data: TreeNode) => {
|
||||
loadOrganizationDetail(data.id)
|
||||
}
|
||||
|
||||
// 添加根节点
|
||||
const handleAddRoot = () => {
|
||||
editRef.value?.open(undefined, undefined)
|
||||
}
|
||||
|
||||
// 添加子节点
|
||||
const handleAdd = (data: TreeNode) => {
|
||||
editRef.value?.open(undefined, data.id)
|
||||
}
|
||||
|
||||
// 编辑
|
||||
const handleEdit = (data: TreeNode) => {
|
||||
editRef.value?.open(data)
|
||||
}
|
||||
|
||||
// 提交成功回调,重新加载列表
|
||||
const handleSuccess = () => {
|
||||
fetchOrganizationList()
|
||||
}
|
||||
|
||||
// 删除
|
||||
const handleDelete = (data: TreeNode) => {
|
||||
ElMessageBox.confirm(`确定要删除部门「${data.org_name}」吗?`, '提示', {
|
||||
type: 'warning'
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const res = await deleteOrganization(data.id)
|
||||
if (res.code === 200) {
|
||||
ElMessage.success('删除成功')
|
||||
fetchOrganizationList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrganizationList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.organization-container {
|
||||
// padding: 16px;
|
||||
}
|
||||
|
||||
.tree-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding-right: 8px;
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.node-actions {
|
||||
display: none;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&:hover .node-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
.el-descriptions {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.detail-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user