Compare commits
22
Commits
07d3614d9c
..
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 |
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
# 生产环境接口
|
# 生产环境接口
|
||||||
VITE_API_BASE_URL='https://backend.yunzer.cn'
|
VITE_API_BASE_URL='https://api.yunzer.cn'
|
||||||
|
|||||||
Vendored
+1
@@ -21,6 +21,7 @@ declare module 'vue' {
|
|||||||
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCascader: typeof import('element-plus/es')['ElCascader']
|
ElCascader: typeof import('element-plus/es')['ElCascader']
|
||||||
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ import { onMounted } from 'vue';
|
|||||||
const authStore = useAuthStore();
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
// 获取租户ID
|
// 获取租户ID
|
||||||
const tenantId = (authStore.user as any)?.tenant_id;
|
const tenantId = (authStore.user as any)?.tid;
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
const userInfo = authStore.user;
|
const userInfo = authStore.user;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import request from "@/utils/request";
|
|||||||
// 获取内容统计
|
// 获取内容统计
|
||||||
export function getContentStats() {
|
export function getContentStats() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/contentstats",
|
url: "/backend/contentstats",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ export function getContentStats() {
|
|||||||
// 获取用户统计
|
// 获取用户统计
|
||||||
export function getUserStats() {
|
export function getUserStats() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/usersstats",
|
url: "/backend/usersstats",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+19
-19
@@ -4,7 +4,7 @@ import request from "@/utils/request";
|
|||||||
// 获取文章列表
|
// 获取文章列表
|
||||||
export function listArticles(params) {
|
export function listArticles(params) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/articlesList`,
|
url: `/backend/articlesList`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -13,7 +13,7 @@ export function listArticles(params) {
|
|||||||
// 获取文章所有文章
|
// 获取文章所有文章
|
||||||
export function listAllArticles(params) {
|
export function listAllArticles(params) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/allarticles`,
|
url: `/backend/allarticles`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -22,7 +22,7 @@ export function listAllArticles(params) {
|
|||||||
// 获取文章详情
|
// 获取文章详情
|
||||||
export function getArticle(id) {
|
export function getArticle(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/articles/${id}`,
|
url: `/backend/articles/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export function getArticle(id) {
|
|||||||
// 创建文章
|
// 创建文章
|
||||||
export function createArticle(data) {
|
export function createArticle(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/createarticle',
|
url: '/backend/createarticle',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -39,7 +39,7 @@ export function createArticle(data) {
|
|||||||
// 编辑文章
|
// 编辑文章
|
||||||
export function editArticle(id, data) {
|
export function editArticle(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/editarticle/${id}`,
|
url: `/backend/editarticle/${id}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -48,7 +48,7 @@ export function editArticle(id, data) {
|
|||||||
// 删除文章
|
// 删除文章
|
||||||
export function deleteArticle(id) {
|
export function deleteArticle(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/deletearticle/${id}`,
|
url: `/backend/deletearticle/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -56,7 +56,7 @@ export function deleteArticle(id) {
|
|||||||
// 发布文章
|
// 发布文章
|
||||||
export function publishArticle(id,uid) {
|
export function publishArticle(id,uid) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/publisharticle/${id}`,
|
url: `/backend/publisharticle/${id}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
uid
|
uid
|
||||||
@@ -67,7 +67,7 @@ export function publishArticle(id,uid) {
|
|||||||
// 下架文章
|
// 下架文章
|
||||||
export function unPublishArticle(id) {
|
export function unPublishArticle(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/unPublisharticle/${id}`,
|
url: `/backend/unPublisharticle/${id}`,
|
||||||
method: 'post'
|
method: 'post'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ export function unPublishArticle(id) {
|
|||||||
// 文章推荐
|
// 文章推荐
|
||||||
export function articleRecommend(id) {
|
export function articleRecommend(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/articleRecommend/${id}`,
|
url: `/backend/articleRecommend/${id}`,
|
||||||
method: 'post'
|
method: 'post'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ export function articleRecommend(id) {
|
|||||||
// 取消文章推荐
|
// 取消文章推荐
|
||||||
export function unArticleRecommend(id) {
|
export function unArticleRecommend(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/unArticleRecommend/${id}`,
|
url: `/backend/unArticleRecommend/${id}`,
|
||||||
method: 'post'
|
method: 'post'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ export function unArticleRecommend(id) {
|
|||||||
// 文章置顶
|
// 文章置顶
|
||||||
export function articleTop(id) {
|
export function articleTop(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/articleTop/${id}`,
|
url: `/backend/articleTop/${id}`,
|
||||||
method: 'post'
|
method: 'post'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ export function articleTop(id) {
|
|||||||
// 取消文章置顶
|
// 取消文章置顶
|
||||||
export function unArticleTop(id) {
|
export function unArticleTop(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/unArticleTop/${id}`,
|
url: `/backend/unArticleTop/${id}`,
|
||||||
method: 'post'
|
method: 'post'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ export function unArticleTop(id) {
|
|||||||
// 获取所有分类列表
|
// 获取所有分类列表
|
||||||
export function allCategories(params) {
|
export function allCategories(params) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/allcategories`,
|
url: `/backend/allcategories`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -120,7 +120,7 @@ export function allCategories(params) {
|
|||||||
// 获取分类列表
|
// 获取分类列表
|
||||||
export function listCategories(params) {
|
export function listCategories(params) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/categories`,
|
url: `/backend/categories`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -129,7 +129,7 @@ export function listCategories(params) {
|
|||||||
// 获取分类详情
|
// 获取分类详情
|
||||||
export function getCategory(id) {
|
export function getCategory(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/categories/${id}`,
|
url: `/backend/categories/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,7 +137,7 @@ export function getCategory(id) {
|
|||||||
// 创建分类
|
// 创建分类
|
||||||
export function createCategory(data) {
|
export function createCategory(data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/createCategory`,
|
url: `/backend/createCategory`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -146,7 +146,7 @@ export function createCategory(data) {
|
|||||||
// 更新分类
|
// 更新分类
|
||||||
export function editCategory(id, data) {
|
export function editCategory(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/editCategory/${id}`,
|
url: `/backend/editCategory/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -155,7 +155,7 @@ export function editCategory(id, data) {
|
|||||||
// 删除分类
|
// 删除分类
|
||||||
export function deleteCategory(id) {
|
export function deleteCategory(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/categories/${id}`,
|
url: `/backend/categories/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ export function deleteCategory(id) {
|
|||||||
// 更新分类状态
|
// 更新分类状态
|
||||||
export function updateCategoryStatus(id, status) {
|
export function updateCategoryStatus(id, status) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/categories/${id}/status`,
|
url: `/backend/categories/${id}/status`,
|
||||||
method: "patch",
|
method: "patch",
|
||||||
data: { status },
|
data: { status },
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-13
@@ -10,7 +10,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getBabyList() {
|
export function getBabyList() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/babys/list',
|
url: '/backend/babys/list',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export function getBabyList() {
|
|||||||
*/
|
*/
|
||||||
export function getBabyDetail(id) {
|
export function getBabyDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/babys/${id}`,
|
url: `/backend/babys/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ export function getBabyDetail(id) {
|
|||||||
*/
|
*/
|
||||||
export function createBaby(data) {
|
export function createBaby(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/babys",
|
url: "/backend/babys",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -46,7 +46,7 @@ export function createBaby(data) {
|
|||||||
// 更新宝贝信息
|
// 更新宝贝信息
|
||||||
export function editBaby(id, data) {
|
export function editBaby(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/baby/update/${id}`,
|
url: `/backend/baby/update/${id}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -59,7 +59,7 @@ export function editBaby(id, data) {
|
|||||||
*/
|
*/
|
||||||
export function deleteBaby(id) {
|
export function deleteBaby(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/babys/${id}`,
|
url: `/backend/babys/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -72,7 +72,7 @@ export function deleteBaby(id) {
|
|||||||
*/
|
*/
|
||||||
export function bindParent(id, data) {
|
export function bindParent(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/babys/bindparents/${id}`,
|
url: `/backend/babys/bindparents/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
});
|
});
|
||||||
@@ -85,7 +85,7 @@ export function bindParent(id, data) {
|
|||||||
*/
|
*/
|
||||||
export function getParents(id) {
|
export function getParents(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/babys/getParents/${id}`,
|
url: `/backend/babys/getParents/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ export function getParents(id) {
|
|||||||
*/
|
*/
|
||||||
export function getUserList() {
|
export function getUserList() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/babyhealthUser/list",
|
url: "/backend/babyhealthUser/list",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ export function getUserList() {
|
|||||||
*/
|
*/
|
||||||
export function getUserDetail(id) {
|
export function getUserDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/babyhealthUser/detail/${id}`,
|
url: `/backend/babyhealthUser/detail/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ export function getUserDetail(id) {
|
|||||||
*/
|
*/
|
||||||
export function createUser(data) {
|
export function createUser(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/babyhealthUser/create",
|
url: "/backend/babyhealthUser/create",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -141,7 +141,7 @@ export function createUser(data) {
|
|||||||
*/
|
*/
|
||||||
export function updateUser(id, data) {
|
export function updateUser(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/babyhealthUser/update/${id}`,
|
url: `/backend/babyhealthUser/update/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -157,7 +157,7 @@ export function updateUser(id, data) {
|
|||||||
*/
|
*/
|
||||||
export function deleteUser(id) {
|
export function deleteUser(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/babyhealthUser/delete/${id}`,
|
url: `/backend/babyhealthUser/delete/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -173,7 +173,7 @@ export function deleteUser(id) {
|
|||||||
*/
|
*/
|
||||||
export function getDashborad() {
|
export function getDashborad() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/babyhealthDashborad/dashborad",
|
url: "/backend/babyhealthDashborad/dashborad",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+4
-4
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getBanners() {
|
export function getBanners() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/allbanners",
|
url: "/backend/allbanners",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export function getBanners() {
|
|||||||
*/
|
*/
|
||||||
export function createBanner(formData) {
|
export function createBanner(formData) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/createbanner",
|
url: "/backend/createbanner",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: formData,
|
data: formData,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -35,7 +35,7 @@ export function createBanner(formData) {
|
|||||||
*/
|
*/
|
||||||
export function editBanner(id, bannerData) {
|
export function editBanner(id, bannerData) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/editbanner/${id}`,
|
url: `/backend/editbanner/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: bannerData,
|
data: bannerData,
|
||||||
});
|
});
|
||||||
@@ -48,7 +48,7 @@ export function editBanner(id, bannerData) {
|
|||||||
*/
|
*/
|
||||||
export function deleteBanner(id) {
|
export function deleteBanner(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/deletebanner/${id}`,
|
url: `/backend/deletebanner/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,7 @@ import request from '@/utils/request'
|
|||||||
|
|
||||||
export function listContacts(params) {
|
export function listContacts(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/crm/contact/list',
|
url: '/backend/crm/contact/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
@@ -10,7 +10,7 @@ export function listContacts(params) {
|
|||||||
|
|
||||||
export function createContact(data) {
|
export function createContact(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/crm/contact/add',
|
url: '/backend/crm/contact/add',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
@@ -18,7 +18,7 @@ export function createContact(data) {
|
|||||||
|
|
||||||
export function updateContact(data) {
|
export function updateContact(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/crm/contact/edit',
|
url: '/backend/crm/contact/edit',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
@@ -26,7 +26,7 @@ export function updateContact(data) {
|
|||||||
|
|
||||||
export function deleteContact(data) {
|
export function deleteContact(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/crm/contact/delete',
|
url: '/backend/crm/contact/delete',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getPlatformStats() {
|
export function getPlatformStats() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/dashboard/platform-stats",
|
url: "/backend/dashboard/platform-stats",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export function getPlatformStats() {
|
|||||||
*/
|
*/
|
||||||
export function getTenantStats() {
|
export function getTenantStats() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/dashboard/tenant-stats",
|
url: "/backend/dashboard/tenant-stats",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -30,7 +30,7 @@ export function getTenantStats() {
|
|||||||
*/
|
*/
|
||||||
export function getActivityLogs(pageNum = 1, pageSize = 10) {
|
export function getActivityLogs(pageNum = 1, pageSize = 10) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/dashboard/user-activity-logs",
|
url: "/backend/dashboard/user-activity-logs",
|
||||||
method: "get",
|
method: "get",
|
||||||
params: {
|
params: {
|
||||||
page_num: pageNum,
|
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) {
|
export function getTenantDepartments(tenantId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/departments/tenant/${tenantId}`,
|
url: `/backend/departments/tenant/${tenantId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ export function getTenantDepartments(tenantId) {
|
|||||||
// 获取部门详情
|
// 获取部门详情
|
||||||
export function getDepartmentInfo(departmentId) {
|
export function getDepartmentInfo(departmentId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/departments/${departmentId}`,
|
url: `/backend/departments/${departmentId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export function getDepartmentInfo(departmentId) {
|
|||||||
// 添加部门
|
// 添加部门
|
||||||
export function addDepartment(data) {
|
export function addDepartment(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/departments',
|
url: '/backend/departments',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -28,7 +28,7 @@ export function addDepartment(data) {
|
|||||||
// 更新部门信息
|
// 更新部门信息
|
||||||
export function editDepartment(departmentId, data) {
|
export function editDepartment(departmentId, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/departments/${departmentId}`,
|
url: `/backend/departments/${departmentId}`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -37,7 +37,7 @@ export function editDepartment(departmentId, data) {
|
|||||||
// 删除部门
|
// 删除部门
|
||||||
export function deleteDepartment(departmentId) {
|
export function deleteDepartment(departmentId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/departments/${departmentId}`,
|
url: `/backend/departments/${departmentId}`,
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -3,7 +3,7 @@ import request from '@/utils/request'
|
|||||||
// 获取字典类型列表
|
// 获取字典类型列表
|
||||||
export function getDictTypes(params) {
|
export function getDictTypes(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/dict/types',
|
url: '/backend/dict/types',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
@@ -12,7 +12,7 @@ export function getDictTypes(params) {
|
|||||||
// 根据ID获取字典类型
|
// 根据ID获取字典类型
|
||||||
export function getDictTypeById(id) {
|
export function getDictTypeById(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/dict/types/${id}`,
|
url: `/backend/dict/types/${id}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ export function getDictTypeById(id) {
|
|||||||
// 添加字典类型
|
// 添加字典类型
|
||||||
export function addDictType(data) {
|
export function addDictType(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/dict/types',
|
url: '/backend/dict/types',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
@@ -32,7 +32,7 @@ export function addDictType(data) {
|
|||||||
// 更新字典类型
|
// 更新字典类型
|
||||||
export function updateDictType(id, data) {
|
export function updateDictType(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/dict/types/${id}`,
|
url: `/backend/dict/types/${id}`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
@@ -44,7 +44,7 @@ export function updateDictType(id, data) {
|
|||||||
// 删除字典类型
|
// 删除字典类型
|
||||||
export function deleteDictType(id) {
|
export function deleteDictType(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/dict/types/${id}`,
|
url: `/backend/dict/types/${id}`,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ export function deleteDictType(id) {
|
|||||||
// 获取字典项列表
|
// 获取字典项列表
|
||||||
export function getDictItems(params) {
|
export function getDictItems(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/dict/items',
|
url: '/backend/dict/items',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params
|
||||||
})
|
})
|
||||||
@@ -61,7 +61,7 @@ export function getDictItems(params) {
|
|||||||
// 根据ID获取字典项
|
// 根据ID获取字典项
|
||||||
export function getDictItemById(id) {
|
export function getDictItemById(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/dict/items/${id}`,
|
url: `/backend/dict/items/${id}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ export function getDictItemById(id) {
|
|||||||
// 添加字典项
|
// 添加字典项
|
||||||
export function addDictItem(data) {
|
export function addDictItem(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/dict/items',
|
url: '/backend/dict/items',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
@@ -78,7 +78,7 @@ export function addDictItem(data) {
|
|||||||
// 更新字典项
|
// 更新字典项
|
||||||
export function updateDictItem(id, data) {
|
export function updateDictItem(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/dict/items/${id}`,
|
url: `/backend/dict/items/${id}`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
@@ -87,7 +87,7 @@ export function updateDictItem(id, data) {
|
|||||||
// 删除字典项
|
// 删除字典项
|
||||||
export function deleteDictItem(id) {
|
export function deleteDictItem(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/dict/items/${id}`,
|
url: `/backend/dict/items/${id}`,
|
||||||
method: 'delete'
|
method: 'delete'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ export function deleteDictItem(id) {
|
|||||||
// 根据字典编码获取字典项(用于业务查询)
|
// 根据字典编码获取字典项(用于业务查询)
|
||||||
export function getDictItemsByCode(code, includeDisabled = false) {
|
export function getDictItemsByCode(code, includeDisabled = false) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/dict/items/code/${code}`,
|
url: `/backend/dict/items/code/${code}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: {
|
params: {
|
||||||
include_disabled: includeDisabled ? '1' : '0'
|
include_disabled: includeDisabled ? '1' : '0'
|
||||||
@@ -106,7 +106,7 @@ export function getDictItemsByCode(code, includeDisabled = false) {
|
|||||||
// 批量更新字典项排序
|
// 批量更新字典项排序
|
||||||
export function batchUpdateDictItemSort(data) {
|
export function batchUpdateDictItemSort(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/dict/items/sort',
|
url: '/backend/dict/items/sort',
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
+3
-3
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getEmailInfo() {
|
export function getEmailInfo() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/email/info",
|
url: "/backend/email/info",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export function getEmailInfo() {
|
|||||||
*/
|
*/
|
||||||
export function editEmailInfo(data) {
|
export function editEmailInfo(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/email/editinfo",
|
url: "/backend/email/editinfo",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -29,7 +29,7 @@ export function editEmailInfo(data) {
|
|||||||
*/
|
*/
|
||||||
export function sendTestEmail(data) {
|
export function sendTestEmail(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/email/sendtestemail",
|
url: "/backend/email/sendtestemail",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-13
@@ -10,7 +10,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getOrganizationList() {
|
export function getOrganizationList() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/erp/getOrganization',
|
url: '/backend/erp/getOrganization',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export function getOrganizationList() {
|
|||||||
*/
|
*/
|
||||||
export function getOrganizationDetail(id) {
|
export function getOrganizationDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/erp/getOrganizationDetail/${id}`,
|
url: `/backend/erp/getOrganizationDetail/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -34,7 +34,7 @@ export function getOrganizationDetail(id) {
|
|||||||
*/
|
*/
|
||||||
export function createOrganization(data) {
|
export function createOrganization(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/erp/createOrganization",
|
url: "/backend/erp/createOrganization",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -46,7 +46,7 @@ export function createOrganization(data) {
|
|||||||
// 更新组织机构信息
|
// 更新组织机构信息
|
||||||
export function editOrganization(id, data) {
|
export function editOrganization(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/erp/editOrganization/${id}`,
|
url: `/backend/erp/editOrganization/${id}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -59,7 +59,7 @@ export function editOrganization(id, data) {
|
|||||||
*/
|
*/
|
||||||
export function deleteOrganization(id) {
|
export function deleteOrganization(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/erp/deleteOrganization/${id}`,
|
url: `/backend/erp/deleteOrganization/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ export function deleteOrganization(id) {
|
|||||||
*/
|
*/
|
||||||
export function getCompanys() {
|
export function getCompanys() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/erp/getCompanys',
|
url: '/backend/erp/getCompanys',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ export function getCompanys() {
|
|||||||
*/
|
*/
|
||||||
export function getDepartments(parentId) {
|
export function getDepartments(parentId) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/erp/getDepartments',
|
url: '/backend/erp/getDepartments',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: parentId ? { parent_id: parentId } : {}
|
params: parentId ? { parent_id: parentId } : {}
|
||||||
});
|
});
|
||||||
@@ -99,9 +99,9 @@ export function getDepartments(parentId) {
|
|||||||
*/
|
*/
|
||||||
export function getEmployeeList(tenantId) {
|
export function getEmployeeList(tenantId) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/erp/getEmployee',
|
url: '/backend/erp/getEmployee',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { tenant_id: tenantId }
|
params: { tid: tenantId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export function getEmployeeList(tenantId) {
|
|||||||
*/
|
*/
|
||||||
export function getEmployeeDetail(id) {
|
export function getEmployeeDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/erp/getEmployeeDetail/${id}`,
|
url: `/backend/erp/getEmployeeDetail/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ export function getEmployeeDetail(id) {
|
|||||||
*/
|
*/
|
||||||
export function createEmployee(data) {
|
export function createEmployee(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/erp/createEmployee",
|
url: "/backend/erp/createEmployee",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -136,7 +136,7 @@ export function createEmployee(data) {
|
|||||||
// 更新员工信息
|
// 更新员工信息
|
||||||
export function editEmployee(id, data) {
|
export function editEmployee(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/erp/editEmployee/${id}`,
|
url: `/backend/erp/editEmployee/${id}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: data
|
data: data
|
||||||
});
|
});
|
||||||
@@ -149,7 +149,7 @@ export function editEmployee(id, data) {
|
|||||||
*/
|
*/
|
||||||
export function deleteEmployee(id) {
|
export function deleteEmployee(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/erp/deleteEmployee/${id}`,
|
url: `/backend/erp/deleteEmployee/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-31
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getUserCate() {
|
export function getUserCate() {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/usercate`,
|
url: `/backend/usercate`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export function getUserCate() {
|
|||||||
*/
|
*/
|
||||||
export function getAllFiles() {
|
export function getAllFiles() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/allfiles",
|
url: "/backend/allfiles",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ export function getAllFiles() {
|
|||||||
*/
|
*/
|
||||||
export function createFileCate(data) {
|
export function createFileCate(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/createfilecate",
|
url: "/backend/createfilecate",
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -43,7 +43,7 @@ export function createFileCate(data) {
|
|||||||
*/
|
*/
|
||||||
export function renameFileCate(id, data) {
|
export function renameFileCate(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/renamefilecate/${id}`,
|
url: `/backend/renamefilecate/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -56,7 +56,7 @@ export function renameFileCate(id, data) {
|
|||||||
*/
|
*/
|
||||||
export function deleteFileCate(id) {
|
export function deleteFileCate(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/deletefilecate/${id}`,
|
url: `/backend/deletefilecate/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
|
|||||||
params.keyword = keyword;
|
params.keyword = keyword;
|
||||||
}
|
}
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/catefiles/${id}`,
|
url: `/backend/catefiles/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -91,7 +91,7 @@ export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
|
|||||||
*/
|
*/
|
||||||
export function getFileById(id) {
|
export function getFileById(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/catefiles`,
|
url: `/backend/catefiles`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -109,7 +109,7 @@ export function uploadFile(formData, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/uploadfile",
|
url: "/backend/uploadfile",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: formData,
|
data: formData,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -126,7 +126,7 @@ export function uploadFile(formData, options = {}) {
|
|||||||
*/
|
*/
|
||||||
export function updateFile(id, fileData) {
|
export function updateFile(id, fileData) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/updatefile/${id}`,
|
url: `/backend/updatefile/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: fileData,
|
data: fileData,
|
||||||
});
|
});
|
||||||
@@ -139,7 +139,19 @@ export function updateFile(id, fileData) {
|
|||||||
*/
|
*/
|
||||||
export function deleteFile(id) {
|
export function deleteFile(id) {
|
||||||
return request({
|
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",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -152,44 +164,48 @@ export function deleteFile(id) {
|
|||||||
*/
|
*/
|
||||||
export function moveFile(id, cate) {
|
export function moveFile(id, cate) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/movefile/${id}`,
|
url: `/backend/movefile/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params: { cate },
|
params: { cate },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 上传头像
|
* 批量删除文件
|
||||||
* @param {FormData} formData 文件数据
|
* @param {Array} ids 文件ID数组
|
||||||
* @param {Object} options 额外选项
|
|
||||||
* @param {string} [options.cate]
|
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function uploadAvatar(formData, options = {}) {
|
export function batchDeleteFiles(ids) {
|
||||||
if (options.cate) {
|
|
||||||
formData.append('cate', options.cate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/uploadavatar",
|
url: "/backend/batchDeleteFiles",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: formData,
|
data: { ids },
|
||||||
headers: {
|
|
||||||
"Content-Type": "multipart/form-data"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新头像
|
* 批量彻底删除文件
|
||||||
* @param {number|string} id 文件ID
|
* @param {Array} ids 文件ID数组
|
||||||
* @param {Object} fileData 更新头像
|
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function updateAvatar(id, fileData) {
|
export function batchDeleteFilesPermanently(ids) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/uploadavatar/${id}`,
|
url: "/backend/batchDeleteFilesPermanently",
|
||||||
method: "post",
|
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() {
|
export function getFrontMenus() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/frontmenus",
|
url: "/backend/frontmenus",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export function getFrontMenus() {
|
|||||||
*/
|
*/
|
||||||
export function createFrontMenu(formData, options = {}) {
|
export function createFrontMenu(formData, options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/createfrontmenu",
|
url: "/backend/createfrontmenu",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: formData,
|
data: formData,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -35,7 +35,7 @@ export function createFrontMenu(formData, options = {}) {
|
|||||||
*/
|
*/
|
||||||
export function editFrontMenu(id, frontMenuData) {
|
export function editFrontMenu(id, frontMenuData) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/editfrontmenu/${id}`,
|
url: `/backend/editfrontmenu/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: frontMenuData,
|
data: frontMenuData,
|
||||||
});
|
});
|
||||||
@@ -48,7 +48,7 @@ export function editFrontMenu(id, frontMenuData) {
|
|||||||
*/
|
*/
|
||||||
export function deleteFrontMenu(id) {
|
export function deleteFrontMenu(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/deletefrontmenu/${id}`,
|
url: `/backend/deletefrontmenu/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+60
-10
@@ -3,13 +3,27 @@ import request from "@/utils/request";
|
|||||||
// 登录(使用租户名称)
|
// 登录(使用租户名称)
|
||||||
export function login(data) {
|
export function login(data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/login`,
|
url: `/backend/login`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data,
|
||||||
tenant_name: data.tenant_name,
|
});
|
||||||
account: data.account,
|
}
|
||||||
password: data.password,
|
|
||||||
},
|
// 发送登录验证码(手机号)
|
||||||
|
export function sendLoginCode(data) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/sendLoginCode",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手机号验证码登录
|
||||||
|
export function loginBySms(data) {
|
||||||
|
return request({
|
||||||
|
url: "/backend/loginBySms",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// 登出
|
// 登出
|
||||||
@@ -27,7 +41,7 @@ export function logout(userInfo = null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/logout`,
|
url: `/backend/logout`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: userInfo ? { userInfo: userInfo } : {},
|
data: userInfo ? { userInfo: userInfo } : {},
|
||||||
});
|
});
|
||||||
@@ -39,7 +53,7 @@ export function logout(userInfo = null) {
|
|||||||
*/
|
*/
|
||||||
export function getGeetest3Infos() {
|
export function getGeetest3Infos() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/login/getGeetest3Infos',
|
url: '/backend/login/getGeetest3Infos',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -50,7 +64,7 @@ export function getGeetest3Infos() {
|
|||||||
*/
|
*/
|
||||||
export function getGeetest4Infos() {
|
export function getGeetest4Infos() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/login/getGeetest4Infos',
|
url: '/backend/login/getGeetest4Infos',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -61,7 +75,43 @@ export function getGeetest4Infos() {
|
|||||||
*/
|
*/
|
||||||
export function getOpenVerify() {
|
export function getOpenVerify() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/login/getOpenVerify',
|
url: '/backend/login/getOpenVerify',
|
||||||
method: 'get'
|
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() {
|
export function getAllMenus() {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/allmenu`,
|
url: `/backend/allmenu`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ export function getAllMenus() {
|
|||||||
//获取用户菜单
|
//获取用户菜单
|
||||||
export function getMenus(id){
|
export function getMenus(id){
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/menu/${parseInt(id)}`,
|
url: `/backend/menu/${parseInt(id)}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export function getMenus(id){
|
|||||||
// 更新菜单状态
|
// 更新菜单状态
|
||||||
export function updateMenuStatus(menuId, status) {
|
export function updateMenuStatus(menuId, status) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/menu/status/${menuId}`,
|
url: `/backend/menu/status/${menuId}`,
|
||||||
method: "patch",
|
method: "patch",
|
||||||
data: { status },
|
data: { status },
|
||||||
});
|
});
|
||||||
@@ -28,7 +28,7 @@ export function updateMenuStatus(menuId, status) {
|
|||||||
// 创建菜单
|
// 创建菜单
|
||||||
export function createMenu(menuData) {
|
export function createMenu(menuData) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/createmenu`,
|
url: `/backend/createmenu`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: menuData,
|
data: menuData,
|
||||||
});
|
});
|
||||||
@@ -37,7 +37,7 @@ export function createMenu(menuData) {
|
|||||||
// 更新菜单
|
// 更新菜单
|
||||||
export function updateMenu(menuId, menuData) {
|
export function updateMenu(menuId, menuData) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/updatemenu/${menuId}`,
|
url: `/backend/updatemenu/${menuId}`,
|
||||||
method: "put",
|
method: "put",
|
||||||
data: menuData,
|
data: menuData,
|
||||||
});
|
});
|
||||||
@@ -46,7 +46,7 @@ export function updateMenu(menuId, menuData) {
|
|||||||
// 删除菜单
|
// 删除菜单
|
||||||
export function deleteMenu(menuId) {
|
export function deleteMenu(menuId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/deletemenu/${menuId}`,
|
url: `/backend/deletemenu/${menuId}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getModuleCategory() {
|
export function getModuleCategory() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/moduleCategory",
|
url: "/backend/moduleCategory",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export function getModuleCategory() {
|
|||||||
*/
|
*/
|
||||||
export function getModules(cid) {
|
export function getModules(cid) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/moduleCenter/modules",
|
url: "/backend/moduleCenter/modules",
|
||||||
method: "get",
|
method: "get",
|
||||||
params: { cid }
|
params: { cid }
|
||||||
});
|
});
|
||||||
@@ -34,7 +34,7 @@ export function getModules(cid) {
|
|||||||
*/
|
*/
|
||||||
export function editModuleCategory(data) {
|
export function editModuleCategory(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/moduleCenter/editCategory",
|
url: "/backend/moduleCenter/editCategory",
|
||||||
method: "post",
|
method: "post",
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
@@ -50,7 +50,7 @@ export function editModuleCategory(data) {
|
|||||||
*/
|
*/
|
||||||
export function editModules(data) {
|
export function editModules(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/moduleCenter/editModules",
|
url: "/backend/moduleCenter/editModules",
|
||||||
method: "post",
|
method: "post",
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-8
@@ -2,21 +2,28 @@ import request from '@/utils/request';
|
|||||||
|
|
||||||
export function getModulesList() {
|
export function getModulesList() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/modules/list',
|
url: '/backend/modules/list',
|
||||||
|
method: 'get',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTenantList() {
|
||||||
|
return request({
|
||||||
|
url: '/backend/modules/getTenantList',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModuleDetail(id) {
|
export function getModuleDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/modules/${id}`,
|
url: `/backend/modules/${id}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addModule(data) {
|
export function addModule(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/modules',
|
url: '/backend/modules',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -24,7 +31,7 @@ export function addModule(data) {
|
|||||||
|
|
||||||
export function editModule(id, data) {
|
export function editModule(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/modules/${id}`,
|
url: `/backend/modules/${id}`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -32,14 +39,14 @@ export function editModule(id, data) {
|
|||||||
|
|
||||||
export function deleteModule(id) {
|
export function deleteModule(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/modules/${id}`,
|
url: `/backend/modules/${id}`,
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function batchDeleteModules(ids) {
|
export function batchDeleteModules(ids) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/modules/batchDelete',
|
url: '/backend/modules/batchDelete',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { ids },
|
data: { ids },
|
||||||
});
|
});
|
||||||
@@ -47,7 +54,7 @@ export function batchDeleteModules(ids) {
|
|||||||
|
|
||||||
export function changeModuleStatus(id, status) {
|
export function changeModuleStatus(id, status) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/modules/status',
|
url: '/backend/modules/status',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { id, status },
|
data: { id, status },
|
||||||
});
|
});
|
||||||
@@ -55,7 +62,7 @@ export function changeModuleStatus(id, status) {
|
|||||||
|
|
||||||
export function getModulesSelectList() {
|
export function getModulesSelectList() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/modules/select/list',
|
url: '/backend/modules/select/list',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -6,7 +6,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getOnePages() {
|
export function getOnePages() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/allonepages",
|
url: "/backend/allonepages",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export function getOnePages() {
|
|||||||
*/
|
*/
|
||||||
export function createOnePage(formData) {
|
export function createOnePage(formData) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/createonepage",
|
url: "/backend/createonepage",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: formData,
|
data: formData,
|
||||||
});
|
});
|
||||||
@@ -32,7 +32,7 @@ export function createOnePage(formData) {
|
|||||||
*/
|
*/
|
||||||
export function editOnePage(id, onePageData) {
|
export function editOnePage(id, onePageData) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/editonepage/${id}`,
|
url: `/backend/editonepage/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: onePageData,
|
data: onePageData,
|
||||||
});
|
});
|
||||||
@@ -45,7 +45,7 @@ export function editOnePage(id, onePageData) {
|
|||||||
*/
|
*/
|
||||||
export function deleteOnePage(id) {
|
export function deleteOnePage(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/deleteonepage/${id}`,
|
url: `/backend/deleteonepage/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getOperationLogs(params) {
|
export function getOperationLogs(params) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/operationLogs",
|
url: "/backend/operationLogs",
|
||||||
method: "get",
|
method: "get",
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
@@ -28,7 +28,7 @@ export function getOperationLogs(params) {
|
|||||||
*/
|
*/
|
||||||
export function getOperationLogDetail(id) {
|
export function getOperationLogDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/operationLogs/${id}`,
|
url: `/backend/operationLogs/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export function getOperationLogDetail(id) {
|
|||||||
*/
|
*/
|
||||||
export function deleteOperationLog(id) {
|
export function deleteOperationLog(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/operationLogs/${id}`,
|
url: `/backend/operationLogs/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ export function deleteOperationLog(id) {
|
|||||||
*/
|
*/
|
||||||
export function batchDeleteOperationLogs(ids) {
|
export function batchDeleteOperationLogs(ids) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/operationLogs/batchDelete",
|
url: "/backend/operationLogs/batchDelete",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: { ids },
|
data: { ids },
|
||||||
});
|
});
|
||||||
@@ -64,7 +64,7 @@ export function batchDeleteOperationLogs(ids) {
|
|||||||
*/
|
*/
|
||||||
export function getOperationStatistics() {
|
export function getOperationStatistics() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/operationLogs/statistics",
|
url: "/backend/operationLogs/statistics",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import request from '@/utils/request';
|
|||||||
|
|
||||||
export function getAllMenuPermissions(params = {}) {
|
export function getAllMenuPermissions(params = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/allmenupermissions',
|
url: '/backend/allmenupermissions',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params
|
params
|
||||||
});
|
});
|
||||||
@@ -10,14 +10,14 @@ export function getAllMenuPermissions(params = {}) {
|
|||||||
|
|
||||||
export function getRolePermissions(roleId) {
|
export function getRolePermissions(roleId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/rolepermissions/${roleId}`,
|
url: `/backend/rolepermissions/${roleId}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function assignRolePermissions(roleId, permissions) {
|
export function assignRolePermissions(roleId, permissions) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/assignrolepermissions/${roleId}`,
|
url: `/backend/assignrolepermissions/${roleId}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: { permissions }
|
data: { permissions }
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-6
@@ -3,7 +3,7 @@ import request from '@/utils/request';
|
|||||||
// 获取租户下的所有职位
|
// 获取租户下的所有职位
|
||||||
export function getTenantPositions(tenantId) {
|
export function getTenantPositions(tenantId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/positions/tenant/${tenantId}`,
|
url: `/backend/positions/tenant/${tenantId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ export function getTenantPositions(tenantId) {
|
|||||||
// 根据部门ID获取职位列表
|
// 根据部门ID获取职位列表
|
||||||
export function getPositionsByDepartment(departmentId) {
|
export function getPositionsByDepartment(departmentId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/positions/department/${departmentId}`,
|
url: `/backend/positions/department/${departmentId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export function getPositionsByDepartment(departmentId) {
|
|||||||
// 获取职位详情
|
// 获取职位详情
|
||||||
export function getPositionInfo(positionId) {
|
export function getPositionInfo(positionId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/positions/${positionId}`,
|
url: `/backend/positions/${positionId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ export function getPositionInfo(positionId) {
|
|||||||
// 添加职位
|
// 添加职位
|
||||||
export function addPosition(data) {
|
export function addPosition(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/positions',
|
url: '/backend/positions',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -36,7 +36,7 @@ export function addPosition(data) {
|
|||||||
// 更新职位信息
|
// 更新职位信息
|
||||||
export function editPosition(positionId, data) {
|
export function editPosition(positionId, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/positions/${positionId}`,
|
url: `/backend/positions/${positionId}`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -45,7 +45,7 @@ export function editPosition(positionId, data) {
|
|||||||
// 删除职位
|
// 删除职位
|
||||||
export function deletePosition(positionId) {
|
export function deletePosition(positionId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/positions/${positionId}`,
|
url: `/backend/positions/${positionId}`,
|
||||||
method: 'delete',
|
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() {
|
export function getAllRoles() {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/allRoles',
|
url: '/backend/allRoles',
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoleById(id) {
|
export function getRoleById(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/roles/${id}`,
|
url: `/backend/roles/${id}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createRole(data) {
|
export function createRole(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/roles',
|
url: '/backend/roles',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
@@ -24,7 +24,7 @@ export function createRole(data) {
|
|||||||
|
|
||||||
export function updateRole(id, data) {
|
export function updateRole(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/roles/${id}`,
|
url: `/backend/roles/${id}`,
|
||||||
method: 'put',
|
method: 'put',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
@@ -32,7 +32,7 @@ export function updateRole(id, data) {
|
|||||||
|
|
||||||
export function deleteRole(id) {
|
export function deleteRole(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/roles/${id}`,
|
url: `/backend/roles/${id}`,
|
||||||
method: 'delete'
|
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'
|
||||||
|
})
|
||||||
|
}
|
||||||
+66
-14
@@ -2,12 +2,14 @@ import request from "@/utils/request";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取基本信息
|
* 获取基本信息
|
||||||
|
* @param {number} tid 租户ID
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function getNormalInfos() {
|
export function getNormalInfos(tid) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/normalInfos",
|
url: "/backend/normalInfos",
|
||||||
method: "get",
|
method: "get",
|
||||||
|
params: { tid }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,11 +20,9 @@ export function getNormalInfos() {
|
|||||||
*/
|
*/
|
||||||
export function saveNormalInfos(data) {
|
export function saveNormalInfos(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/saveNormalInfos",
|
url: "/backend/saveNormalInfos",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data: data,
|
||||||
data: JSON.stringify(data)
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export function saveNormalInfos(data) {
|
|||||||
*/
|
*/
|
||||||
export function getVerifyInfos() {
|
export function getVerifyInfos() {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/loginVerifyInfos",
|
url: "/backend/loginVerifyInfos",
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ export function getVerifyInfos() {
|
|||||||
*/
|
*/
|
||||||
export function saveVerifyInfos(data) {
|
export function saveVerifyInfos(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/saveloginVerifyInfos",
|
url: "/backend/saveloginVerifyInfos",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
});
|
});
|
||||||
@@ -52,12 +52,14 @@ export function saveVerifyInfos(data) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取法律声明和隐私条款
|
* 获取法律声明和隐私条款
|
||||||
|
* @param {number} tid 租户ID
|
||||||
* @returns {Promise}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
export function getLegalInfos() {
|
export function getLegalInfos(tid) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/legalInfos",
|
url: "/backend/legalInfos",
|
||||||
method: "get",
|
method: "get",
|
||||||
|
params: { tid }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +70,60 @@ export function getLegalInfos() {
|
|||||||
*/
|
*/
|
||||||
export function saveLegalInfos(data) {
|
export function saveLegalInfos(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/saveLegalInfos",
|
url: "/backend/saveLegalInfos",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: {
|
data: data,
|
||||||
data: JSON.stringify(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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
+6
-6
@@ -11,7 +11,7 @@ import request from "@/utils/request";
|
|||||||
*/
|
*/
|
||||||
export function getTenantList(params) {
|
export function getTenantList(params) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/tenant/getTenant",
|
url: "/backend/tenant/getTenant",
|
||||||
method: "get",
|
method: "get",
|
||||||
params: params,
|
params: params,
|
||||||
});
|
});
|
||||||
@@ -24,7 +24,7 @@ export function getTenantList(params) {
|
|||||||
*/
|
*/
|
||||||
export function getTenantDetail(id) {
|
export function getTenantDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/tenant/getTenantDetail/${id}`,
|
url: `/backend/tenant/getTenantDetail/${id}`,
|
||||||
method: "get",
|
method: "get",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ export function getTenantDetail(id) {
|
|||||||
*/
|
*/
|
||||||
export function createTenant(data) {
|
export function createTenant(data) {
|
||||||
return request({
|
return request({
|
||||||
url: "/admin/tenant/createTenant",
|
url: "/backend/tenant/createTenant",
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -52,7 +52,7 @@ export function createTenant(data) {
|
|||||||
*/
|
*/
|
||||||
export function editTenant(id, data) {
|
export function editTenant(id, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/tenant/editTenant/${id}`,
|
url: `/backend/tenant/editTenant/${id}`,
|
||||||
method: "post",
|
method: "post",
|
||||||
data: data,
|
data: data,
|
||||||
});
|
});
|
||||||
@@ -65,7 +65,7 @@ export function editTenant(id, data) {
|
|||||||
*/
|
*/
|
||||||
export function deleteTenant(id) {
|
export function deleteTenant(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/tenant/deleteTenant/${id}`,
|
url: `/backend/tenant/deleteTenant/${id}`,
|
||||||
method: "delete",
|
method: "delete",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@ export function deleteTenant(id) {
|
|||||||
*/
|
*/
|
||||||
export function checkTenantCode(tenant_code) {
|
export function checkTenantCode(tenant_code) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/tenant/findTenantCode',
|
url: '/backend/tenant/findTenantCode',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: { tenant_code }
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
+24
-8
@@ -1,17 +1,18 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
|
|
||||||
//获取所有用户信息
|
//获取所有用户信息
|
||||||
export function getAllUsers() {
|
export function getAllUsers(params) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/getAllUsers',
|
url: '/backend/getAllUsers',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
params,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//获取租户用户
|
//获取租户用户
|
||||||
export function getTenantUsers(tenantId) {
|
export function getTenantUsers(tenantId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/getTenantUsers/${tenantId}`,
|
url: `/backend/getTenantUsers/${tenantId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -19,7 +20,7 @@ export function getTenantUsers(tenantId) {
|
|||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
export function getUserInfo(userId) {
|
export function getUserInfo(userId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/getUserInfo/${userId}`,
|
url: `/backend/getUserInfo/${userId}`,
|
||||||
method: 'get',
|
method: 'get',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,7 @@ export function getUserInfo(userId) {
|
|||||||
// 添加用户
|
// 添加用户
|
||||||
export function addUser(data) {
|
export function addUser(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/addUser',
|
url: '/backend/addUser',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -36,7 +37,7 @@ export function addUser(data) {
|
|||||||
// 编辑用户信息
|
// 编辑用户信息
|
||||||
export function editUser(userId, data) {
|
export function editUser(userId, data) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/editUser/${userId}`,
|
url: `/backend/editUser/${userId}`,
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -47,10 +48,25 @@ export function updateUserInfo(userId, data) {
|
|||||||
return editUser(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) {
|
export function deleteUser(userId) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/deleteUser/${userId}`,
|
url: `/backend/deleteUser/${userId}`,
|
||||||
method: 'delete',
|
method: 'delete',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,7 +74,7 @@ export function deleteUser(userId) {
|
|||||||
// 修改密码
|
// 修改密码
|
||||||
export function changePassword(userId, data) {
|
export function changePassword(userId, data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/changePassword',
|
url: '/backend/changePassword',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: {
|
data: {
|
||||||
id: userId,
|
id: userId,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import request from "@/utils/request";
|
|||||||
// 获取文章列表
|
// 获取文章列表
|
||||||
export function GetCRMWorkbench(params) {
|
export function GetCRMWorkbench(params) {
|
||||||
return request({
|
return request({
|
||||||
url: `/admin/workbench/crm`,
|
url: `/backend/workbench/crm`,
|
||||||
method: "get",
|
method: "get",
|
||||||
params,
|
params,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
:default-active="route.path"
|
:default-active="route.path"
|
||||||
>
|
>
|
||||||
<!-- 菜单标题 -->
|
<!-- 菜单标题 -->
|
||||||
<h3>{{ isCollapse ? "管理" : currentModule?.title || "子菜单" }}</h3>
|
<h3>{{ isCollapse ? "管理" : asideTitle }}</h3>
|
||||||
|
|
||||||
<!-- 无模块时显示提示 -->
|
<!-- 无模块时显示提示 -->
|
||||||
<el-menu-item v-if="!currentModule" index="/home">
|
<el-menu-item v-if="!currentModule" index="/home">
|
||||||
@@ -248,12 +248,30 @@ const displayMenus = computed(() => {
|
|||||||
if (!currentModule.value) {
|
if (!currentModule.value) {
|
||||||
return [];
|
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 || [];
|
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) => {
|
const processMenus = (menus) => {
|
||||||
return menus
|
return menus
|
||||||
.filter((menu) => {
|
.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.path && menu.path.trim() !== "") return true;
|
||||||
if (menu.children && menu.children.length > 0) return true;
|
if (menu.children && menu.children.length > 0) return true;
|
||||||
return false;
|
return false;
|
||||||
@@ -445,7 +463,7 @@ h3 {
|
|||||||
background-color: rgba(60, 60, 60, 0.8) !important;
|
background-color: rgba(60, 60, 60, 0.8) !important;
|
||||||
}
|
}
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
border-left: 3px solid #4f84ff;
|
/* border-left: 3px solid #4f84ff; */
|
||||||
margin-left: -3px;
|
margin-left: -3px;
|
||||||
|
|
||||||
.menu-icon {
|
.menu-icon {
|
||||||
@@ -486,7 +504,7 @@ h3 {
|
|||||||
.el-menu-item.is-active {
|
.el-menu-item.is-active {
|
||||||
background: rgba(219, 148, 148, 0.8) !important;
|
background: rgba(219, 148, 148, 0.8) !important;
|
||||||
color: var(--el-color-primary-light-3) !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 {
|
.menu-icon {
|
||||||
color: var(--el-color-primary);
|
color: var(--el-color-primary);
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ const handleCommand = async (command) => {
|
|||||||
// 遍历 localStorage
|
// 遍历 localStorage
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
const key = localStorage.key(i);
|
const key = localStorage.key(i);
|
||||||
if (key && key.startsWith('menu_cache_')) {
|
if (key && key.startsWith('menu_')) {
|
||||||
menuCacheKeys.push(key);
|
menuCacheKeys.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -237,7 +237,7 @@ const handleCommand = async (command) => {
|
|||||||
const sessionMenuCacheKeys: string[] = [];
|
const sessionMenuCacheKeys: string[] = [];
|
||||||
for (let i = 0; i < sessionStorage.length; i++) {
|
for (let i = 0; i < sessionStorage.length; i++) {
|
||||||
const key = sessionStorage.key(i);
|
const key = sessionStorage.key(i);
|
||||||
if (key && key.startsWith('menu_cache_')) {
|
if (key && key.startsWith('menu_')) {
|
||||||
sessionMenuCacheKeys.push(key);
|
sessionMenuCacheKeys.push(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+19
@@ -12,6 +12,25 @@ declare module '@/*' {
|
|||||||
export default component;
|
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 {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_BASE_URL: string;
|
readonly VITE_API_BASE_URL: string;
|
||||||
// 添加其他环境变量...
|
// 添加其他环境变量...
|
||||||
|
|||||||
+11
-1
@@ -7,6 +7,7 @@ import 'element-plus/dist/index.css'
|
|||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import '@/assets/less/index.less'
|
import '@/assets/less/index.less'
|
||||||
import '@/assets/css/all.min.css'
|
import '@/assets/css/all.min.css'
|
||||||
|
import '@/assets/js/all.min.js'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { loadAndAddDynamicRoutes } from './router'
|
import { loadAndAddDynamicRoutes } from './router'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
@@ -36,8 +37,17 @@ authStore.checkAuth()
|
|||||||
|
|
||||||
// 如果用户已登录,在应用启动时加载动态路由
|
// 如果用户已登录,在应用启动时加载动态路由
|
||||||
if (authStore.isLoggedIn) {
|
if (authStore.isLoggedIn) {
|
||||||
loadAndAddDynamicRoutes().catch(err => {
|
loadAndAddDynamicRoutes()
|
||||||
|
.catch(err => {
|
||||||
console.error('应用启动时加载动态路由失败:', err);
|
console.error('应用启动时加载动态路由失败:', err);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// 检查是否因为 token 无效而导致路由加载失败
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (!token) {
|
||||||
|
authStore.clearToken();
|
||||||
|
window.location.href = '#/login';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+148
-29
@@ -1,55 +1,174 @@
|
|||||||
import { createComponentLoader } from '@/utils/pathResolver';
|
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 = '') {
|
export function convertMenusToRoutes(menus, parentPath = '') {
|
||||||
if (!menus || menus.length === 0) return [];
|
if (!menus || menus.length === 0) return [];
|
||||||
|
|
||||||
return menus.map(menu => {
|
return menus.map((menu) => {
|
||||||
// 拼接完整的路由路径(处理相对路径)
|
const fullPath = menu.path
|
||||||
const fullPath = menu.path ?
|
? menu.path.startsWith('/')
|
||||||
(menu.path.startsWith('/') ? menu.path : `${parentPath}/${menu.path}`)
|
? 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 = {
|
const route = {
|
||||||
path: fullPath || menu.path || '',
|
path: fullPath || menu.path || '',
|
||||||
name: `menu_${menu.id}`,
|
name: `menu_${menu.id}`,
|
||||||
meta: {
|
meta,
|
||||||
title: menu.title,
|
|
||||||
icon: menu.icon,
|
|
||||||
id: menu.id,
|
|
||||||
componentPath: menu.component_path
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 1. 处理组件加载
|
|
||||||
if (menu.type === 4) {
|
if (menu.type === 4) {
|
||||||
// 单页类型
|
|
||||||
route.component = () => import('@/views/onepage/index.vue');
|
route.component = () => import('@/views/onepage/index.vue');
|
||||||
} else if (menu.component_path && menu.component_path.trim() !== '') {
|
} else if (menu.component_path && menu.component_path.trim() !== '') {
|
||||||
// 正常页面
|
|
||||||
route.component = createComponentLoader(menu.component_path);
|
route.component = createComponentLoader(menu.component_path);
|
||||||
} else if (menu.children && menu.children.length > 0) {
|
} else if (hasChildren) {
|
||||||
// 目录节点:必须给组件,否则父级无法渲染子级
|
|
||||||
route.component = () => import('@/views/layouts/EmptyLayout.vue');
|
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);
|
route.children = convertMenusToRoutes(menu.children, fullPath);
|
||||||
|
|
||||||
// 目录节点添加重定向,防止点击父菜单页面空白
|
|
||||||
const firstChild = menu.children[0];
|
const firstChild = menu.children[0];
|
||||||
if (firstChild && firstChild.path) {
|
if (firstChild && firstChild.path) {
|
||||||
// 计算第一个子路由的完整路径
|
const childFullPath = firstChild.path.startsWith('/')
|
||||||
const childFullPath = firstChild.path.startsWith('/') ?
|
? firstChild.path
|
||||||
firstChild.path :
|
: `${fullPath}/${firstChild.path}`;
|
||||||
`${fullPath}/${firstChild.path}`;
|
|
||||||
route.redirect = childFullPath;
|
route.redirect = childFullPath;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
route.component = () => import('@/views/404/404.vue');
|
||||||
}
|
}
|
||||||
|
|
||||||
return route;
|
return route;
|
||||||
|
|||||||
+15
-3
@@ -24,6 +24,18 @@ const staticRoutes = [
|
|||||||
component: () => import("@/views/login/index.vue"),
|
component: () => import("@/views/login/index.vue"),
|
||||||
meta: { requiresAuth: false }
|
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",
|
path: "/home",
|
||||||
name: "Home",
|
name: "Home",
|
||||||
@@ -86,7 +98,7 @@ export async function loadAndAddDynamicRoutes() {
|
|||||||
console.error('加载动态路由失败:', error);
|
console.error('加载动态路由失败:', error);
|
||||||
dynamicRoutesAdded = true;
|
dynamicRoutesAdded = true;
|
||||||
routesLoadingPromise = null;
|
routesLoadingPromise = null;
|
||||||
return Promise.resolve();
|
throw error;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -117,7 +129,6 @@ function addDynamicRoutes(menus) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
dynamicRoutesAdded = true;
|
dynamicRoutesAdded = true;
|
||||||
// console.log('动态路由添加完成:', router.getRoutes().map(r => r.path));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findRouteByName(routes, routeName) {
|
function findRouteByName(routes, routeName) {
|
||||||
@@ -152,8 +163,9 @@ function findFirstValidRoute(routes) {
|
|||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const token = localStorage.getItem("token");
|
const token = localStorage.getItem("token");
|
||||||
|
const publicPaths = ["/login", "/register", "/forget"];
|
||||||
|
|
||||||
if (to.path === "/login") {
|
if (publicPaths.includes(to.path)) {
|
||||||
if (token) {
|
if (token) {
|
||||||
if (!dynamicRoutesAdded) {
|
if (!dynamicRoutesAdded) {
|
||||||
await loadAndAddDynamicRoutes();
|
await loadAndAddDynamicRoutes();
|
||||||
|
|||||||
+3
-1
@@ -7,6 +7,7 @@ const defaultUser = {
|
|||||||
account: '',
|
account: '',
|
||||||
name: '',
|
name: '',
|
||||||
group_id: '',
|
group_id: '',
|
||||||
|
type: 'backend',
|
||||||
avatar: ''
|
avatar: ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
account: userInfo.account || '',
|
account: userInfo.account || '',
|
||||||
name: userInfo.name || '',
|
name: userInfo.name || '',
|
||||||
group_id: userInfo.group_id || '',
|
group_id: userInfo.group_id || '',
|
||||||
tenant_id: userInfo.tenant_id || '',
|
type: 'backend',
|
||||||
|
tid: userInfo.tid || '',
|
||||||
avatar: userInfo.avatar || ''
|
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 {
|
try {
|
||||||
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
|
||||||
const loginType = userInfo.type || 'user';
|
const loginType = userInfo.type || 'user';
|
||||||
const roleId = userInfo.role || 0;
|
const roleId = userInfo.group_id || 0;
|
||||||
return `menu_cache_${loginType}_${roleId}`;
|
return `menu_cache_${loginType}_${roleId}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'menu_cache_default';
|
return 'menu_cache_default';
|
||||||
@@ -92,10 +92,6 @@ export const useMenuStore = defineStore('menu', () => {
|
|||||||
const cachedMenus = loadFromCache();
|
const cachedMenus = loadFromCache();
|
||||||
if (cachedMenus && cachedMenus.length > 0) {
|
if (cachedMenus && cachedMenus.length > 0) {
|
||||||
menus.value = cachedMenus;
|
menus.value = cachedMenus;
|
||||||
// 注释掉后台更新缓存的逻辑,避免重复请求
|
|
||||||
// fetchMenus(true).catch(err => {
|
|
||||||
// console.warn('后台更新菜单失败:', err);
|
|
||||||
// });
|
|
||||||
return Promise.resolve(cachedMenus);
|
return Promise.resolve(cachedMenus);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +109,7 @@ export const useMenuStore = defineStore('menu', () => {
|
|||||||
try {
|
try {
|
||||||
const userInfo = getUserInfo();
|
const userInfo = getUserInfo();
|
||||||
const loginType = userInfo.type || 'user';
|
const loginType = userInfo.type || 'user';
|
||||||
const roleId = userInfo.role || 0;
|
const roleId = userInfo.group_id || 0;
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
|
|
||||||
@@ -175,6 +171,13 @@ export const useMenuStore = defineStore('menu', () => {
|
|||||||
stack: err.stack
|
stack: err.stack
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 如果是 token 无效错误,不使用缓存,直接抛出
|
||||||
|
if (err.message === 'token无效' || err.response?.status === 401) {
|
||||||
|
clearCache();
|
||||||
|
menus.value = [];
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
// 如果出错,尝试使用缓存数据
|
// 如果出错,尝试使用缓存数据
|
||||||
const cachedMenus = loadFromCache();
|
const cachedMenus = loadFromCache();
|
||||||
if (cachedMenus && cachedMenus.length > 0) {
|
if (cachedMenus && cachedMenus.length > 0) {
|
||||||
|
|||||||
@@ -15,15 +15,20 @@ Object.keys(viewsModules).forEach(relativePath => {
|
|||||||
|
|
||||||
// 统一去掉扩展名进行存储,方便各种格式匹配
|
// 统一去掉扩展名进行存储,方便各种格式匹配
|
||||||
const baseNoExt = relativePath.replace('../views/', '').replace('.vue', '');
|
const baseNoExt = relativePath.replace('../views/', '').replace('.vue', '');
|
||||||
|
const baseWithExt = relativePath.replace('../views/', '');
|
||||||
|
|
||||||
// 1. 存储标准路径
|
// 1. 存储标准路径
|
||||||
pathMap.set(relativePath, viewsModules[relativePath]);
|
pathMap.set(relativePath, viewsModules[relativePath]);
|
||||||
// 2. 存储 @/views 路径
|
// 2. 存储 @/views 路径
|
||||||
pathMap.set(relativePath.replace('../views', '@/views'), viewsModules[relativePath]);
|
pathMap.set(relativePath.replace('../views', '@/views'), viewsModules[relativePath]);
|
||||||
// 3. 存储 /system/users 格式
|
// 3. 存储 /system/users 格式(不带扩展名)
|
||||||
pathMap.set(`/${baseNoExt}`, viewsModules[relativePath]);
|
pathMap.set(`/${baseNoExt}`, viewsModules[relativePath]);
|
||||||
// 4. 存储 system/users 格式
|
// 4. 存储 system/users 格式(不带扩展名)
|
||||||
pathMap.set(baseNoExt, viewsModules[relativePath]);
|
pathMap.set(baseNoExt, viewsModules[relativePath]);
|
||||||
|
// 5. 存储 /system/users.vue 格式(带扩展名)
|
||||||
|
pathMap.set(`/${baseWithExt}`, viewsModules[relativePath]);
|
||||||
|
// 6. 存储 system/users.vue 格式(带扩展名)
|
||||||
|
pathMap.set(baseWithExt, viewsModules[relativePath]);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-26
@@ -2,12 +2,6 @@ import axios from 'axios';
|
|||||||
|
|
||||||
// 获取API基础URL,添加调试信息
|
// 获取API基础URL,添加调试信息
|
||||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
||||||
if (!apiBaseURL) {
|
|
||||||
console.error('警告: VITE_API_BASE_URL 环境变量未设置!');
|
|
||||||
console.log('当前环境变量:', import.meta.env);
|
|
||||||
} else {
|
|
||||||
console.log('API基础URL:', apiBaseURL);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
@@ -24,22 +18,6 @@ service.interceptors.request.use(
|
|||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
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
|
// 对于有 body 的请求(POST、PUT、PATCH),确保设置 Content-Type
|
||||||
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
||||||
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
||||||
@@ -62,16 +40,14 @@ service.interceptors.response.use(
|
|||||||
if (error.response) {
|
if (error.response) {
|
||||||
switch (error.response.status) {
|
switch (error.response.status) {
|
||||||
case 401:
|
case 401:
|
||||||
// 处理未授权的错误 - 清除token并跳转登录页
|
|
||||||
console.error('未授权,请重新登录');
|
console.error('未授权,请重新登录');
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
// 避免循环导入,直接使用路由
|
localStorage.removeItem('userInfo');
|
||||||
if (window.location.hash !== '#/login') {
|
if (window.location.hash !== '#/login') {
|
||||||
window.location.href = '#/login';
|
window.location.href = '#/login';
|
||||||
}
|
}
|
||||||
break;
|
return Promise.reject(new Error('token无效'));
|
||||||
case 404:
|
case 404:
|
||||||
// 处理资源不存在的错误
|
|
||||||
console.error('请求的资源不存在');
|
console.error('请求的资源不存在');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
+15
-6
@@ -4,13 +4,22 @@
|
|||||||
<div class="not-found-big">404</div>
|
<div class="not-found-big">404</div>
|
||||||
<div class="not-found-message">页面未找到</div>
|
<div class="not-found-message">页面未找到</div>
|
||||||
<div class="not-found-desc">很抱歉,您访问的页面不存在或已被删除。</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
// 这里可以根据需要添加自定义逻辑
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const backToHome = () => {
|
||||||
|
// 使用 /home 作为首页(Hash模式下的系统导航页)
|
||||||
|
router.push("/home");
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -26,9 +35,9 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 60px 48px 40px;
|
padding: 60px 48px 40px;
|
||||||
border-radius: 16px;
|
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;
|
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 {
|
.not-found-big {
|
||||||
@@ -64,7 +73,7 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: background 0.2s;
|
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 {
|
.back-home-btn:hover {
|
||||||
|
|||||||
@@ -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="less">
|
|
||||||
.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,428 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑宝贝' : '添加宝贝'" width="600px" :close-on-click-modal="false"
|
|
||||||
@close="handleClose">
|
|
||||||
<div class="dialog-content">
|
|
||||||
|
|
||||||
<!-- 表单区域 -->
|
|
||||||
<el-form ref="editFormRef" :model="formData" :rules="formRules" label-width="100px">
|
|
||||||
<el-form-item label="头像" prop="avatar">
|
|
||||||
<div class="preview-image">
|
|
||||||
<img v-if="formData.avatar" :src="getEnvUrl(formData.avatar)" class="preview-img" />
|
|
||||||
<div v-else class="no-preview">
|
|
||||||
<el-icon>
|
|
||||||
<Picture />
|
|
||||||
</el-icon>
|
|
||||||
<span>头像预览</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 裁剪区域 -->
|
|
||||||
<div class="cutter-container">
|
|
||||||
<img-cutter ref="imgCutterRef" :img="formData.avatar ? getEnvUrl(formData.avatar) : ''" :cutWidth="400"
|
|
||||||
:cutHeight="400" :rate="'1:1'" :fixedNumber="[1, 1]" :fixed="true" :fixedBox="true" :original="false"
|
|
||||||
:autoCrop="true" :autoCropWidth="300" :autoCropHeight="300" :centerBox="true" :showChooseBtn="true"
|
|
||||||
:boxWidth="300" :boxHeight="300" @cutDown="handleCut" class="img-cutter-wrapper">
|
|
||||||
<template #cut>
|
|
||||||
<div style="padding: 20px; text-align: center;">
|
|
||||||
<el-button type="primary" size="small" @click="handleCropClick">确认裁剪</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #placeholder>
|
|
||||||
<div class="avatar-uploader">
|
|
||||||
<el-icon class="avatar-uploader-icon">
|
|
||||||
<Plus />
|
|
||||||
</el-icon>
|
|
||||||
<span class="upload-text">点击上传头像</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</img-cutter>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- <ImgCutter :cutWidth="300" :cutHeight="300" :tool="true" V-on:cutDown="handleCut" /> -->
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, reactive, watch, nextTick } from 'vue';
|
|
||||||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
|
|
||||||
import { Plus, Picture } from '@element-plus/icons-vue';
|
|
||||||
import { createBaby, editBaby } from '@/api/babyhealth';
|
|
||||||
import { uploadAvatar } from '@/api/file';
|
|
||||||
|
|
||||||
import ImgCutter from 'vue-img-cutter';
|
|
||||||
|
|
||||||
// 定义 ImgCutter 实例类型
|
|
||||||
interface ImgCutterInstance {
|
|
||||||
chooseImg: () => void;
|
|
||||||
resetCrop: () => void;
|
|
||||||
crop: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定义裁剪数据结构类型
|
|
||||||
interface CroppedData {
|
|
||||||
blob: Blob;
|
|
||||||
dataURL: string;
|
|
||||||
file: File;
|
|
||||||
fileName: string;
|
|
||||||
index: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BabyFormData {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
nickname: string;
|
|
||||||
sex: number;
|
|
||||||
birth: string;
|
|
||||||
height: number;
|
|
||||||
weight: number;
|
|
||||||
avatar: string;
|
|
||||||
status: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const imgCutterRef = ref<ImgCutterInstance | null>(null);
|
|
||||||
|
|
||||||
// 处理裁剪按钮点击
|
|
||||||
const handleCropClick = () => {
|
|
||||||
if (imgCutterRef.value) {
|
|
||||||
(imgCutterRef.value as any).crop();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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 cropDialogVisible = ref(false);
|
|
||||||
const croppingImage = ref('');
|
|
||||||
let selectedFile: File | null = null;
|
|
||||||
// 图片大小限制 (5MB)
|
|
||||||
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
|
||||||
|
|
||||||
// 支持的图片类型
|
|
||||||
const SUPPORTED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/gif'];
|
|
||||||
|
|
||||||
// 处理裁剪完成
|
|
||||||
const handleCut = async (data: CroppedData) => {
|
|
||||||
try {
|
|
||||||
// 创建FormData
|
|
||||||
const uploadFormData = new FormData();
|
|
||||||
uploadFormData.append('file', data.file);
|
|
||||||
uploadFormData.append('cate', 'baby_avatar');
|
|
||||||
|
|
||||||
// 上传裁剪后的图片
|
|
||||||
const response = await uploadAvatar(uploadFormData);
|
|
||||||
|
|
||||||
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("上传失败,请重试");
|
|
||||||
} finally {
|
|
||||||
// 清理
|
|
||||||
croppingImage.value = '';
|
|
||||||
selectedFile = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 关闭对话框
|
|
||||||
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);
|
|
||||||
} else {
|
|
||||||
isEdit.value = false;
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
}, { immediate: true });
|
|
||||||
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
if (!editFormRef.value) return;
|
|
||||||
|
|
||||||
await editFormRef.value.validate(async (valid) => {
|
|
||||||
if (valid) {
|
|
||||||
loading.value = true;
|
|
||||||
try {
|
|
||||||
let res;
|
|
||||||
if (formData.id) {
|
|
||||||
// 编辑
|
|
||||||
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 {
|
|
||||||
// 新增
|
|
||||||
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 lang="less" scoped>
|
|
||||||
.dialog-content {
|
|
||||||
padding: 20px 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-image {
|
|
||||||
width: 168px;
|
|
||||||
height: 168px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px dashed var(--el-border-color);
|
|
||||||
margin-right: 30px;
|
|
||||||
|
|
||||||
.preview-img {
|
|
||||||
max-width: 100%;
|
|
||||||
max-height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-preview {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 48px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: var(--el-text-color-placeholder);
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-section {
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
.cutter-container {
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.avatar-uploader) {
|
|
||||||
border: 1px dashed var(--el-border-color);
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: var(--el-transition-duration-fast);
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--el-color-primary);
|
|
||||||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-uploader-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
color: var(--el-text-color-placeholder);
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-text {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.img-cutter-wrapper {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 6px;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
:deep(.img-cutter) {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
max-width: none;
|
|
||||||
|
|
||||||
.cut-btn {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cut-box {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 350px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cut-container {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 350px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cut-preview-box {
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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="less">
|
|
||||||
.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,293 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="statistics-container">
|
|
||||||
<!-- 第一行:宝贝统计 -->
|
|
||||||
<el-row :gutter="20" class="data-overview">
|
|
||||||
<el-col :span="8" v-for="item in babyData" :key="item.title">
|
|
||||||
<el-card shadow="hover" class="data-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="icon-box" :style="{ backgroundColor: item.color }">
|
|
||||||
<i :class="item.icon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="text-box">
|
|
||||||
<div class="title">{{ item.title }}</div>
|
|
||||||
<div class="value">{{ item.value.toLocaleString() }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 第二行:用户统计 -->
|
|
||||||
<el-row :gutter="20" class="data-overview" style="margin-top: 20px;">
|
|
||||||
<el-col :span="8" v-for="item in userData" :key="item.title">
|
|
||||||
<el-card shadow="hover" class="data-card">
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="icon-box" :style="{ backgroundColor: item.color }">
|
|
||||||
<i :class="item.icon"></i>
|
|
||||||
</div>
|
|
||||||
<div class="text-box">
|
|
||||||
<div class="title">{{ item.title }}</div>
|
|
||||||
<div class="value">{{ item.value.toLocaleString() }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 图表区域 -->
|
|
||||||
<el-row :gutter="20" class="charts-row" style="margin-top: 20px;">
|
|
||||||
<!-- 宝贝增长趋势柱状图 -->
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card shadow="hover" header="宝贝增长趋势">
|
|
||||||
<div ref="babyChartRef" class="chart-box"></div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<!-- 用户增长趋势柱状图 -->
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card shadow="hover" header="用户增长趋势">
|
|
||||||
<div ref="userChartRef" class="chart-box"></div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
|
|
||||||
import * as echarts from 'echarts';
|
|
||||||
import { getDashborad } from '@/api/babyhealth';
|
|
||||||
|
|
||||||
// --- 类型定义 ---
|
|
||||||
interface SummaryItem {
|
|
||||||
title: string;
|
|
||||||
value: number;
|
|
||||||
icon: string;
|
|
||||||
color: string;
|
|
||||||
percentage: number;
|
|
||||||
isUp: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 响应式数据 ---
|
|
||||||
const babyChartRef = ref<HTMLElement | null>(null);
|
|
||||||
const userChartRef = ref<HTMLElement | null>(null);
|
|
||||||
const babyChartInstance = shallowRef<echarts.ECharts | null>(null);
|
|
||||||
const userChartInstance = shallowRef<echarts.ECharts | null>(null);
|
|
||||||
|
|
||||||
const babyData = ref<SummaryItem[]>([
|
|
||||||
{ title: '总宝贝数', value: 0, icon: 'fa-solid fa-baby', color: '#67C23A', percentage: 0, isUp: false },
|
|
||||||
{ title: '男宝宝数', value: 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
|
|
||||||
{ title: '女宝宝数', value: 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
|
|
||||||
]);
|
|
||||||
|
|
||||||
const userData = ref<SummaryItem[]>([
|
|
||||||
{ title: '总用户数', value: 0, icon: 'fa-solid fa-users', color: '#3973FF', percentage: 0, isUp: false },
|
|
||||||
{ title: '父亲数', value: 0, icon: 'fa-solid fa-person', color: '#409EFF', percentage: 0, isUp: false },
|
|
||||||
{ title: '母亲数', value: 0, icon: 'fa-solid fa-person-dress', color: '#F56C6C', percentage: 0, isUp: false }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 调用总输出接口
|
|
||||||
async function fetchGetDashborad() {
|
|
||||||
try {
|
|
||||||
const res = await getDashborad();
|
|
||||||
if (res.code === 200 && res.data) {
|
|
||||||
const { userCounts, babyCounts } = res.data;
|
|
||||||
|
|
||||||
// 更新宝贝数据
|
|
||||||
if (babyCounts) {
|
|
||||||
babyData.value[0].value = babyCounts.total || 0; // 总宝贝数
|
|
||||||
babyData.value[1].value = babyCounts.male || 0; // 男宝宝数
|
|
||||||
babyData.value[2].value = babyCounts.female || 0; // 女宝宝数
|
|
||||||
|
|
||||||
// 更新宝贝图表
|
|
||||||
updateBabyChart(
|
|
||||||
babyCounts.total || 0,
|
|
||||||
babyCounts.male || 0,
|
|
||||||
babyCounts.female || 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户数据
|
|
||||||
if (userCounts) {
|
|
||||||
userData.value[0].value = userCounts.total || 0; // 总用户数
|
|
||||||
userData.value[1].value = userCounts.father || 0; // 父亲数
|
|
||||||
userData.value[2].value = userCounts.mother || 0; // 母亲数
|
|
||||||
|
|
||||||
// 更新用户图表
|
|
||||||
updateUserChart(
|
|
||||||
userCounts.total || 0,
|
|
||||||
userCounts.father || 0,
|
|
||||||
userCounts.mother || 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取仪表盘数据失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 初始化宝贝柱状图
|
|
||||||
const initBabyChart = () => {
|
|
||||||
if (babyChartRef.value) {
|
|
||||||
babyChartInstance.value = echarts.init(babyChartRef.value);
|
|
||||||
babyChartInstance.value.setOption({
|
|
||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: ['总宝贝数', '男宝宝数', '女宝宝数'],
|
|
||||||
axisTick: { alignWithLabel: true }
|
|
||||||
},
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '数量',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: '60%',
|
|
||||||
data: [
|
|
||||||
{ value: 0, itemStyle: { color: '#67C23A' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 初始化用户柱状图
|
|
||||||
const initUserChart = () => {
|
|
||||||
if (userChartRef.value) {
|
|
||||||
userChartInstance.value = echarts.init(userChartRef.value);
|
|
||||||
userChartInstance.value.setOption({
|
|
||||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
|
||||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
|
||||||
xAxis: {
|
|
||||||
type: 'category',
|
|
||||||
data: ['总用户数', '父亲数', '母亲数'],
|
|
||||||
axisTick: { alignWithLabel: true }
|
|
||||||
},
|
|
||||||
yAxis: { type: 'value' },
|
|
||||||
series: [
|
|
||||||
{
|
|
||||||
name: '数量',
|
|
||||||
type: 'bar',
|
|
||||||
barWidth: '60%',
|
|
||||||
data: [
|
|
||||||
{ value: 0, itemStyle: { color: '#3973FF' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: 0, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新宝贝柱状图数据
|
|
||||||
const updateBabyChart = (total: number, male: number, female: number) => {
|
|
||||||
if (babyChartInstance.value) {
|
|
||||||
babyChartInstance.value.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: total, itemStyle: { color: '#67C23A' } },
|
|
||||||
{ value: male, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: female, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新用户柱状图数据
|
|
||||||
const updateUserChart = (total: number, father: number, mother: number) => {
|
|
||||||
if (userChartInstance.value) {
|
|
||||||
userChartInstance.value.setOption({
|
|
||||||
series: [{
|
|
||||||
data: [
|
|
||||||
{ value: total, itemStyle: { color: '#3973FF' } },
|
|
||||||
{ value: father, itemStyle: { color: '#409EFF' } },
|
|
||||||
{ value: mother, itemStyle: { color: '#F56C6C' } }
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 生命周期与自适应
|
|
||||||
const handleResize = () => {
|
|
||||||
babyChartInstance.value?.resize();
|
|
||||||
userChartInstance.value?.resize();
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
initBabyChart();
|
|
||||||
initUserChart();
|
|
||||||
// 等待图表初始化完成后再加载数据
|
|
||||||
await nextTick();
|
|
||||||
await fetchGetDashborad();
|
|
||||||
window.addEventListener('resize', handleResize);
|
|
||||||
});
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', handleResize);
|
|
||||||
babyChartInstance.value?.dispose();
|
|
||||||
userChartInstance.value?.dispose();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
.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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.charts-row {
|
|
||||||
margin-top: 20px;
|
|
||||||
|
|
||||||
.chart-box {
|
|
||||||
height: 350px;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<template>
|
|
||||||
<router-view />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup></script>
|
|
||||||
|
|
||||||
<style lang="less" scoped></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="less" 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,521 +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" :action="uploadUrl" :headers="uploadHeaders"
|
|
||||||
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 裁剪对话框 -->
|
|
||||||
<CropImage ref="cropImageRef" @confirm="handleCropConfirm" />
|
|
||||||
|
|
||||||
<!-- 姓名 -->
|
|
||||||
<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/file';
|
|
||||||
|
|
||||||
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 '';
|
|
||||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
return `${import.meta.env.VITE_API_BASE_URL}${url}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 头像上传前校验 - 只校验格式,打开裁剪对话框
|
|
||||||
const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
|
|
||||||
const isImage = rawFile.type.startsWith('image/');
|
|
||||||
const isLt2M = rawFile.size / 1024 / 1024 < 2;
|
|
||||||
|
|
||||||
if (!isImage) {
|
|
||||||
ElMessage.error('只能上传图片文件!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!isLt2M) {
|
|
||||||
ElMessage.error('图片大小不能超过 2MB!');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 上传文件到服务器(裁剪后调用)
|
|
||||||
const uploadFile = async (file: File) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('file', file);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用uploadAvatar接口上传文件
|
|
||||||
const response = await uploadAvatar(formData, { cate: 'user_avatar' });
|
|
||||||
|
|
||||||
if (response.code === 200 || response.code === 201) {
|
|
||||||
// 更新用户头像
|
|
||||||
form.value.avatar = response.data.url || response.data.path || '';
|
|
||||||
ElMessage.success("头像上传成功");
|
|
||||||
} else {
|
|
||||||
ElMessage.error(response.msg || "上传失败");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('上传失败:', error);
|
|
||||||
ElMessage.error("上传失败,请重试");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 裁剪确认后的处理
|
|
||||||
const handleCropConfirm = async (file: File) => {
|
|
||||||
await uploadFile(file);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 头像上传成功
|
|
||||||
const handleAvatarSuccess = (response: any) => {
|
|
||||||
if (response.code === 200 || response.code === 201) {
|
|
||||||
form.value.avatar = response.data.url || response.data.path || '';
|
|
||||||
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="less" 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</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>
|
|
||||||
@@ -1,7 +1,23 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="category-manager">
|
<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 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">
|
<div v-else class="category-tree">
|
||||||
<category-node
|
<category-node
|
||||||
@@ -53,6 +69,13 @@ const currentEdit = ref(null);
|
|||||||
const searchText = ref("");
|
const searchText = ref("");
|
||||||
const categories = ref([]);
|
const categories = ref([]);
|
||||||
|
|
||||||
|
// 新增分类
|
||||||
|
const addCategory = ref(null);
|
||||||
|
const handleAdd = () => {
|
||||||
|
addCategory.value = { parentId: 0 };
|
||||||
|
dialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const totalCount = computed(() => categories.value.length);
|
const totalCount = computed(() => categories.value.length);
|
||||||
|
|
||||||
@@ -176,6 +199,8 @@ onMounted(() => {
|
|||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.category-manager {
|
.category-manager {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: var(--el-bg-color-page);
|
||||||
|
|
||||||
// 页面头部样式
|
// 页面头部样式
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ const cateOptions = ref([]);
|
|||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
title: "",
|
title: "",
|
||||||
author: "云泽网",
|
author: "",
|
||||||
cate: "",
|
cate: "",
|
||||||
content: "",
|
content: "",
|
||||||
image: "",
|
image: "",
|
||||||
@@ -503,7 +503,7 @@ function resetForm() {
|
|||||||
// 重置表单数据
|
// 重置表单数据
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
title: "",
|
title: "",
|
||||||
author: "云泽网",
|
author: "",
|
||||||
cate: "",
|
cate: "",
|
||||||
content: "",
|
content: "",
|
||||||
image: "",
|
image: "",
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<div class="search-bar">
|
<div class="search-bar">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="搜索文章标题/作者"
|
placeholder="搜索文章标题"
|
||||||
clearable
|
clearable
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
<template>
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
<!-- 性别 -->
|
<!-- 性别 -->
|
||||||
<el-form-item label="性别" required>
|
<el-form-item label="性别" required>
|
||||||
<el-radio-group v-model="form.sex" placeholder="请选择性别">
|
<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="1" />
|
||||||
<el-radio-button label="女" :value="2" />
|
<el-radio-button label="女" :value="2" />
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
<el-date-picker
|
<el-date-picker
|
||||||
v-model="form.birthday"
|
v-model="form.birthday"
|
||||||
type="date"
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
placeholder="请选择生日"
|
placeholder="请选择生日"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -57,14 +59,14 @@
|
|||||||
<!-- 隶属单位 -->
|
<!-- 隶属单位 -->
|
||||||
<el-form-item label="隶属单位" required>
|
<el-form-item label="隶属单位" required>
|
||||||
<el-select v-model="form.affiliate_unit" placeholder="请选择隶属单位" clearable style="width: 100%">
|
<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="item.id" />
|
<el-option v-for="item in companyList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 部门 -->
|
<!-- 部门 -->
|
||||||
<el-form-item label="部门" required>
|
<el-form-item label="部门" required>
|
||||||
<el-select v-model="form.department" placeholder="请先选择隶属单位" clearable style="width: 100%" :disabled="!form.affiliate_unit">
|
<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="item.id" />
|
<el-option v-for="item in departmentList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
@@ -80,7 +82,7 @@
|
|||||||
|
|
||||||
<!-- 民族 -->
|
<!-- 民族 -->
|
||||||
<el-form-item label="民族" required>
|
<el-form-item label="民族" required>
|
||||||
<el-input v-model="form.nationality" placeholder="请输入民族" />
|
<el-input v-model="form.nation" placeholder="请输入民族" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 手机 -->
|
<!-- 手机 -->
|
||||||
@@ -126,7 +128,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed, watch, nextTick } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { createEmployee, editEmployee, getEmployeeDetail, getCompanys, getDepartments } from "@/api/erp";
|
import { createEmployee, editEmployee, getEmployeeDetail, getCompanys, getDepartments } from "@/api/erp";
|
||||||
|
|
||||||
@@ -152,6 +154,7 @@ import { ref, computed, watch } from "vue";
|
|||||||
const isAdd = ref(false);
|
const isAdd = ref(false);
|
||||||
const companyList = ref<any[]>([]);
|
const companyList = ref<any[]>([]);
|
||||||
const departmentList = ref<any[]>([]);
|
const departmentList = ref<any[]>([]);
|
||||||
|
const isHydratingUserData = ref(false);
|
||||||
|
|
||||||
// 获取公司列表
|
// 获取公司列表
|
||||||
const loadCompanyList = async () => {
|
const loadCompanyList = async () => {
|
||||||
@@ -176,25 +179,37 @@ import { ref, computed, watch } from "vue";
|
|||||||
|
|
||||||
const form = ref<any>({
|
const form = ref<any>({
|
||||||
id: null,
|
id: null,
|
||||||
|
tid: 1,
|
||||||
|
tenant_id: 1,
|
||||||
account: "",
|
account: "",
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
qq: "",
|
qq: "",
|
||||||
sex: 1,
|
sex: 0,
|
||||||
|
birthday: "",
|
||||||
|
affiliate_unit: "",
|
||||||
|
department: "",
|
||||||
|
position: "",
|
||||||
|
education: "",
|
||||||
|
nation: "",
|
||||||
|
wechat: "",
|
||||||
|
home_address: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
account_status: 1,
|
||||||
status: 1,
|
status: 1,
|
||||||
affiliate_unit: "",
|
|
||||||
department: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听隶属单位变化,加载对应部门
|
// 监听隶属单位变化,加载对应部门
|
||||||
watch(() => form.value.affiliate_unit, (newVal) => {
|
watch(() => form.value.affiliate_unit, (newVal) => {
|
||||||
|
if (isHydratingUserData.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
form.value.department = "";
|
form.value.department = "";
|
||||||
departmentList.value = [];
|
departmentList.value = [];
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
loadDepartments(newVal);
|
loadDepartments(Number(newVal));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,25 +316,59 @@ import { ref, computed, watch } from "vue";
|
|||||||
const sexValue =
|
const sexValue =
|
||||||
data.sex !== undefined && data.sex !== null
|
data.sex !== undefined && data.sex !== null
|
||||||
? Number(data.sex)
|
? Number(data.sex)
|
||||||
: 1;
|
: data.gender !== undefined && data.gender !== null
|
||||||
|
? Number(data.gender)
|
||||||
|
: 0;
|
||||||
|
|
||||||
const statusValue =
|
const statusValue =
|
||||||
data.status !== undefined && data.status !== null
|
data.account_status !== undefined && data.account_status !== null
|
||||||
|
? Number(data.account_status)
|
||||||
|
: data.status !== undefined && data.status !== null
|
||||||
? Number(data.status)
|
? Number(data.status)
|
||||||
: 1;
|
: 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 = {
|
form.value = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
account: data.account,
|
tid: data.tid || data.tenant_id || 1,
|
||||||
name: data.name,
|
tenant_id: data.tenant_id || data.tid || 1,
|
||||||
phone: data.phone,
|
account: data.account || "",
|
||||||
qq: data.qq,
|
name: data.name || "",
|
||||||
|
phone: data.phone || "",
|
||||||
|
qq: data.qq || "",
|
||||||
sex: sexValue,
|
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: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
email: data.email,
|
email: data.email || "",
|
||||||
|
account_status: statusValue,
|
||||||
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) {
|
} catch (e: any) {
|
||||||
console.error("Failed to load user data:", e);
|
console.error("Failed to load user data:", e);
|
||||||
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
|
||||||
@@ -370,13 +419,24 @@ import { ref, computed, watch } from "vue";
|
|||||||
if (isAdd.value) {
|
if (isAdd.value) {
|
||||||
// 新增用户
|
// 新增用户
|
||||||
const submitData: any = {
|
const submitData: any = {
|
||||||
|
tid: form.value.tid,
|
||||||
|
tenant_id: form.value.tenant_id,
|
||||||
account: form.value.account,
|
account: form.value.account,
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
phone: form.value.phone,
|
phone: form.value.phone,
|
||||||
qq: form.value.qq,
|
qq: form.value.qq,
|
||||||
sex: form.value.sex,
|
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,
|
email: form.value.email,
|
||||||
status: form.value.status,
|
home_address: form.value.home_address,
|
||||||
|
account_status: form.value.account_status,
|
||||||
|
status: form.value.account_status,
|
||||||
password: form.value.password,
|
password: form.value.password,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -391,13 +451,24 @@ import { ref, computed, watch } from "vue";
|
|||||||
|
|
||||||
const submitData: any = {
|
const submitData: any = {
|
||||||
id: form.value.id,
|
id: form.value.id,
|
||||||
|
tid: form.value.tid,
|
||||||
|
tenant_id: form.value.tenant_id,
|
||||||
account: form.value.account,
|
account: form.value.account,
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
phone: form.value.phone,
|
phone: form.value.phone,
|
||||||
qq: form.value.qq,
|
qq: form.value.qq,
|
||||||
sex: form.value.sex,
|
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,
|
email: form.value.email,
|
||||||
status: form.value.status,
|
home_address: form.value.home_address,
|
||||||
|
account_status: form.value.account_status,
|
||||||
|
status: form.value.account_status,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 只有在填写了密码时才添加到提交数据中
|
// 只有在填写了密码时才添加到提交数据中
|
||||||
@@ -424,14 +495,25 @@ import { ref, computed, watch } from "vue";
|
|||||||
isAdd.value = true;
|
isAdd.value = true;
|
||||||
form.value = {
|
form.value = {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
tid: 1,
|
||||||
|
tenant_id: 1,
|
||||||
account: "",
|
account: "",
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
qq: "",
|
qq: "",
|
||||||
sex: 1,
|
sex: 0,
|
||||||
|
birthday: "",
|
||||||
|
affiliate_unit: "",
|
||||||
|
department: "",
|
||||||
|
position: "",
|
||||||
|
education: "",
|
||||||
|
nation: "",
|
||||||
|
wechat: "",
|
||||||
|
home_address: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
account_status: 1,
|
||||||
status: 1,
|
status: 1,
|
||||||
};
|
};
|
||||||
loadCompanyList();
|
loadCompanyList();
|
||||||
@@ -461,14 +543,25 @@ import { ref, computed, watch } from "vue";
|
|||||||
isAdd.value = true;
|
isAdd.value = true;
|
||||||
form.value = {
|
form.value = {
|
||||||
id: 0,
|
id: 0,
|
||||||
|
tid: 1,
|
||||||
|
tenant_id: 1,
|
||||||
account: "",
|
account: "",
|
||||||
name: "",
|
name: "",
|
||||||
phone: "",
|
phone: "",
|
||||||
qq: "",
|
qq: "",
|
||||||
sex: 1,
|
sex: 0,
|
||||||
|
birthday: "",
|
||||||
|
affiliate_unit: "",
|
||||||
|
department: "",
|
||||||
|
position: "",
|
||||||
|
education: "",
|
||||||
|
nation: "",
|
||||||
|
wechat: "",
|
||||||
|
home_address: "",
|
||||||
password: "",
|
password: "",
|
||||||
confirmPassword: "",
|
confirmPassword: "",
|
||||||
email: "",
|
email: "",
|
||||||
|
account_status: 1,
|
||||||
status: 1,
|
status: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -488,4 +581,3 @@ import { ref, computed, watch } from "vue";
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
||||||
|
|||||||
@@ -22,25 +22,33 @@
|
|||||||
|
|
||||||
<!-- 用户列表表格 -->
|
<!-- 用户列表表格 -->
|
||||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
<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="id" label="ID" align="center" fixed="left" width="80" />
|
||||||
<el-table-column prop="account" label="账号" align="center" />
|
<el-table-column prop="account" label="账号" align="center" width="120" />
|
||||||
<el-table-column prop="name" label="姓名" align="center">
|
<el-table-column prop="name" label="姓名" align="center" width="120">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="qq" label="隶属单位" align="center" />
|
<el-table-column label="隶属单位" align="center">
|
||||||
<el-table-column prop="phone" label="部门" align="center" />
|
|
||||||
<el-table-column prop="phone" label="职位" align="center" />
|
|
||||||
<el-table-column prop="status" label="账号状态" width="80" align="center">
|
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
{{ scope.row.affiliate_unit_name || scope.row.affiliate_unit || "-" }}
|
||||||
scope.row.status === 1 ? "启用" : "禁用"
|
</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>
|
}}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="240" align="center" fixed="right">
|
<el-table-column label="操作" width="240" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope"><!-- -->
|
||||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
||||||
<el-button size="small" @click="handleEdit(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 size="small" type="warning" @click="handleChangePassword(scope.row)">
|
||||||
@@ -58,6 +66,12 @@
|
|||||||
layout="total, prev, pager, next" />
|
layout="total, prev, pager, next" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 查看弹窗 -->
|
||||||
|
<View ref="viewRef" />
|
||||||
|
|
||||||
|
<!-- 修改密码弹窗 -->
|
||||||
|
<ChangePass ref="changePassRef" @submit="fetchData" />
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
<!-- 编辑弹窗 -->
|
||||||
<Edit ref="editRef" @submit="fetchData" />
|
<Edit ref="editRef" @submit="fetchData" />
|
||||||
</div>
|
</div>
|
||||||
@@ -69,6 +83,8 @@ import { ElMessage, ElMessageBox } from "element-plus";
|
|||||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||||
import { getEmployeeList, deleteEmployee } from "@/api/erp";
|
import { getEmployeeList, deleteEmployee } from "@/api/erp";
|
||||||
import Edit from "./components/edit.vue";
|
import Edit from "./components/edit.vue";
|
||||||
|
import View from "./components/view.vue";
|
||||||
|
import ChangePass from "./components/changepass.vue";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
const users = ref<any[]>([]);
|
const users = ref<any[]>([]);
|
||||||
@@ -77,17 +93,24 @@ const page = ref(1);
|
|||||||
const pageSize = ref(10);
|
const pageSize = ref(10);
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const editRef = ref<any>(null);
|
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 authStore = useAuthStore();
|
||||||
const tenantId = (authStore.user as any)?.tenant_id;
|
const tenantId = (authStore.user as any)?.tid;
|
||||||
|
|
||||||
// console.log(tenantId);
|
// console.log(tenantId);
|
||||||
|
|
||||||
// 获取列表数据
|
// 获取列表数据
|
||||||
const fetchData = async (tenantId: number) => {
|
const fetchData = async (currentTenantId = tenantId) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getEmployeeList(tenantId);
|
const res = await getEmployeeList(currentTenantId);
|
||||||
users.value = res.data || res || [];
|
users.value = res.data || res || [];
|
||||||
total.value = users.value.length;
|
total.value = users.value.length;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -112,9 +135,13 @@ const handleEdit = (row: any) => {
|
|||||||
editRef.value?.openEdit(row);
|
editRef.value?.openEdit(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangePassword = (row: any) => {
|
||||||
|
changePassRef.value?.openChangePass(row);
|
||||||
|
};
|
||||||
|
|
||||||
// 查看
|
// 查看
|
||||||
const handlePreview = (row: any) => {
|
const handlePreview = (row: any) => {
|
||||||
editRef.value?.openEdit(row);
|
viewRef.value?.openView(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 删除用户
|
// 删除用户
|
||||||
|
|||||||
@@ -15,8 +15,13 @@
|
|||||||
<el-input v-model="formData.org_code" placeholder="请输入组织编码,例如:LYG-YZ-GROUP" />
|
<el-input v-model="formData.org_code" placeholder="请输入组织编码,例如:LYG-YZ-GROUP" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="负责人" prop="leader_id">
|
<el-form-item label="负责人" prop="leader_id">
|
||||||
<el-select v-model="formData.leader_id" placeholder="请选择负责人">
|
<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" :value="item.id" />
|
<el-option
|
||||||
|
v-for="item in leaderList"
|
||||||
|
:key="item.id"
|
||||||
|
:label="item.name || item.account || String(item.id)"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序" prop="sort">
|
<el-form-item label="排序" prop="sort">
|
||||||
@@ -90,8 +95,8 @@ const loading = ref(false)
|
|||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
const isEdit = ref(false)
|
const isEdit = ref(false)
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const tenantId = (authStore.user as any)?.tenant_id
|
const tenantId = (authStore.user as any)?.tid
|
||||||
const leaderList = ref([])
|
const leaderList = ref<Array<{ id: number; name?: string; account?: string }>>([])
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
id: 0,
|
id: 0,
|
||||||
parent_id: undefined as number | undefined,
|
parent_id: undefined as number | undefined,
|
||||||
@@ -126,14 +131,26 @@ const extendedTreeData = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const getLeaderList = async () => {
|
const getLeaderList = async () => {
|
||||||
const res = await getTenantUsers(tenantId)
|
if (!tenantId) {
|
||||||
if (res.code === 200) {
|
leaderList.value = []
|
||||||
leaderList.value = res.data.list
|
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) => {
|
const open = (data?: TreeNode, parentId?: number) => {
|
||||||
|
getLeaderList()
|
||||||
if (data) {
|
if (data) {
|
||||||
// 编辑:只拷贝需要的字段,避免把 children 等多余字段带到表单
|
// 编辑:只拷贝需要的字段,避免把 children 等多余字段带到表单
|
||||||
isEdit.value = true
|
isEdit.value = true
|
||||||
@@ -141,7 +158,7 @@ const open = (data?: TreeNode, parentId?: number) => {
|
|||||||
formData.parent_id = data.parent_id ?? 0
|
formData.parent_id = data.parent_id ?? 0
|
||||||
formData.org_name = data.org_name
|
formData.org_name = data.org_name
|
||||||
formData.org_code = data.org_code || ''
|
formData.org_code = data.org_code || ''
|
||||||
formData.leader_id = data.leader_id
|
formData.leader_id = data.leader_id === undefined || data.leader_id === null ? undefined : Number(data.leader_id)
|
||||||
formData.is_company = data.is_company ?? 0
|
formData.is_company = data.is_company ?? 0
|
||||||
formData.sort = data.sort ?? 0
|
formData.sort = data.sort ?? 0
|
||||||
formData.status = data.status ?? 1
|
formData.status = data.status ?? 1
|
||||||
@@ -192,7 +209,7 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
submitData.tenant_id = tenantId
|
submitData.tid = tenantId
|
||||||
}
|
}
|
||||||
res = await editOrganization(formData.id, submitData)
|
res = await editOrganization(formData.id, submitData)
|
||||||
} else {
|
} else {
|
||||||
@@ -205,7 +222,7 @@ const handleSubmit = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (tenantId) {
|
if (tenantId) {
|
||||||
submitData.append('tenant_id', String(tenantId))
|
submitData.append('tid', String(tenantId))
|
||||||
}
|
}
|
||||||
res = await createOrganization(submitData)
|
res = await createOrganization(submitData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '
|
|||||||
// 树形结构数据(与后台字段对应)
|
// 树形结构数据(与后台字段对应)
|
||||||
interface TreeNode {
|
interface TreeNode {
|
||||||
id: number
|
id: number
|
||||||
tenant_id?: number
|
tid?: number
|
||||||
org_name: string
|
org_name: string
|
||||||
org_code?: string
|
org_code?: string
|
||||||
parent_id?: number
|
parent_id?: number
|
||||||
@@ -153,7 +153,7 @@ const buildTree = (list: any[]): TreeNode[] => {
|
|||||||
list.forEach(item => {
|
list.forEach(item => {
|
||||||
const node: TreeNode = {
|
const node: TreeNode = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
tenant_id: item.tenant_id,
|
tid: item.tid,
|
||||||
org_name: item.org_name,
|
org_name: item.org_name,
|
||||||
org_code: item.org_code,
|
org_code: item.org_code,
|
||||||
parent_id: item.parent_id,
|
parent_id: item.parent_id,
|
||||||
@@ -193,7 +193,7 @@ const loadOrganizationDetail = async (id: number) => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
selectedNode.value = {
|
selectedNode.value = {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
tenant_id: data.tenant_id,
|
tid: data.tid,
|
||||||
org_name: data.org_name,
|
org_name: data.org_name,
|
||||||
org_code: data.org_code,
|
org_code: data.org_code,
|
||||||
parent_id: data.parent_id,
|
parent_id: data.parent_id,
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -227,17 +227,20 @@ const handleSubmit = async () => {
|
|||||||
try {
|
try {
|
||||||
await formRef.value.validate();
|
await formRef.value.validate();
|
||||||
|
|
||||||
// 组合选中节点(全选 + 半选)
|
// 只获取完全选中的节点(不包含半选父节点)
|
||||||
const checkedKeys = treeRef.value.getCheckedKeys();
|
const checkedKeys = treeRef.value.getCheckedKeys();
|
||||||
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
|
|
||||||
const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys];
|
|
||||||
|
|
||||||
const submitData = {
|
const submitData: any = {
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
status: form.value.status,
|
status: form.value.status,
|
||||||
rights: allCheckedKeys,
|
rights: checkedKeys,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 编辑时添加 id 字段
|
||||||
|
if (isEdit.value && props.role) {
|
||||||
|
submitData.id = props.role.id;
|
||||||
|
}
|
||||||
|
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
|
|
||||||
let res;
|
let res;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
详情
|
详情
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.default !== 1 && row.default !== 2"
|
v-if="userInfo?.id === 2"
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
link
|
link
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="row.id !== 1"
|
v-if="userInfo?.id === 2"
|
||||||
size="small"
|
size="small"
|
||||||
type="danger"
|
type="danger"
|
||||||
link
|
link
|
||||||
|
|||||||
@@ -20,10 +20,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import type { FormInstance, FormRules } from "element-plus";
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
|
|
||||||
|
import { getCompanyInfos, saveCompanyInfos } from "@/api/sitesettings";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
const contactFormRef = ref<FormInstance>();
|
const contactFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
const contactForm = reactive({
|
const contactForm = reactive({
|
||||||
@@ -37,12 +41,35 @@ const contactRules: FormRules = {
|
|||||||
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }]
|
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取企业信息
|
||||||
|
const initCompanyInfos = async () => {
|
||||||
|
const res = await getCompanyInfos(authStore.user.tid);
|
||||||
|
if (res.code === 200 && res.data) {
|
||||||
|
const data = res.data;
|
||||||
|
contactForm.phone = data.contact_phone || "";
|
||||||
|
contactForm.email = data.contact_email || "";
|
||||||
|
contactForm.address = data.address || "";
|
||||||
|
contactForm.workTime = data.worktime || "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const saveContactSettings = async () => {
|
const saveContactSettings = async () => {
|
||||||
if (!contactFormRef.value) return;
|
if (!contactFormRef.value) return;
|
||||||
await contactFormRef.value.validate((valid) => {
|
await contactFormRef.value.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
// TODO: 保存联系方式
|
const data = {
|
||||||
|
tid: authStore.user.tid,
|
||||||
|
contact_phone: contactForm.phone,
|
||||||
|
contact_email: contactForm.email,
|
||||||
|
address: contactForm.address,
|
||||||
|
worktime: contactForm.workTime
|
||||||
|
};
|
||||||
|
const res = await saveCompanyInfos(data);
|
||||||
|
if (res.code === 200) {
|
||||||
ElMessage.success("联系方式保存成功");
|
ElMessage.success("联系方式保存成功");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "保存失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -57,4 +84,9 @@ const resetContactForm = () => {
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
contactForm
|
contactForm
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 初始化数据
|
||||||
|
onMounted(() => {
|
||||||
|
initCompanyInfos();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="saveLegalInfos">保存设置</el-button>
|
<el-button type="primary" @click="handleSaveLegalInfos">保存设置</el-button>
|
||||||
<el-button @click="resetLegalNoticeForm">重置</el-button>
|
<el-button @click="resetLegalNoticeForm">重置</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -34,7 +34,9 @@ import { ElMessage } from "element-plus";
|
|||||||
import type { FormInstance, FormRules } from "element-plus";
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
|
|
||||||
import { getLegalInfos, saveLegalInfos } from "@/api/sitesettings";
|
import { getLegalInfos, saveLegalInfos } from "@/api/sitesettings";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
const legalNoticeFormRef = ref<FormInstance>();
|
const legalNoticeFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
const legalNotice = ref("");
|
const legalNotice = ref("");
|
||||||
@@ -64,12 +66,21 @@ const initLegalInfos = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveLegalInfos = async () => {
|
const handleSaveLegalInfos = async () => {
|
||||||
if (!legalNoticeFormRef.value) return;
|
if (!legalNoticeFormRef.value) return;
|
||||||
await legalNoticeFormRef.value.validate((valid) => {
|
await legalNoticeFormRef.value.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
// TODO: 保存法律声明和隐私条款
|
const data = {
|
||||||
ElMessage.success("法律声明和隐私条款保存成功");
|
tid: authStore.user.tid,
|
||||||
|
legalNotice: legalNotice.value,
|
||||||
|
privacyTerms: privacyTerms.value
|
||||||
|
};
|
||||||
|
const res = await saveLegalInfos(data);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("保存成功");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "保存失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="logo"
|
v-if="logo"
|
||||||
:src="API_BASE_URL + logo.replace(/^\//, '/')"
|
:src="getPreviewUrl(logo)"
|
||||||
class="logo-image"
|
class="logo-image"
|
||||||
/>
|
/>
|
||||||
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
|
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
|
||||||
@@ -33,17 +33,36 @@
|
|||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="logow"
|
v-if="logow"
|
||||||
:src="API_BASE_URL + logow.replace(/^\//, '/')"
|
:src="getPreviewUrl(logow)"
|
||||||
class="logo-image darkbg"
|
class="logo-image darkbg"
|
||||||
/>
|
/>
|
||||||
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
|
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="站点图标" prop="ico">
|
||||||
|
<el-upload
|
||||||
|
class="logo-uploader"
|
||||||
|
:show-file-list="false"
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="handleIconChange"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="ico"
|
||||||
|
:src="getPreviewUrl(ico)"
|
||||||
|
class="logo-image"
|
||||||
|
/>
|
||||||
|
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
|
||||||
|
</el-upload>
|
||||||
|
<div class="form-tip">请上传ico格式文件</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="企业名称" prop="companyname">
|
<el-form-item label="企业名称" prop="companyname">
|
||||||
<el-input v-model="companyname" placeholder="请输入企业名称" />
|
<el-input v-model="companyname" placeholder="请输入企业名称" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="站点SEO描述" prop="description">
|
<el-form-item label="企业介绍" prop="companyintroduction">
|
||||||
<el-input v-model="description" placeholder="请输入站点SEO描述" />
|
<el-input v-model="companyintroduction" type="textarea" rows="5" placeholder="请输入企业介绍" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="站点简介" prop="description">
|
||||||
|
<el-input v-model="description" placeholder="请输入站点简介" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="版权信息" prop="copyright">
|
<el-form-item label="版权信息" prop="copyright">
|
||||||
<el-input v-model="copyright" placeholder="如:© 2026 公司名称" />
|
<el-input v-model="copyright" placeholder="如:© 2026 公司名称" />
|
||||||
@@ -67,24 +86,30 @@ import { Plus } from "@element-plus/icons-vue";
|
|||||||
import type { FormInstance, FormRules, UploadFile } from "element-plus";
|
import type { FormInstance, FormRules, UploadFile } from "element-plus";
|
||||||
import { getNormalInfos, saveNormalInfos } from "@/api/sitesettings";
|
import { getNormalInfos, saveNormalInfos } from "@/api/sitesettings";
|
||||||
import { uploadFile } from "@/api/file";
|
import { uploadFile } from "@/api/file";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
const normalFormRef = ref<FormInstance>();
|
const normalFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
const sitename = ref("");
|
const sitename = ref("");
|
||||||
const companyname = ref("");
|
const companyname = ref("");
|
||||||
|
const companyintroduction = ref("");
|
||||||
const logo = ref("");
|
const logo = ref("");
|
||||||
const logow = ref("");
|
const logow = ref("");
|
||||||
const description = ref("");
|
const description = ref("");
|
||||||
const copyright = ref("");
|
const copyright = ref("");
|
||||||
const icp = ref("");
|
const icp = ref("");
|
||||||
|
const ico = ref("");
|
||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
sitename,
|
sitename,
|
||||||
companyname,
|
companyname,
|
||||||
|
companyintroduction,
|
||||||
logo,
|
logo,
|
||||||
logow,
|
logow,
|
||||||
|
ico,
|
||||||
description,
|
description,
|
||||||
copyright,
|
copyright,
|
||||||
icp,
|
icp,
|
||||||
@@ -94,22 +119,32 @@ const normalRules: FormRules = {
|
|||||||
sitename: [{ required: true, message: "请输入站点名称", trigger: "blur" }],
|
sitename: [{ required: true, message: "请输入站点名称", trigger: "blur" }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPreviewUrl = (url: string) => {
|
||||||
|
const value = String(url || "").trim().replace(/\\/g, "/");
|
||||||
|
if (!value) return "";
|
||||||
|
if (/^(https?:\/\/|\/\/|data:|blob:)/i.test(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base = String(API_BASE_URL || "").replace(/\/+$/, "");
|
||||||
|
const path = value.startsWith("/") ? value : `/${value}`;
|
||||||
|
return `${base}${path}`;
|
||||||
|
};
|
||||||
|
|
||||||
//调用基础数据
|
//调用基础数据
|
||||||
const initNormalInfos = async () => {
|
const initNormalInfos = async () => {
|
||||||
const res = await getNormalInfos();
|
const res = await getNormalInfos(authStore.user.tid);
|
||||||
if (res.code === 200 && res.data) {
|
if (res.code === 200 && res.data) {
|
||||||
const data = res.data;
|
const data = res.data;
|
||||||
const dataMap: Record<string, string> = {};
|
sitename.value = data.sitename || "";
|
||||||
data.forEach((item: any) => {
|
companyintroduction.value = data.companyintroduction || "";
|
||||||
dataMap[item.label] = item.value;
|
logo.value = data.logo || "";
|
||||||
});
|
logow.value = data.logow || "";
|
||||||
sitename.value = dataMap["sitename"] || "";
|
ico.value = data.ico || "";
|
||||||
logo.value = dataMap["logo"] || "";
|
description.value = data.description || "";
|
||||||
logow.value = dataMap["logow"] || "";
|
copyright.value = data.copyright || "";
|
||||||
description.value = dataMap["description"] || "";
|
icp.value = data.icp || "";
|
||||||
copyright.value = dataMap["copyright"] || "";
|
companyname.value = data.companyname || "";
|
||||||
icp.value = dataMap["icp"] || "";
|
|
||||||
companyname.value = dataMap["companyname"] || "";
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -153,15 +188,18 @@ const handleSaveNormalInfos = async () => {
|
|||||||
if (!normalFormRef.value) return;
|
if (!normalFormRef.value) return;
|
||||||
await normalFormRef.value.validate(async (valid) => {
|
await normalFormRef.value.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
const data = [
|
const data = {
|
||||||
{ label: "sitename", value: sitename.value },
|
tid: authStore.user.tid,
|
||||||
{ label: "logo", value: logo.value },
|
sitename: sitename.value,
|
||||||
{ label: "logow", value: logow.value },
|
companyintroduction: companyintroduction.value,
|
||||||
{ label: "description", value: description.value },
|
logo: logo.value,
|
||||||
{ label: "companyname", value: companyname.value },
|
logow: logow.value,
|
||||||
{ label: "copyright", value: copyright.value },
|
ico: ico.value,
|
||||||
{ label: "icp", value: icp.value },
|
description: description.value,
|
||||||
];
|
companyname: companyname.value,
|
||||||
|
copyright: copyright.value,
|
||||||
|
icp: icp.value,
|
||||||
|
};
|
||||||
const res = await saveNormalInfos(data);
|
const res = await saveNormalInfos(data);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success("保存成功");
|
ElMessage.success("保存成功");
|
||||||
@@ -172,11 +210,27 @@ const handleSaveNormalInfos = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleIconChange = (file: UploadFile) => {
|
||||||
|
const uploadFormData = new FormData();
|
||||||
|
uploadFormData.append("file", file.raw);
|
||||||
|
uploadFormData.append("cate", "site");
|
||||||
|
|
||||||
|
uploadFile(uploadFormData).then((uploadRes) => {
|
||||||
|
if ((uploadRes.code === 200 || uploadRes.code === 201) && uploadRes.data?.url) {
|
||||||
|
ico.value = uploadRes.data.url.replace(/\\/g, "/");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(uploadRes.msg || "上传失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const resetnormalForm = () => {
|
const resetnormalForm = () => {
|
||||||
sitename.value = "";
|
sitename.value = "";
|
||||||
companyname.value = "";
|
companyname.value = "";
|
||||||
|
companyintroduction.value = "";
|
||||||
logo.value = "";
|
logo.value = "";
|
||||||
logow.value = "";
|
logow.value = "";
|
||||||
|
ico.value = "";
|
||||||
description.value = "";
|
description.value = "";
|
||||||
copyright.value = "";
|
copyright.value = "";
|
||||||
icp.value = "";
|
icp.value = "";
|
||||||
@@ -185,8 +239,10 @@ const resetnormalForm = () => {
|
|||||||
defineExpose({
|
defineExpose({
|
||||||
sitename,
|
sitename,
|
||||||
companyname,
|
companyname,
|
||||||
|
companyintroduction,
|
||||||
logo,
|
logo,
|
||||||
logow,
|
logow,
|
||||||
|
ico,
|
||||||
description,
|
description,
|
||||||
copyright,
|
copyright,
|
||||||
icp,
|
icp,
|
||||||
@@ -199,10 +255,17 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.darkbg{
|
.darkbg {
|
||||||
background-color: #efefef;
|
background-color: #efefef;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-tip{
|
||||||
|
margin-left: 20px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.logo-uploader {
|
.logo-uploader {
|
||||||
:deep(.el-upload) {
|
:deep(.el-upload) {
|
||||||
border: 1px dashed #d9d9d9;
|
border: 1px dashed #d9d9d9;
|
||||||
|
|||||||
@@ -7,10 +7,7 @@
|
|||||||
style="max-width: 600px"
|
style="max-width: 600px"
|
||||||
>
|
>
|
||||||
<el-form-item label="SEO标题" prop="seoTitle">
|
<el-form-item label="SEO标题" prop="seoTitle">
|
||||||
<el-input
|
<el-input v-model="seoForm.seoTitle" placeholder="请输入SEO标题" />
|
||||||
v-model="seoForm.seoTitle"
|
|
||||||
placeholder="请输入SEO标题"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="SEO关键词" prop="seoKeywords">
|
<el-form-item label="SEO关键词" prop="seoKeywords">
|
||||||
<el-input
|
<el-input
|
||||||
@@ -34,39 +31,71 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage, ElLoading } from "element-plus";
|
||||||
import type { FormInstance, FormRules } from "element-plus";
|
import type { FormInstance, FormRules } from "element-plus";
|
||||||
|
import { getCompanySeo, saveCompanySeo } from "@/api/sitesettings";
|
||||||
|
|
||||||
const seoFormRef = ref<FormInstance>();
|
const seoFormRef = ref<FormInstance>();
|
||||||
|
|
||||||
const seoForm = reactive({
|
const seoForm = reactive({
|
||||||
seoTitle: "",
|
seoTitle: "",
|
||||||
seoKeywords: "",
|
seoKeywords: "",
|
||||||
seoDescription: ""
|
seoDescription: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
const seoRules: FormRules = {
|
const seoRules: FormRules = {
|
||||||
seoTitle: [{ required: true, message: "请输入SEO标题", trigger: "blur" }]
|
seoTitle: [{ required: true, message: "请输入SEO标题", trigger: "blur" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchSeoData = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getCompanySeo();
|
||||||
|
if (res.code === 200) {
|
||||||
|
Object.assign(seoForm, res.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("获取SEO数据失败");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveSeoSettings = async () => {
|
const saveSeoSettings = async () => {
|
||||||
if (!seoFormRef.value) return;
|
if (!seoFormRef.value) return;
|
||||||
await seoFormRef.value.validate((valid) => {
|
|
||||||
|
await seoFormRef.value.validate(async (valid) => {
|
||||||
if (valid) {
|
if (valid) {
|
||||||
// TODO: 保存SEO设置
|
const loading = ElLoading.service({ text: "正在保存..." });
|
||||||
|
try {
|
||||||
|
const res = await saveCompanySeo({
|
||||||
|
seoTitle: seoForm.seoTitle,
|
||||||
|
seoKeywords: seoForm.seoKeywords,
|
||||||
|
seoDescription: seoForm.seoDescription,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
ElMessage.success("SEO设置保存成功");
|
ElMessage.success("SEO设置保存成功");
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error("网络请求失败");
|
||||||
|
} finally {
|
||||||
|
loading.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetSeoForm = () => {
|
const resetSeoForm = () => {
|
||||||
seoForm.seoTitle = "";
|
if (!seoFormRef.value) return;
|
||||||
seoForm.seoKeywords = "";
|
seoFormRef.value.resetFields();
|
||||||
seoForm.seoDescription = "";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchSeoData();
|
||||||
|
});
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
seoForm
|
seoForm,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,7 +9,10 @@
|
|||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
<el-tabs v-model="activeTab" class="settings-tabs">
|
<el-tabs v-model="activeTab" class="settings-tabs">
|
||||||
<el-tab-pane label="基本信息" name="basic">
|
<el-tab-pane label="基本信息" name="basic">
|
||||||
<normalSettings ref="normalSettingsRef" v-if="activeTab === 'basic'" />
|
<normalSettings
|
||||||
|
ref="normalSettingsRef"
|
||||||
|
v-if="activeTab === 'basic'"
|
||||||
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="SEO设置" name="seo">
|
<el-tab-pane label="SEO设置" name="seo">
|
||||||
@@ -17,15 +20,24 @@
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="联系方式" name="contact">
|
<el-tab-pane label="联系方式" name="contact">
|
||||||
<contactSettings ref="contactSettingsRef" v-if="activeTab === 'contact'" />
|
<contactSettings
|
||||||
|
ref="contactSettingsRef"
|
||||||
|
v-if="activeTab === 'contact'"
|
||||||
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="登录验证" name="loginVerification">
|
<el-tab-pane label="登录验证" v-if="groupId === 1" name="loginVerification">
|
||||||
<loginVerificationSettings ref="loginVerificationSettingsRef" v-if="activeTab === 'loginVerification'" />
|
<loginVerificationSettings
|
||||||
|
ref="loginVerificationSettingsRef"
|
||||||
|
v-if="activeTab === 'loginVerification'"
|
||||||
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="法律声明&隐私条款" name="legalNotice">
|
<el-tab-pane label="法律声明&隐私条款" name="legalNotice">
|
||||||
<legalNoticeSettings ref="legalNoticeSettingsRef" v-if="activeTab === 'legalNotice'" />
|
<legalNoticeSettings
|
||||||
|
ref="legalNoticeSettingsRef"
|
||||||
|
v-if="activeTab === 'legalNotice'"
|
||||||
|
/>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<el-tab-pane label="其他设置" name="other">
|
<el-tab-pane label="其他设置" name="other">
|
||||||
@@ -38,49 +50,17 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { ElMessage } from "element-plus";
|
|
||||||
import normalSettings from "./components/normalSettings.vue";
|
import normalSettings from "./components/normalSettings.vue";
|
||||||
import seoSettings from "./components/seoSettings.vue";
|
import seoSettings from "./components/seoSettings.vue";
|
||||||
import contactSettings from "./components/contactSettings.vue";
|
import contactSettings from "./components/contactSettings.vue";
|
||||||
import otherSettings from "./components/otherSettings.vue";
|
import otherSettings from "./components/otherSettings.vue";
|
||||||
import legalNoticeSettings from "./components/legalNotice.vue";
|
import legalNoticeSettings from "./components/legalNotice.vue";
|
||||||
import loginVerificationSettings from "./components/loginVerification.vue";
|
import loginVerificationSettings from "./components/loginVerification.vue";
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const groupId = (authStore.userInfo as any)?.group_id;
|
||||||
const activeTab = ref("basic");
|
const activeTab = ref("basic");
|
||||||
|
|
||||||
const normalSettingsRef = ref();
|
|
||||||
const seoSettingsRef = ref();
|
|
||||||
const contactSettingsRef = ref();
|
|
||||||
const otherSettingsRef = ref();
|
|
||||||
const legalNoticeSettingsRef = ref();
|
|
||||||
const loginVerificationSettingsRef = ref();
|
|
||||||
|
|
||||||
// 初始化各标签页数据
|
|
||||||
const initSettings = async () => {
|
|
||||||
// TODO: 从后端获取各设置数据并赋值给对应组件
|
|
||||||
if (normalSettingsRef.value) {
|
|
||||||
// normalSettingsRef.value.normalinfos = ...
|
|
||||||
}
|
|
||||||
if (seoSettingsRef.value) {
|
|
||||||
// seoSettingsRef.value.seoForm = ...
|
|
||||||
}
|
|
||||||
if (contactSettingsRef.value) {
|
|
||||||
// contactSettingsRef.value.contactForm = ...
|
|
||||||
}
|
|
||||||
if (otherSettingsRef.value) {
|
|
||||||
// otherSettingsRef.value.otherForm = ...
|
|
||||||
}
|
|
||||||
if (legalNoticeSettingsRef.value) {
|
|
||||||
// legalNoticeSettingsRef.value.legalNoticeForm = ...
|
|
||||||
}
|
|
||||||
if (loginVerificationSettingsRef.value) {
|
|
||||||
// loginVerificationSettingsRef.value.loginVerificationForm = ...
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
initSettings();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tenant-users-tab">
|
||||||
|
<div class="section-header">
|
||||||
|
<div class="section-title">用户列表</div>
|
||||||
|
<el-button type="primary" size="small" :disabled="!tid" @click="handleAddUser">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
添加用户
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-form :inline="true" class="user-search-form" @submit.prevent>
|
||||||
|
<el-form-item label="关键词">
|
||||||
|
<el-input
|
||||||
|
v-model="userSearchKeyword"
|
||||||
|
clearable
|
||||||
|
placeholder="姓名 / 手机 / 邮箱 / 账号"
|
||||||
|
style="width: 260px"
|
||||||
|
:disabled="!tid"
|
||||||
|
@keyup.enter="handleSearchUsers"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" :disabled="!tid" @click="handleSearchUsers">查询</el-button>
|
||||||
|
<el-button :disabled="!tid" @click="resetUserSearch">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table :data="tenantUsers" v-loading="usersLoading" border style="width: 100%">
|
||||||
|
<el-table-column prop="account" label="用户名" min-width="140" align="center" />
|
||||||
|
<el-table-column prop="name" label="姓名" min-width="120" align="center" />
|
||||||
|
<el-table-column prop="phone" label="手机号" min-width="140" align="center" />
|
||||||
|
<el-table-column prop="email" label="邮箱" min-width="180" align="center" />
|
||||||
|
<el-table-column prop="status" label="状态" width="90" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="Number(row.status) === 1 ? 'success' : 'danger'">
|
||||||
|
{{ Number(row.status) === 1 ? "启用" : "禁用" }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="140" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button text type="primary" @click="openPasswordDialog(row)">
|
||||||
|
修改密码
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<AddUser ref="addUserRef" @success="refreshTenantUsers" />
|
||||||
|
|
||||||
|
<el-dialog v-model="passwordDialogVisible" title="修改密码" width="420px" destroy-on-close>
|
||||||
|
<el-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules" label-width="90px">
|
||||||
|
<el-form-item label="新密码" prop="password">
|
||||||
|
<el-input v-model="passwordForm.password" type="password" show-password placeholder="请输入新密码" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="password2">
|
||||||
|
<el-input v-model="passwordForm.password2" type="password" show-password placeholder="请再次输入新密码" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="passwordDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="passwordSubmitting" @click="submitPassword">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, watch } from "vue";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { Plus } from "@element-plus/icons-vue";
|
||||||
|
import { getAllUsers, changePassword } from "@/api/user";
|
||||||
|
import AddUser from "./adduser.vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
/** 当前租户 ID,为空时不请求 */
|
||||||
|
tid: number | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tenantUsers = ref<any[]>([]);
|
||||||
|
const usersLoading = ref(false);
|
||||||
|
const userSearchKeyword = ref("");
|
||||||
|
const addUserRef = ref<{ open: (tenantId: number) => void } | null>(null);
|
||||||
|
|
||||||
|
const passwordDialogVisible = ref(false);
|
||||||
|
const passwordSubmitting = ref(false);
|
||||||
|
const currentTenantUserId = ref<number | null>(null);
|
||||||
|
const passwordFormRef = ref();
|
||||||
|
const passwordForm = reactive({
|
||||||
|
password: "",
|
||||||
|
password2: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const passwordRules = {
|
||||||
|
password: [
|
||||||
|
{ required: true, message: "请输入新密码", trigger: "blur" },
|
||||||
|
{ min: 5, message: "密码至少 5 位", trigger: "blur" },
|
||||||
|
],
|
||||||
|
password2: [
|
||||||
|
{ required: true, message: "请再次输入新密码", trigger: "blur" },
|
||||||
|
{
|
||||||
|
validator: (_rule: any, value: string, callback: any) => {
|
||||||
|
if (!value) return callback(new Error("请再次输入新密码"));
|
||||||
|
if (value !== passwordForm.password) return callback(new Error("两次密码不一致"));
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildTenantUserQuery = (tid: number) => {
|
||||||
|
const params: Record<string, string | number> = { tid };
|
||||||
|
const kw = userSearchKeyword.value.trim();
|
||||||
|
if (kw) params.keyword = kw;
|
||||||
|
return params;
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshTenantUsers = async () => {
|
||||||
|
const id = props.tid;
|
||||||
|
if (id == null) return;
|
||||||
|
usersLoading.value = true;
|
||||||
|
try {
|
||||||
|
const usersRes = await getAllUsers(buildTenantUserQuery(id));
|
||||||
|
if (usersRes?.code === 200) {
|
||||||
|
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
|
||||||
|
} else {
|
||||||
|
tenantUsers.value = [];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
tenantUsers.value = [];
|
||||||
|
} finally {
|
||||||
|
usersLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadUsersForTid = async (tid: number) => {
|
||||||
|
usersLoading.value = true;
|
||||||
|
try {
|
||||||
|
const usersRes = await getAllUsers({ tid });
|
||||||
|
if (usersRes?.code === 200) {
|
||||||
|
tenantUsers.value = usersRes?.data?.list || usersRes?.data || [];
|
||||||
|
} else {
|
||||||
|
tenantUsers.value = [];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
tenantUsers.value = [];
|
||||||
|
} finally {
|
||||||
|
usersLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.tid,
|
||||||
|
(id) => {
|
||||||
|
userSearchKeyword.value = "";
|
||||||
|
if (id == null) {
|
||||||
|
tenantUsers.value = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadUsersForTid(id);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSearchUsers = () => {
|
||||||
|
refreshTenantUsers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetUserSearch = () => {
|
||||||
|
userSearchKeyword.value = "";
|
||||||
|
refreshTenantUsers();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddUser = () => {
|
||||||
|
if (props.tid != null) {
|
||||||
|
addUserRef.value?.open(props.tid);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openPasswordDialog = (row: any) => {
|
||||||
|
currentTenantUserId.value = Number(row?.id || 0) || null;
|
||||||
|
passwordForm.password = "";
|
||||||
|
passwordForm.password2 = "";
|
||||||
|
passwordDialogVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitPassword = async () => {
|
||||||
|
if (!passwordFormRef.value || !currentTenantUserId.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await passwordFormRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
passwordSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
const res = await changePassword(currentTenantUserId.value, { newPassword: passwordForm.password });
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("密码修改成功");
|
||||||
|
passwordDialogVisible.value = false;
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "密码修改失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
passwordSubmitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refreshTenantUsers,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tenant-users-tab {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search-form {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: var(--el-fill-color-light);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-search-form :deep(.el-form-item) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog v-model="visible" title="添加用户" width="600px" @closed="handleClosed"
|
||||||
|
destroy-on-close>
|
||||||
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px" v-loading="loading"
|
||||||
|
style="padding: 20px">
|
||||||
|
<el-form-item label="用户名" prop="account">
|
||||||
|
<el-input v-model="formData.account" placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input v-model="formData.password" type="password" placeholder="请输入密码" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认密码" prop="password2">
|
||||||
|
<el-input v-model="formData.password2" type="password" placeholder="请再次输入密码" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="姓名" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入姓名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="手机号" prop="phone">
|
||||||
|
<el-input v-model="formData.phone" placeholder="请输入手机号" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||||
|
</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>
|
||||||
|
<el-button @click="visible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitForm">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 租户详情内「添加用户」:提交到 /platform/tenantUser/create,
|
||||||
|
* 后端写入 yz_tenant_user(租户-用户绑定,含冗余账号/密码等字段)。
|
||||||
|
*/
|
||||||
|
import { ref, reactive } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { addUser } from '@/api/user';
|
||||||
|
|
||||||
|
const emit = defineEmits(['success']);
|
||||||
|
const visible = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
const currentTenantId = ref<number | null>(null);
|
||||||
|
|
||||||
|
const formData = reactive({
|
||||||
|
account: '',
|
||||||
|
password: '',
|
||||||
|
password2: '',
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||||
|
password: [{ required: true, message: '请输入密码', trigger: 'blur', min: 5 }],
|
||||||
|
password2: [
|
||||||
|
{ required: true, message: '请再次输入密码', trigger: 'blur' },
|
||||||
|
{
|
||||||
|
validator: (_rule: any, value: string, callback: any) => {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请再次输入密码'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (value !== formData.password) {
|
||||||
|
callback(new Error('两次输入的密码不一致'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
phone: [
|
||||||
|
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = (tenantId: number) => {
|
||||||
|
visible.value = true;
|
||||||
|
currentTenantId.value = tenantId;
|
||||||
|
Object.assign(formData, {
|
||||||
|
account: '',
|
||||||
|
password: '',
|
||||||
|
password2: '',
|
||||||
|
name: '',
|
||||||
|
phone: '',
|
||||||
|
email: '',
|
||||||
|
status: 1
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value.validate();
|
||||||
|
} catch {
|
||||||
|
// 校验失败时 validate 会 reject,避免 Uncaught (in promise)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const submitData = {
|
||||||
|
...formData,
|
||||||
|
tid: currentTenantId.value,
|
||||||
|
};
|
||||||
|
// 提交时不需要把确认密码发给后端
|
||||||
|
delete (submitData as any).password2;
|
||||||
|
const res = await addUser(submitData);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('添加成功');
|
||||||
|
visible.value = false;
|
||||||
|
emit('success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('提交失败', error);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClosed = () => {
|
||||||
|
formRef.value?.resetFields();
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open });
|
||||||
|
</script>
|
||||||
@@ -2,11 +2,16 @@
|
|||||||
<el-drawer
|
<el-drawer
|
||||||
v-model="visible"
|
v-model="visible"
|
||||||
title="租户详细信息"
|
title="租户详细信息"
|
||||||
size="600px"
|
size="1200px"
|
||||||
@closed="handleClosed"
|
@closed="handleClosed"
|
||||||
>
|
>
|
||||||
<div v-loading="loading" class="detail-container">
|
<div v-loading="loading" class="detail-container">
|
||||||
<el-descriptions :column="1" border>
|
<el-descriptions
|
||||||
|
:column="2"
|
||||||
|
:label-width="110"
|
||||||
|
border
|
||||||
|
class="tenant-detail-descriptions"
|
||||||
|
>
|
||||||
<el-descriptions-item label="租户名称">
|
<el-descriptions-item label="租户名称">
|
||||||
<span class="detail-value">{{ detailData.tenant_name }}</span>
|
<span class="detail-value">{{ detailData.tenant_name }}</span>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
@@ -22,73 +27,134 @@
|
|||||||
<el-descriptions-item label="电子邮箱">
|
<el-descriptions-item label="电子邮箱">
|
||||||
{{ detailData.contact_email || '-' }}
|
{{ detailData.contact_email || '-' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="租户地址" :span="2">
|
||||||
|
{{ detailData.address || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="租户状态">
|
<el-descriptions-item label="租户状态">
|
||||||
<el-tag :type="detailData.status === 1 ? 'success' : 'danger'">
|
<el-tag :type="detailData.status === 1 ? 'success' : 'danger'">
|
||||||
{{ detailData.status === 1 ? '启用' : '禁用' }}
|
{{ detailData.status === 1 ? '启用' : '禁用' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="租户地址">
|
|
||||||
{{ detailData.address || '-' }}
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="创建时间">
|
<el-descriptions-item label="创建时间">
|
||||||
{{ detailData.create_time || '-' }}
|
{{ detailData.create_time || '-' }}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<div class="footer-actions">
|
<el-divider />
|
||||||
<el-button @click="visible = false">关闭</el-button>
|
|
||||||
|
<div class="tenant-detail-body">
|
||||||
|
<el-tabs
|
||||||
|
v-model="activeTab"
|
||||||
|
tab-position="left"
|
||||||
|
class="tenant-detail-tabs"
|
||||||
|
>
|
||||||
|
<el-tab-pane label="租户用户" name="users">
|
||||||
|
<div class="tab-pane-inner">
|
||||||
|
<TenantUsersTab :tid="detailTenantId" />
|
||||||
</div>
|
</div>
|
||||||
|
</el-tab-pane>
|
||||||
|
<!-- 后续功能:在此继续增加 <el-tab-pane label="..." name="...">...</el-tab-pane> -->
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="footer-actions">
|
||||||
|
<el-button @click="visible = false">关闭</el-button>
|
||||||
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import { getTenantDetail } from '@/api/tenant';
|
import { getTenantDetail } from "@/api/tenant";
|
||||||
|
import TenantUsersTab from "./TenantUsersTab.vue";
|
||||||
|
|
||||||
const visible = ref(false);
|
const visible = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const detailData = ref<any>({});
|
const detailData = ref<any>({});
|
||||||
|
const detailTenantId = ref<number | null>(null);
|
||||||
|
const activeTab = ref("users");
|
||||||
|
|
||||||
// 暴露给父组件的方法
|
const open = async (id: number) => {
|
||||||
const open = async (id: number) => {
|
detailTenantId.value = id;
|
||||||
|
activeTab.value = "users";
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getTenantDetail(id);
|
const detailRes = await getTenantDetail(id);
|
||||||
if (res.code === 200) {
|
if (detailRes.code === 200) {
|
||||||
detailData.value = res.data;
|
detailData.value = detailRes.data;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取详情失败', error);
|
console.error("获取详情失败", error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClosed = () => {
|
const handleClosed = () => {
|
||||||
detailData.value = {};
|
detailData.value = {};
|
||||||
};
|
detailTenantId.value = null;
|
||||||
|
activeTab.value = "users";
|
||||||
|
};
|
||||||
|
|
||||||
// 必须暴露给外部
|
defineExpose({ open });
|
||||||
defineExpose({ open });
|
</script>
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.detail-container {
|
.detail-container {
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
}
|
}
|
||||||
.detail-value {
|
.detail-value {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
.footer-actions {
|
.footer-actions {
|
||||||
margin-top: 30px;
|
margin-top: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
:deep(.el-descriptions__label) {
|
|
||||||
|
.tenant-detail-body {
|
||||||
|
margin-top: 20px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-detail-tabs {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-detail-tabs :deep(.el-tabs__header) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-detail-tabs :deep(.el-tabs__nav-wrap.is-left) {
|
||||||
|
min-width: 112px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-detail-tabs :deep(.el-tabs__item) {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 44px;
|
||||||
|
line-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tenant-detail-tabs :deep(.el-tabs__content) {
|
||||||
|
padding: 0 0 0 16px;
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-pane-inner {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
width: 120px;
|
width: 120px;
|
||||||
background-color: #f5f7fa;
|
background-color: #f5f7fa;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.tenant-detail-descriptions :deep(.el-descriptions__table) {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
destroy-on-close>
|
destroy-on-close>
|
||||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px" v-loading="loading"
|
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px" v-loading="loading"
|
||||||
style="padding: 20px">
|
style="padding: 20px">
|
||||||
<el-form-item label="租户名称" prop="tenant_name">
|
|
||||||
<el-input v-model="formData.tenant_name" placeholder="请输入租户名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="租户编码" prop="tenant_code">
|
<el-form-item label="租户编码" prop="tenant_code">
|
||||||
<el-input v-model="formData.tenant_code" placeholder="系统自动生成" disabled />
|
<el-input v-model="formData.tenant_code" placeholder="系统自动生成" disabled />
|
||||||
<div class="form-tip" v-if="!formData.id" style="font-size: 12px; color: #999;">
|
<div class="form-tip" v-if="!formData.id" style="font-size: 12px; color: #999;">
|
||||||
* 编码由系统随机分配,提交时将自动校验唯一性
|
* 编码由系统随机分配,提交时将自动校验唯一性
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="租户名称" prop="tenant_name">
|
||||||
|
<el-input v-model="formData.tenant_name" placeholder="请输入租户名称" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="联系人" prop="contact_person">
|
<el-form-item label="联系人" prop="contact_person">
|
||||||
<el-input v-model="formData.contact_person" placeholder="请输入联系人" />
|
<el-input v-model="formData.contact_person" placeholder="请输入联系人" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<el-form-item label="资质图片" prop="file_url">
|
<el-form-item label="资质图片" prop="file_url">
|
||||||
<el-upload
|
<el-upload
|
||||||
class="avatar-uploader"
|
class="avatar-uploader"
|
||||||
action="/api/admin/common/upload"
|
action="/api/platform/common/upload"
|
||||||
:show-file-list="false"
|
:show-file-list="false"
|
||||||
:on-success="handleUploadSuccess"
|
:on-success="handleUploadSuccess"
|
||||||
:before-upload="beforeAvatarUpload"
|
:before-upload="beforeAvatarUpload"
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
const formRef = ref();
|
const formRef = ref();
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
tenant_id: null,
|
tid: null,
|
||||||
type: '',
|
type: '',
|
||||||
file_url: '',
|
file_url: '',
|
||||||
expire_time: '',
|
expire_time: '',
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
const open = (row: any) => {
|
const open = (row: any) => {
|
||||||
visible.value = true;
|
visible.value = true;
|
||||||
tenantName.value = row.tenant_name;
|
tenantName.value = row.tenant_name;
|
||||||
formData.tenant_id = row.id;
|
formData.tid = row.id;
|
||||||
|
|
||||||
// 如果后端有资质详情接口,可以在此获取回显数据
|
// 如果后端有资质详情接口,可以在此获取回显数据
|
||||||
// fetchDetail(row.id);
|
// fetchDetail(row.id);
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container-box">
|
||||||
|
<div class="header-bar">
|
||||||
|
<h2>我的域名</h2>
|
||||||
|
<el-button type="primary" @click="dialogVisible = true">
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
申请二级域名
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<!-- 我的域名列表 -->
|
||||||
|
<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="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">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-link
|
||||||
|
type="primary"
|
||||||
|
:href="'http://' + scope.row.full_domain"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{{ scope.row.full_domain }}
|
||||||
|
</el-link>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<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="180" fixed="right" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
type="primary"
|
||||||
|
@click="handleCopy(scope.row.full_domain)"
|
||||||
|
>
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
text
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 申请域名弹窗 -->
|
||||||
|
<el-dialog v-model="dialogVisible" title="申请二级域名" width="500px">
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="选择主域名" prop="main_domain">
|
||||||
|
<el-select
|
||||||
|
v-model="form.main_domain"
|
||||||
|
placeholder="请选择主域名"
|
||||||
|
style="width: 100%"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="item in domainList"
|
||||||
|
:key="item.main_domain"
|
||||||
|
:label="item.main_domain"
|
||||||
|
:value="item.main_domain"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="二级前缀" prop="sub_domain">
|
||||||
|
<el-input v-model="form.sub_domain" placeholder="请输入二级域名前缀">
|
||||||
|
<template #append>{{
|
||||||
|
form.main_domain ? "." + form.main_domain : ""
|
||||||
|
}}</template>
|
||||||
|
</el-input>
|
||||||
|
<div class="form-tip">
|
||||||
|
只能包含字母、数字和连字符,不能以连字符开头或结尾
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="预览">
|
||||||
|
<div class="domain-preview">
|
||||||
|
{{
|
||||||
|
form.sub_domain
|
||||||
|
? form.sub_domain + "." + (form.main_domain || "example.com")
|
||||||
|
: "请填写上方信息"
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</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 } from "@element-plus/icons-vue";
|
||||||
|
import {
|
||||||
|
getMyDomains,
|
||||||
|
applyTenantDomain,
|
||||||
|
getEnabledDomains,
|
||||||
|
deleteTenantDomain,
|
||||||
|
} from "@/api/domain";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
|
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const loading = ref(false);
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
const dialogVisible = ref(false);
|
||||||
|
const formRef = ref();
|
||||||
|
|
||||||
|
const tableData = ref<any[]>([]);
|
||||||
|
const domainList = ref<any[]>([]);
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
main_domain: "",
|
||||||
|
sub_domain: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
main_domain: [{ required: true, message: "请选择主域名", trigger: "change" }],
|
||||||
|
sub_domain: [
|
||||||
|
{ required: true, message: "请输入二级域名前缀", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: /^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$/,
|
||||||
|
message: "格式不正确",
|
||||||
|
trigger: "blur",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchDomains = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
// 获取我的域名列表
|
||||||
|
const res = await getMyDomains({ tid: authStore.user.tid });
|
||||||
|
if (res.code === 200) {
|
||||||
|
tableData.value = res.data || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取可选主域名
|
||||||
|
const domainRes = await getEnabledDomains();
|
||||||
|
if (domainRes.code === 200) {
|
||||||
|
domainList.value = domainRes.data || [];
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await formRef.value.validate();
|
||||||
|
submitLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await applyTenantDomain({
|
||||||
|
tid: authStore.user.tid,
|
||||||
|
main_domain: form.main_domain,
|
||||||
|
sub_domain: form.sub_domain,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success(res.msg);
|
||||||
|
dialogVisible.value = false;
|
||||||
|
form.sub_domain = "";
|
||||||
|
fetchDomains();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "申请失败");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCopy = (domain: string) => {
|
||||||
|
navigator.clipboard.writeText("http://" + domain);
|
||||||
|
ElMessage.success("链接已复制到剪贴板");
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (row: any) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`确定要删除域名 "${row.full_domain}" 吗?`,
|
||||||
|
"提示",
|
||||||
|
{
|
||||||
|
confirmButtonText: "确定",
|
||||||
|
cancelButtonText: "取消",
|
||||||
|
type: "warning",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const res = await deleteTenantDomain(row.id);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("删除成功");
|
||||||
|
fetchDomains();
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || "删除失败");
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== "cancel") {
|
||||||
|
console.error("删除失败:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchDomains();
|
||||||
|
});
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-preview {
|
||||||
|
padding: 10px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -22,20 +22,36 @@
|
|||||||
|
|
||||||
<el-form :inline="true" :model="searchForm" class="search-form">
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
<el-form-item label="租户名称">
|
<el-form-item label="租户名称">
|
||||||
<el-input v-model="searchForm.tenant_name" placeholder="请输入租户名称" clearable
|
<el-input
|
||||||
@keyup.enter="handleSearch" />
|
v-model="searchForm.tenant_name"
|
||||||
|
placeholder="请输入租户名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="租户编码">
|
<el-form-item label="租户编码">
|
||||||
<el-input v-model="searchForm.tenant_code" placeholder="请输入租户编码" clearable
|
<el-input
|
||||||
@keyup.enter="handleSearch" />
|
v-model="searchForm.tenant_code"
|
||||||
|
placeholder="请输入租户编码"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="联系人">
|
<el-form-item label="联系人">
|
||||||
<el-input v-model="searchForm.contact_person" placeholder="请输入联系人" clearable
|
<el-input
|
||||||
@keyup.enter="handleSearch" />
|
v-model="searchForm.contact_person"
|
||||||
|
placeholder="请输入联系人"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="联系电话">
|
<el-form-item label="联系电话">
|
||||||
<el-input v-model="searchForm.contact_phone" placeholder="请输入联系电话" clearable
|
<el-input
|
||||||
@keyup.enter="handleSearch" />
|
v-model="searchForm.contact_phone"
|
||||||
|
placeholder="请输入联系电话"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="handleSearch">查询</el-button>
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
@@ -46,12 +62,48 @@
|
|||||||
<!-- 租户列表表格 -->
|
<!-- 租户列表表格 -->
|
||||||
<el-table :data="tenants" style="width: 100%" v-loading="loading">
|
<el-table :data="tenants" style="width: 100%" v-loading="loading">
|
||||||
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
<el-table-column prop="id" label="ID" align="center" fixed="left" />
|
||||||
<el-table-column prop="tenant_name" label="租户名称" min-width="220" align="center" />
|
<el-table-column
|
||||||
<el-table-column prop="tenant_code" label="租户编码" min-width="120" align="center" />
|
prop="tenant_name"
|
||||||
<el-table-column prop="contact_person" label="联系人" min-width="120" align="center" />
|
label="租户名称"
|
||||||
<el-table-column prop="contact_phone" label="联系电话" min-width="120" align="center" />
|
min-width="220"
|
||||||
<el-table-column prop="contact_email" label="电子邮箱" min-width="180" align="center" />
|
align="center"
|
||||||
<el-table-column prop="address" label="租户地址" min-width="300" align="center" />
|
>
|
||||||
|
<template #default="scope">
|
||||||
|
<span class="name-link" @click="handlePreview(scope.row)">
|
||||||
|
{{ scope.row.tenant_name }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
prop="tenant_code"
|
||||||
|
label="租户编码"
|
||||||
|
min-width="120"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="contact_person"
|
||||||
|
label="联系人"
|
||||||
|
min-width="120"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="contact_phone"
|
||||||
|
label="联系电话"
|
||||||
|
min-width="120"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="contact_email"
|
||||||
|
label="电子邮箱"
|
||||||
|
min-width="180"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
<el-table-column
|
||||||
|
prop="address"
|
||||||
|
label="租户地址"
|
||||||
|
min-width="300"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
<el-table-column prop="status" label="租户状态" width="80" align="center">
|
<el-table-column prop="status" label="租户状态" width="80" align="center">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">{{
|
||||||
@@ -59,12 +111,25 @@
|
|||||||
}}</el-tag>
|
}}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="280" align="center" fixed="right">
|
<el-table-column label="操作" width="160" align="center" fixed="right">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<!-- <el-button size="small" @click="handleQualification(scope.row)">资质</el-button> -->
|
<div class="action-icons">
|
||||||
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
|
<el-tooltip content="编辑" placement="top">
|
||||||
<el-button size="small" @click="editRef.open(scope.row.id)">编辑</el-button>
|
<el-button text size="small" @click="editRef.open(scope.row.id)">
|
||||||
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
<el-icon><Edit /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip content="删除" placement="top">
|
||||||
|
<el-button
|
||||||
|
text
|
||||||
|
size="small"
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row)"
|
||||||
|
>
|
||||||
|
<el-icon><Delete /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -75,19 +140,26 @@
|
|||||||
|
|
||||||
<!-- 分页 -->
|
<!-- 分页 -->
|
||||||
<div class="pagination-bar">
|
<div class="pagination-bar">
|
||||||
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
|
<el-pagination
|
||||||
layout="total, prev, pager, next" />
|
:current-page="page"
|
||||||
|
:page-size="pageSize"
|
||||||
|
:total="total"
|
||||||
|
@current-change="handlePageChange"
|
||||||
|
layout="total, prev, pager, next"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getTenantList, deleteTenant } from '@/api/tenant';
|
import { getTenantList, deleteTenant } from "@/api/tenant";
|
||||||
import { onMounted, ref, reactive } from 'vue';
|
import { onMounted, ref, reactive } from "vue";
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from "vue-router";
|
||||||
import EditModal from './components/edit.vue';
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import DetailDrawer from './components/detail.vue';
|
import EditModal from "./components/edit.vue";
|
||||||
import Qualification from './components/qualification.vue';
|
import DetailDrawer from "./components/detail.vue";
|
||||||
|
import Qualification from "./components/qualification.vue";
|
||||||
|
import { Edit, Delete } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const total = ref(0);
|
const total = ref(0);
|
||||||
const page = ref(1);
|
const page = ref(1);
|
||||||
@@ -102,16 +174,18 @@ const qualificationRef = ref();
|
|||||||
|
|
||||||
// 删除
|
// 删除
|
||||||
const handleDelete = (row) => {
|
const handleDelete = (row) => {
|
||||||
ElMessageBox.confirm('确定要删除该租户吗?', '提示', {
|
ElMessageBox.confirm("确定要删除该租户吗?", "提示", {
|
||||||
type: 'warning'
|
type: "warning",
|
||||||
}).then(() => {
|
})
|
||||||
deleteTenant(row.id).then(res => {
|
.then(() => {
|
||||||
|
deleteTenant(row.id).then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
ElMessage.success('删除成功');
|
ElMessage.success("删除成功");
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}).catch(() => { });
|
})
|
||||||
|
.catch(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 查看
|
// 查看
|
||||||
@@ -132,10 +206,10 @@ const handlePageChange = (val: number) => {
|
|||||||
|
|
||||||
// 搜索表单数据
|
// 搜索表单数据
|
||||||
const searchForm = reactive({
|
const searchForm = reactive({
|
||||||
tenant_name: '',
|
tenant_name: "",
|
||||||
tenant_code: '',
|
tenant_code: "",
|
||||||
contact_person: '',
|
contact_person: "",
|
||||||
contact_phone: ''
|
contact_phone: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
// 执行查询(回到第一页再刷新)
|
// 执行查询(回到第一页再刷新)
|
||||||
@@ -146,10 +220,10 @@ const handleSearch = () => {
|
|||||||
|
|
||||||
// 重置查询
|
// 重置查询
|
||||||
const resetSearch = () => {
|
const resetSearch = () => {
|
||||||
searchForm.tenant_name = '';
|
searchForm.tenant_name = "";
|
||||||
searchForm.tenant_code = '';
|
searchForm.tenant_code = "";
|
||||||
searchForm.contact_person = '';
|
searchForm.contact_person = "";
|
||||||
searchForm.contact_phone = '';
|
searchForm.contact_phone = "";
|
||||||
handleSearch();
|
handleSearch();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,15 +235,17 @@ const refresh = () => {
|
|||||||
const queryParams = {
|
const queryParams = {
|
||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
...searchForm
|
...searchForm,
|
||||||
};
|
};
|
||||||
|
|
||||||
getTenantList(queryParams).then(res => {
|
getTenantList(queryParams)
|
||||||
|
.then((res) => {
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
tenants.value = res.data.list;
|
tenants.value = res.data.list;
|
||||||
total.value = res.data.total;
|
total.value = res.data.total;
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
})
|
||||||
|
.finally(() => {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -214,4 +290,10 @@ onMounted(() => {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-icons {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -20,6 +20,16 @@
|
|||||||
|
|
||||||
<el-divider></el-divider>
|
<el-divider></el-divider>
|
||||||
|
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="租户筛选">
|
||||||
|
<el-select v-model="selectedTenantId" style="width: 220px" disabled>
|
||||||
|
<el-option :label="`当前租户(ID: ${selectedTenantId || '-'})`" :value="selectedTenantId" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 用户列表表格 -->
|
<!-- 用户列表表格 -->
|
||||||
<el-table :data="users" style="width: 100%" v-loading="loading">
|
<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="id" label="ID" align="center" fixed="left" />
|
||||||
@@ -84,14 +94,16 @@
|
|||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { Plus, Refresh } from "@element-plus/icons-vue";
|
import { Plus, Refresh } from "@element-plus/icons-vue";
|
||||||
import { getAllUsers, deleteUser } from "@/api/user";
|
import { getTenantUsers, deleteUser } from "@/api/user";
|
||||||
import { getAllRoles } from "@/api/role";
|
import { getAllRoles } from "@/api/role";
|
||||||
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import UserEditDialog from "./components/userEdit.vue";
|
import UserEditDialog from "./components/userEdit.vue";
|
||||||
import ChangePasswordDialog from "./components/changePassword.vue";
|
import ChangePasswordDialog from "./components/changePassword.vue";
|
||||||
import PreviewDialog from "./components/preview.vue";
|
import PreviewDialog from "./components/preview.vue";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
id: number;
|
id: number;
|
||||||
|
tid?: number;
|
||||||
account: string;
|
account: string;
|
||||||
name: string;
|
name: string;
|
||||||
phone: string;
|
phone: string;
|
||||||
@@ -121,6 +133,8 @@ const total = ref(0);
|
|||||||
const users = ref<User[]>([]);
|
const users = ref<User[]>([]);
|
||||||
const roles = ref<Role[]>([]);
|
const roles = ref<Role[]>([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const selectedTenantId = ref<number>(Number(authStore.user.tid || 0));
|
||||||
|
|
||||||
// 组件引用
|
// 组件引用
|
||||||
const userEditRef = ref();
|
const userEditRef = ref();
|
||||||
@@ -174,10 +188,17 @@ function getRoleTagText(roles: Role[] | undefined, group_id: number): string {
|
|||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const res = await getAllUsers();
|
if (!selectedTenantId.value) {
|
||||||
users.value = res.data.list;
|
users.value = [];
|
||||||
|
total.value = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await getTenantUsers(selectedTenantId.value);
|
||||||
|
users.value = res?.data?.list || [];
|
||||||
|
total.value = Number(res?.data?.total || 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
users.value = [];
|
users.value = [];
|
||||||
|
total.value = 0;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -254,12 +275,17 @@ const handleDelete = async (user: User) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
selectedTenantId.value = Number(authStore.user.tid || 0);
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
|
.filter-bar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ import {
|
|||||||
Moon,
|
Moon,
|
||||||
} from "@element-plus/icons-vue";
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
import { getModulesList } from "@/api/modules";
|
import { getTenantList } from "@/api/modules";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useMenuStore } from "@/stores/menu";
|
import { useMenuStore } from "@/stores/menu";
|
||||||
|
|
||||||
@@ -271,7 +271,7 @@ async function handleLogout() {
|
|||||||
// 加载模块列表
|
// 加载模块列表
|
||||||
async function loadModules() {
|
async function loadModules() {
|
||||||
try {
|
try {
|
||||||
const res = await getModulesList();
|
const res = await getTenantList();
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const list = res.data?.list || [];
|
const list = res.data?.list || [];
|
||||||
const filteredList = list
|
const filteredList = list
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="card">
|
||||||
|
<h2>忘记密码</h2>
|
||||||
|
<p class="desc">通过租户、账号和手机号重置密码</p>
|
||||||
|
|
||||||
|
<input v-model="form.tenant_name" class="input" placeholder="租户名称" />
|
||||||
|
<input v-model="form.account" class="input" placeholder="账号" />
|
||||||
|
<input v-model="form.phone" class="input" placeholder="手机号" />
|
||||||
|
<div class="code-row">
|
||||||
|
<input v-model="form.sms_code" class="input code-input" placeholder="短信验证码" />
|
||||||
|
<button class="code-btn" :disabled="codeLoading || countdown > 0" @click="handleSendCode">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : (codeLoading ? "发送中..." : "发送验证码") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input v-model="form.new_password" class="input" type="password" placeholder="新密码" />
|
||||||
|
<input v-model="form.confirm_password" class="input" type="password" placeholder="确认新密码" />
|
||||||
|
|
||||||
|
<div v-if="errorMsg" class="error">{{ errorMsg }}</div>
|
||||||
|
<button class="btn" :disabled="loading" @click="handleSubmit">
|
||||||
|
{{ loading ? "提交中..." : "重置密码" }}
|
||||||
|
</button>
|
||||||
|
<a class="back" @click.prevent="router.push('/login')">返回登录</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { reactive, ref, onUnmounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { ElMessage } from "element-plus";
|
||||||
|
import { resetPassword, sendResetCode } from "@/api/login";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const loading = ref(false);
|
||||||
|
const codeLoading = ref(false);
|
||||||
|
const countdown = ref(0);
|
||||||
|
let timer = null;
|
||||||
|
const errorMsg = ref("");
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
tenant_name: "",
|
||||||
|
account: "",
|
||||||
|
phone: "",
|
||||||
|
sms_code: "",
|
||||||
|
new_password: "",
|
||||||
|
confirm_password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60;
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
timer = setInterval(() => {
|
||||||
|
countdown.value -= 1;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendCode = async () => {
|
||||||
|
if (codeLoading.value || countdown.value > 0) return;
|
||||||
|
if (!form.tenant_name || !form.account || !form.phone) {
|
||||||
|
errorMsg.value = "请先填写租户名称、账号和手机号";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
codeLoading.value = true;
|
||||||
|
errorMsg.value = "";
|
||||||
|
try {
|
||||||
|
const res = await sendResetCode({
|
||||||
|
tenant_name: form.tenant_name,
|
||||||
|
account: form.account,
|
||||||
|
phone: form.phone,
|
||||||
|
});
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("验证码已发送");
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res.msg || "验证码发送失败";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMsg.value = "验证码发送失败,请稍后重试";
|
||||||
|
} finally {
|
||||||
|
codeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
errorMsg.value = "";
|
||||||
|
if (loading.value) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await resetPassword(form);
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success("密码重置成功,请重新登录");
|
||||||
|
router.push("/login");
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res.msg || "重置失败";
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMsg.value = "重置失败,请稍后重试";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (timer) clearInterval(timer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: #f4f8ff; }
|
||||||
|
.card { width: 420px; background: #fff; border-radius: 12px; padding: 24px; box-shadow: 0 8px 24px rgba(0,0,0,.08); }
|
||||||
|
.desc { color: #6b7b93; margin-bottom: 12px; }
|
||||||
|
.input { width: 100%; margin-bottom: 10px; padding: 10px; border: 1px solid #dbe3ee; border-radius: 6px; box-sizing: border-box; }
|
||||||
|
.code-row { display: flex; gap: 8px; margin-bottom: 10px; }
|
||||||
|
.code-input { margin-bottom: 0; flex: 1; }
|
||||||
|
.code-btn { width: 120px; border: 1px solid #dbe3ee; border-radius: 6px; background: #fff; cursor: pointer; }
|
||||||
|
.code-btn:disabled { opacity: .6; cursor: not-allowed; }
|
||||||
|
.btn { width: 100%; margin-top: 8px; padding: 10px; border: none; border-radius: 6px; background: #3f8cff; color: #fff; cursor: pointer; }
|
||||||
|
.btn:disabled { opacity: .65; cursor: not-allowed; }
|
||||||
|
.error { color: #e34d4d; font-size: 13px; margin: 6px 0; }
|
||||||
|
.back { display: inline-block; margin-top: 10px; color: #3f8cff; cursor: pointer; }
|
||||||
|
</style>
|
||||||
|
|
||||||
+197
-13
@@ -18,7 +18,7 @@
|
|||||||
<div class="copyright">© 2026 Yunzer 管理系统</div>
|
<div class="copyright">© 2026 Yunzer 管理系统</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-panel">
|
<div class="login-panel">
|
||||||
<h2 class="login-title">欢迎登录</h2>
|
<h2 class="login-title">欢迎登录租户端</h2>
|
||||||
<div class="login-desc">请填写您的账号信息</div>
|
<div class="login-desc">请填写您的账号信息</div>
|
||||||
<div class="form-group icon-input-group">
|
<div class="form-group icon-input-group">
|
||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
@@ -31,7 +31,13 @@
|
|||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
<i class="fa-solid fa-user"></i>
|
<i class="fa-solid fa-user"></i>
|
||||||
</span>
|
</span>
|
||||||
<input v-model="account" type="text" placeholder="用户名" autocomplete="account" class="input input-with-icon" />
|
<input
|
||||||
|
v-model="account"
|
||||||
|
type="text"
|
||||||
|
:placeholder="verifyType === 'sms' ? '手机号' : verifyType === 'email' ? '邮箱' : '用户名'"
|
||||||
|
:autocomplete="verifyType === 'sms' ? 'tel' : 'account'"
|
||||||
|
class="input input-with-icon"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group icon-input-group">
|
<div class="form-group icon-input-group">
|
||||||
<span class="input-icon">
|
<span class="input-icon">
|
||||||
@@ -45,10 +51,20 @@
|
|||||||
<i v-else class="fa-solid fa-eye-slash"></i>
|
<i v-else class="fa-solid fa-eye-slash"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="openVerifyEnabled && verifyType === 'captcha'" class="form-group code-row">
|
||||||
|
<input v-model="captchaInput" type="text" placeholder="请输入4位验证码" class="input code-input" />
|
||||||
|
<button class="code-btn" type="button" @click="generateCaptcha">{{ captchaText }}</button>
|
||||||
|
</div>
|
||||||
|
<div v-if="openVerifyEnabled && (verifyType === 'sms' || verifyType === 'email')" class="form-group code-row">
|
||||||
|
<input v-model="smsCode" type="text" placeholder="短信验证码" class="input code-input" />
|
||||||
|
<button class="code-btn" :disabled="codeLoading || countdown > 0" @click="handleSendLoginCode">
|
||||||
|
{{ countdown > 0 ? `${countdown}s` : (codeLoading ? "发送中..." : "发送验证码") }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- 极验验证码容器 -->
|
<!-- 极验验证码容器 -->
|
||||||
<div style="display: none;" v-if="showCaptchaContainer" class="geetest-container" ref="captchaContainer"></div>
|
<div style="display: none;" v-if="openVerifyEnabled && verifyType === 'geetest' && showCaptchaContainer" class="geetest-container" ref="captchaContainer"></div>
|
||||||
|
|
||||||
<div class="remember-me-row">
|
<div class="remember-me-row">
|
||||||
<label class="remember-me-label">
|
<label class="remember-me-label">
|
||||||
@@ -76,10 +92,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, nextTick } from "vue";
|
import { ref, onMounted, nextTick, onUnmounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { login, getOpenVerify, getGeetest4Infos } from "@/api/login";
|
import { login, getOpenVerify, getGeetest4Infos, sendLoginCode } from "@/api/login";
|
||||||
import "@/assets/js/gt4.js";
|
import "@/assets/js/gt4.js";
|
||||||
import { ElMessageBox, ElMessage } from "element-plus";
|
import { ElMessageBox, ElMessage } from "element-plus";
|
||||||
|
|
||||||
@@ -88,11 +104,20 @@ const authStore = useAuthStore();
|
|||||||
|
|
||||||
// --- 表单数据 ---
|
// --- 表单数据 ---
|
||||||
const tenant_name = ref("");
|
const tenant_name = ref("");
|
||||||
|
// 页面加载阶段先不展示验证码,等 getOpenVerify 拉取真实配置后再决定渲染
|
||||||
|
const openVerifyEnabled = ref(false);
|
||||||
|
const verifyType = ref<"captcha" | "sms" | "geetest" | "email">("captcha");
|
||||||
const account = ref("");
|
const account = ref("");
|
||||||
const password = ref("");
|
const password = ref("");
|
||||||
|
const smsCode = ref("");
|
||||||
|
const captchaInput = ref("");
|
||||||
|
const captchaText = ref("");
|
||||||
const passwordVisible = ref(false);
|
const passwordVisible = ref(false);
|
||||||
const rememberMe = ref(false);
|
const rememberMe = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
const codeLoading = ref(false);
|
||||||
|
const countdown = ref(0);
|
||||||
|
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||||
const errorMsg = ref("");
|
const errorMsg = ref("");
|
||||||
|
|
||||||
// --- 极验相关变量 ---
|
// --- 极验相关变量 ---
|
||||||
@@ -100,6 +125,37 @@ const showCaptchaContainer = ref(false);
|
|||||||
const captchaContainer = ref<HTMLElement | null>(null);
|
const captchaContainer = ref<HTMLElement | null>(null);
|
||||||
const captchaInstance = ref<any>(null);
|
const captchaInstance = ref<any>(null);
|
||||||
|
|
||||||
|
// --- 初始化登录验证配置 ---
|
||||||
|
const initVerifyConfig = async () => {
|
||||||
|
try {
|
||||||
|
const verifyRes = await getOpenVerify();
|
||||||
|
let openVerify = "0";
|
||||||
|
let verifyTypeVal: any = "captcha";
|
||||||
|
|
||||||
|
if (verifyRes && verifyRes.code === 200 && Array.isArray(verifyRes.data)) {
|
||||||
|
verifyRes.data.forEach((item: any) => {
|
||||||
|
if (item.label === "openVerify") {
|
||||||
|
openVerify = item.value || "0";
|
||||||
|
}
|
||||||
|
if (item.label === "verifyType") {
|
||||||
|
verifyTypeVal = item.value || "captcha";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
openVerifyEnabled.value = openVerify === "1";
|
||||||
|
verifyType.value = verifyTypeVal as any;
|
||||||
|
|
||||||
|
// 仅 captcha 模式需要生成本地验证码文案
|
||||||
|
if (openVerifyEnabled.value && verifyType.value === "captcha") {
|
||||||
|
generateCaptcha();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 配置拉取失败时:默认关闭验证码模块,避免误展示
|
||||||
|
openVerifyEnabled.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- 加载JS脚本 ---
|
// --- 加载JS脚本 ---
|
||||||
const loadScript = (url: string): Promise<void> => {
|
const loadScript = (url: string): Promise<void> => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -127,10 +183,15 @@ const cleanCaptchaInstance = () => {
|
|||||||
|
|
||||||
// --- 执行登录请求 ---
|
// --- 执行登录请求 ---
|
||||||
const performLoginRequest = async () => {
|
const performLoginRequest = async () => {
|
||||||
|
// 根据不同验证方式,把正确的 code 传给后端
|
||||||
|
const codeToSend =
|
||||||
|
openVerifyEnabled.value && verifyType.value === "captcha" ? captchaInput.value : smsCode.value;
|
||||||
|
|
||||||
const res = await login({
|
const res = await login({
|
||||||
tenant_name: tenant_name.value,
|
tenant_name: tenant_name.value,
|
||||||
account: account.value,
|
account: account.value,
|
||||||
password: password.value
|
password: password.value,
|
||||||
|
code: codeToSend
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res && res.code === 200) {
|
if (res && res.code === 200) {
|
||||||
@@ -163,6 +224,53 @@ const performLoginRequest = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateCaptcha = () => {
|
||||||
|
captchaText.value = `${Math.floor(1000 + Math.random() * 9000)}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const startCountdown = () => {
|
||||||
|
countdown.value = 60;
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
countdownTimer = setInterval(() => {
|
||||||
|
countdown.value -= 1;
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
|
countdownTimer = null;
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendLoginCode = async () => {
|
||||||
|
if (codeLoading.value || countdown.value > 0) return;
|
||||||
|
if (!tenant_name.value.trim()) {
|
||||||
|
errorMsg.value = "请输入租户名称";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!account.value.trim()) {
|
||||||
|
errorMsg.value = "请输入用户名";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
codeLoading.value = true;
|
||||||
|
errorMsg.value = "";
|
||||||
|
try {
|
||||||
|
const res = await sendLoginCode({
|
||||||
|
tenant_name: tenant_name.value,
|
||||||
|
account: account.value,
|
||||||
|
channel: verifyType.value,
|
||||||
|
});
|
||||||
|
if (res && res.code === 200) {
|
||||||
|
ElMessage.success("验证码已发送");
|
||||||
|
startCountdown();
|
||||||
|
} else {
|
||||||
|
errorMsg.value = res?.msg || "发送验证码失败";
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
errorMsg.value = err?.response?.data?.msg || err?.message || "发送验证码失败";
|
||||||
|
} finally {
|
||||||
|
codeLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// --- 初始化极验4.0 ---
|
// --- 初始化极验4.0 ---
|
||||||
const startGeetest4 = async () => {
|
const startGeetest4 = async () => {
|
||||||
showCaptchaContainer.value = true;
|
showCaptchaContainer.value = true;
|
||||||
@@ -285,28 +393,48 @@ const handleLogin = async () => {
|
|||||||
errorMsg.value = "请输入密码";
|
errorMsg.value = "请输入密码";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading.value) return;
|
if (loading.value) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 先判断是否开启验证
|
|
||||||
const verifyRes = await getOpenVerify();
|
const verifyRes = await getOpenVerify();
|
||||||
let openVerify = "0";
|
let openVerify = "0";
|
||||||
|
let verifyTypeVal = "captcha";
|
||||||
if (verifyRes && verifyRes.code === 200 && Array.isArray(verifyRes.data)) {
|
if (verifyRes && verifyRes.code === 200 && Array.isArray(verifyRes.data)) {
|
||||||
verifyRes.data.forEach((item: any) => {
|
verifyRes.data.forEach((item: any) => {
|
||||||
if (item.label === "openVerify") {
|
if (item.label === "openVerify") {
|
||||||
openVerify = item.value || "0";
|
openVerify = item.value || "0";
|
||||||
}
|
}
|
||||||
|
if (item.label === "verifyType") {
|
||||||
|
verifyTypeVal = item.value || "captcha";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
openVerifyEnabled.value = openVerify === "1";
|
||||||
|
verifyType.value = verifyTypeVal as any;
|
||||||
|
if (openVerifyEnabled.value && verifyType.value === "captcha" && !captchaText.value) {
|
||||||
|
generateCaptcha();
|
||||||
|
}
|
||||||
|
|
||||||
// 如果开启了验证,使用极验4.0
|
if (openVerifyEnabled.value && verifyType.value === "captcha") {
|
||||||
if (openVerify === "1") {
|
if (!captchaInput.value.trim()) {
|
||||||
|
errorMsg.value = "请输入验证码";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (captchaInput.value.trim() !== captchaText.value) {
|
||||||
|
errorMsg.value = "验证码错误";
|
||||||
|
generateCaptcha();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (openVerifyEnabled.value && (verifyType.value === "sms" || verifyType.value === "email") && !smsCode.value.trim()) {
|
||||||
|
errorMsg.value = "请输入验证码";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openVerifyEnabled.value && verifyType.value === "geetest") {
|
||||||
await startGeetest4();
|
await startGeetest4();
|
||||||
} else {
|
} else {
|
||||||
// 直接登录
|
|
||||||
await performLoginRequest();
|
await performLoginRequest();
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -337,7 +465,7 @@ const handleRememberMeChange = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 页面加载处理
|
// 页面加载处理
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
// 从本地存储恢复表单
|
// 从本地存储恢复表单
|
||||||
const savedRemember = localStorage.getItem("loginRememberMe");
|
const savedRemember = localStorage.getItem("loginRememberMe");
|
||||||
if (savedRemember === "true") {
|
if (savedRemember === "true") {
|
||||||
@@ -346,6 +474,13 @@ onMounted(() => {
|
|||||||
password.value = localStorage.getItem("loginPassword") || "";
|
password.value = localStorage.getItem("loginPassword") || "";
|
||||||
rememberMe.value = true;
|
rememberMe.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 拉取登录验证配置,决定是否展示验证码模块
|
||||||
|
await initVerifyConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (countdownTimer) clearInterval(countdownTimer);
|
||||||
});
|
});
|
||||||
|
|
||||||
const goRegister = () => router.push("/register");
|
const goRegister = () => router.push("/register");
|
||||||
@@ -447,6 +582,29 @@ const goForget = () => router.push("/forget");
|
|||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mode-switch {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid #d6e6fa;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #f7fbfe;
|
||||||
|
color: #407ad6;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn.active {
|
||||||
|
background: #e8f3ff;
|
||||||
|
border-color: #7cb8ff;
|
||||||
|
color: #1d63c2;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -520,6 +678,32 @@ const goForget = () => router.push("/forget");
|
|||||||
box-shadow: 0 0 0 2px #e3f2ffb1;
|
box-shadow: 0 0 0 2px #e3f2ffb1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.code-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-input {
|
||||||
|
margin-bottom: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn {
|
||||||
|
width: 120px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1.3px solid #d6e6fa;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #fff;
|
||||||
|
color: #3a78ca;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-btn:disabled {
|
||||||
|
opacity: 0.65;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/* 极验验证码容器样式 */
|
/* 极验验证码容器样式 */
|
||||||
.geetest-container {
|
.geetest-container {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user