Compare commits

...
45 Commits
Author SHA1 Message Date
hero920103 de8035a829 修复边侧栏问题 2026-06-15 23:32:34 +08:00
hero920103 a0cdd2b851 gengxin 2026-06-02 21:15:42 +08:00
hero920103 1f5f4d16e6 更新后端界面,用户管理界面 2026-06-01 22:37:53 +08:00
hero920103 cffc718284 更新backend相关问题 2026-06-01 18:10:30 +08:00
hero920103 b727bae790 更新erp模块 2026-05-13 08:42:09 +08:00
hero920103 488b50c28c 更新登录 2026-04-02 17:53:52 +08:00
hero920103 76f90f50c0 批量修复问题 2026-04-02 00:05:00 +08:00
hero920103 c4b24bfc05 更新结构 2026-04-01 16:41:38 +08:00
hero920103 90b290af1f 更新 2026-04-01 10:12:52 +08:00
hero920103 f9db706769 增加产品分类 2026-03-21 14:12:44 +08:00
hero920103 0ad1dc31a1 增加产品和服务界面 2026-03-20 19:48:50 +08:00
hero920103 e3ab9c05bd 更新界面,特色服务 2026-03-20 09:13:27 +08:00
hero920103 9ddcdf71b9 修复模板选择bug 2026-03-19 15:48:06 +08:00
hero920103 db585c5a32 增加友链功能 2026-03-11 23:44:59 +08:00
hero920103 70e581b288 更新seo界面 2026-03-11 17:08:24 +08:00
hero920103 b904c5619d 更新若干bug 2026-03-11 11:11:57 +08:00
hero920103 66e747dd8c 修复若干bug 2026-03-10 22:24:00 +08:00
hero920103 183ade379b 解决404跳转问题 2026-03-10 16:09:08 +08:00
hero920103 a5a02eb8c9 批量更新租户规则 2026-03-09 22:55:27 +08:00
hero920103 108ec50978 优化tenant模式 2026-03-09 18:27:12 +08:00
hero920103 b8761ad3e4 更新网站架构 2026-03-09 16:35:39 +08:00
hero920103 e291e2c48f 更新需求模块 2026-03-09 09:40:03 +08:00
hero920103 07d3614d9c 更新后端传tenantid 2026-02-26 23:43:02 +08:00
hero920103 f8437d15b4 改组织架构不合理地方 2026-02-26 20:56:55 +08:00
hero920103 ef5eb809bc 增加极验模块 2026-02-26 17:38:03 +08:00
hero920103 6686cee4d9 增加登录验证 2026-02-26 11:47:36 +08:00
hero920103 0b268c65d6 优化用户和角色管理 2026-02-26 10:05:41 +08:00
hero920103 86ea16ec4b 增加资质界面 2026-02-26 09:54:02 +08:00
hero920103 4cbb93b19a 增加租户模式 2026-02-25 21:42:26 +08:00
hero920103 9669766eb3 erp功能增加 2026-02-25 10:58:46 +08:00
hero920103 6c904ef3cd 增加erp组织架构 2026-02-24 16:52:28 +08:00
hero920103 186315d82c 登录界面记住我功能 2026-02-24 12:56:39 +08:00
hero920103 c2921b9cc0 增加模块分类 2026-02-24 10:35:15 +08:00
hero920103 db23de308f 批量更新 2026-02-24 10:11:45 +08:00
hero920103 578e412473 更新配置 2026-02-07 00:11:32 +08:00
hero920103 32e7f2b21d 更新前端相关 2026-02-06 00:06:07 +08:00
hero920103 5ffeccad1d 增加邮件发送功能 2026-02-05 22:27:33 +08:00
hero920103 a4339a1cf5 修复裁剪头像代码 2026-02-05 17:42:13 +08:00
hero920103 858f7500bd 完善仪表盘统计 2026-02-05 15:23:37 +08:00
hero920103 eec987041c 更新宝贝监护功能 2026-02-04 23:45:55 +08:00
hero920103 649da2a6fd 增加宝贝监护功能 2026-02-04 18:02:17 +08:00
hero920103 07b95be324 增加cate模块 2026-01-30 17:27:17 +08:00
hero920103 0ecb832bce 修复数据库参数类型错误 2026-01-30 16:29:49 +08:00
hero920103 df0507ed2f 更新模块市场功能 2026-01-30 16:19:46 +08:00
hero920103 992b7e571b 增加模块市场 2026-01-29 17:37:39 +08:00
141 changed files with 14936 additions and 3174 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# 生产环境接口
VITE_API_BASE_URL='https://backend.yunzer.cn'
VITE_API_BASE_URL='https://api.yunzer.cn'
+2 -1
View File
@@ -6,5 +6,6 @@
// biome-ignore lint: disable
export {}
declare global {
const ElMessage: typeof import('element-plus/es').ElMessage
const ElMessageBox: typeof import('element-plus/es').ElMessageBox
}
+1 -1
View File
@@ -21,6 +21,7 @@ declare module 'vue' {
ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup']
ElCard: typeof import('element-plus/es')['ElCard']
ElCascader: typeof import('element-plus/es')['ElCascader']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCol: typeof import('element-plus/es')['ElCol']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElContainer: typeof import('element-plus/es')['ElContainer']
@@ -47,7 +48,6 @@ declare module 'vue' {
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElProgress: typeof import('element-plus/es')['ElProgress']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
+11
View File
@@ -0,0 +1,11 @@
//拼接接口路径
const getEnvUrl = (path: string) => {
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
return `${API_BASE_URL}${path}`;
};
用例:
<img :src="getEnvUrl(module.thumb)" :alt="module.title" />
const url = getEnvUrl('/admin/moduleCenter/modules');
+3
View File
@@ -5,6 +5,9 @@ import { onMounted } from 'vue';
// 使用 auth store 获取用户信息
const authStore = useAuthStore();
// 获取租户ID
const tenantId = (authStore.user as any)?.tid;
// 获取用户信息
const userInfo = authStore.user;
if (userInfo && userInfo.id) {
+32 -32
View File
@@ -1,39 +1,39 @@
<el-form-item label="默认图片" prop="image">
<div class="uploads">
<el-upload v-model:file-list="fileList" :action="uploadUrl" list-type="picture-card" :headers="uploadHeaders"
:limit="1" :auto-upload="false" :before-upload="beforeImgUpload" :on-success="handleImgSuccess">
<el-icon>
<Plus />
</el-icon>
<el-form-item label="默认图片" prop="image">
<div class="uploads">
<el-upload v-model:file-list="fileList" :action="uploadUrl" list-type="picture-card"
:headers="uploadHeaders" :limit="1" :auto-upload="false" :before-upload="beforeImgUpload"
:on-success="handleImgSuccess">
<el-icon>
<Plus />
</el-icon>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon>
<ZoomIn />
</el-icon>
<template #file="{ file }">
<div>
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
<span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
<el-icon>
<ZoomIn />
</el-icon>
</span>
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
<el-icon>
<Delete />
</el-icon>
</span>
</span>
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
<el-icon>
<Delete />
</el-icon>
</span>
</span>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
</div>
</template>
</el-upload>
<el-dialog v-model="dialogVisible">
<img w-full :src="dialogImageUrl" alt="Preview Image" />
</el-dialog>
<div class="upload-tip">
<span>建议尺寸:250px × 140px</span>
<div class="upload-tip">
<span>建议尺寸:250px × 140px</span>
</div>
</div>
</div>
</el-form-item>
</el-form-item>
import { uploadFile } from '@/api/file.js';
import { ElMessage, ElUpload } from 'element-plus'
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/src/assets/svgs/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
+590 -1353
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -22,6 +22,7 @@
"os": "^0.1.2",
"pinia": "^3.0.3",
"vue": "^3.5.22",
"vue-img-cutter": "^3.0.7",
"vue-router": "^4.6.3",
"vue3-pdf-app": "^1.0.3",
"xlsx": "^0.18.5"
@@ -29,7 +30,6 @@
"devDependencies": {
"@types/node": "^24.10.7",
"@vitejs/plugin-vue": "^6.0.1",
"sass-embedded": "^1.93.3",
"typescript": "^5.9.3",
"unplugin-auto-import": "^20.2.0",
"unplugin-vue-components": "^30.0.0",
+2 -2
View File
@@ -4,7 +4,7 @@ import request from "@/utils/request";
// 获取内容统计
export function getContentStats() {
return request({
url: "/admin/contentstats",
url: "/backend/contentstats",
method: "get",
});
}
@@ -12,7 +12,7 @@ export function getContentStats() {
// 获取用户统计
export function getUserStats() {
return request({
url: "/admin/usersstats",
url: "/backend/usersstats",
method: "get",
});
}
+19 -19
View File
@@ -4,7 +4,7 @@ import request from "@/utils/request";
// 获取文章列表
export function listArticles(params) {
return request({
url: `/admin/articlesList`,
url: `/backend/articlesList`,
method: "get",
params,
});
@@ -13,7 +13,7 @@ export function listArticles(params) {
// 获取文章所有文章
export function listAllArticles(params) {
return request({
url: `/admin/allarticles`,
url: `/backend/allarticles`,
method: "get",
params,
});
@@ -22,7 +22,7 @@ export function listAllArticles(params) {
// 获取文章详情
export function getArticle(id) {
return request({
url: `/admin/articles/${id}`,
url: `/backend/articles/${id}`,
method: "get",
});
}
@@ -30,7 +30,7 @@ export function getArticle(id) {
// 创建文章
export function createArticle(data) {
return request({
url: '/admin/createarticle',
url: '/backend/createarticle',
method: 'post',
data,
});
@@ -39,7 +39,7 @@ export function createArticle(data) {
// 编辑文章
export function editArticle(id, data) {
return request({
url: `/admin/editarticle/${id}`,
url: `/backend/editarticle/${id}`,
method: 'post',
data,
});
@@ -48,7 +48,7 @@ export function editArticle(id, data) {
// 删除文章
export function deleteArticle(id) {
return request({
url: `/admin/deletearticle/${id}`,
url: `/backend/deletearticle/${id}`,
method: "delete",
});
}
@@ -56,7 +56,7 @@ export function deleteArticle(id) {
// 发布文章
export function publishArticle(id,uid) {
return request({
url: `/admin/publisharticle/${id}`,
url: `/backend/publisharticle/${id}`,
method: 'post',
data: {
uid
@@ -67,7 +67,7 @@ export function publishArticle(id,uid) {
// 下架文章
export function unPublishArticle(id) {
return request({
url: `/admin/unPublisharticle/${id}`,
url: `/backend/unPublisharticle/${id}`,
method: 'post'
});
}
@@ -75,7 +75,7 @@ export function unPublishArticle(id) {
// 文章推荐
export function articleRecommend(id) {
return request({
url: `/admin/articleRecommend/${id}`,
url: `/backend/articleRecommend/${id}`,
method: 'post'
});
}
@@ -83,7 +83,7 @@ export function articleRecommend(id) {
// 取消文章推荐
export function unArticleRecommend(id) {
return request({
url: `/admin/unArticleRecommend/${id}`,
url: `/backend/unArticleRecommend/${id}`,
method: 'post'
});
}
@@ -91,7 +91,7 @@ export function unArticleRecommend(id) {
// 文章置顶
export function articleTop(id) {
return request({
url: `/admin/articleTop/${id}`,
url: `/backend/articleTop/${id}`,
method: 'post'
});
}
@@ -99,7 +99,7 @@ export function articleTop(id) {
// 取消文章置顶
export function unArticleTop(id) {
return request({
url: `/admin/unArticleTop/${id}`,
url: `/backend/unArticleTop/${id}`,
method: 'post'
});
}
@@ -111,7 +111,7 @@ export function unArticleTop(id) {
// 获取所有分类列表
export function allCategories(params) {
return request({
url: `/admin/allcategories`,
url: `/backend/allcategories`,
method: "get",
params,
});
@@ -120,7 +120,7 @@ export function allCategories(params) {
// 获取分类列表
export function listCategories(params) {
return request({
url: `/admin/categories`,
url: `/backend/categories`,
method: "get",
params,
});
@@ -129,7 +129,7 @@ export function listCategories(params) {
// 获取分类详情
export function getCategory(id) {
return request({
url: `/admin/categories/${id}`,
url: `/backend/categories/${id}`,
method: "get",
});
}
@@ -137,7 +137,7 @@ export function getCategory(id) {
// 创建分类
export function createCategory(data) {
return request({
url: `/admin/createCategory`,
url: `/backend/createCategory`,
method: "post",
data,
});
@@ -146,7 +146,7 @@ export function createCategory(data) {
// 更新分类
export function editCategory(id, data) {
return request({
url: `/admin/editCategory/${id}`,
url: `/backend/editCategory/${id}`,
method: "post",
data,
});
@@ -155,7 +155,7 @@ export function editCategory(id, data) {
// 删除分类
export function deleteCategory(id) {
return request({
url: `/admin/categories/${id}`,
url: `/backend/categories/${id}`,
method: "delete",
});
}
@@ -163,7 +163,7 @@ export function deleteCategory(id) {
// 更新分类状态
export function updateCategoryStatus(id, status) {
return request({
url: `/admin/categories/${id}/status`,
url: `/backend/categories/${id}/status`,
method: "patch",
data: { status },
});
+179
View File
@@ -0,0 +1,179 @@
import request from "@/utils/request";
/*************************************************
****************** 宝贝相关接口 ******************
*************************************************/
/**
* 获取宝贝列表
* @returns {Promise}
*/
export function getBabyList() {
return request({
url: '/backend/babys/list',
method: 'get'
});
}
/**
* 获取宝贝详情
* @param {number} id 宝贝ID
* @returns {Promise}
*/
export function getBabyDetail(id) {
return request({
url: `/backend/babys/${id}`,
method: "get",
});
}
/**
* 创建宝贝数据
* @param {Object} data 宝贝数据
* @returns {Promise}
*/
export function createBaby(data) {
return request({
url: "/backend/babys",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
// 更新宝贝信息
export function editBaby(id, data) {
return request({
url: `/backend/baby/update/${id}`,
method: 'post',
data: data
});
}
/**
* 删除宝贝数据
* @param {number} id 宝贝ID
* @returns {Promise}
*/
export function deleteBaby(id) {
return request({
url: `/backend/babys/${id}`,
method: "delete",
});
}
/**
* 绑定父母
* @param {number} id 宝贝ID
* @param {Object} data 绑定数据
* @returns {Promise}
*/
export function bindParent(id, data) {
return request({
url: `/backend/babys/bindparents/${id}`,
method: "post",
data: data,
});
}
/**
* 获取父母
* @param {number} id 宝贝ID
* @returns {Promise}
*/
export function getParents(id) {
return request({
url: `/backend/babys/getParents/${id}`,
method: "get",
});
}
/*************************************************
****************** 用户相关接口 ******************
*************************************************/
/**
* 获取用户列表
* @returns {Promise}
*/
export function getUserList() {
return request({
url: "/backend/babyhealthUser/list",
method: "get",
});
}
/**
* 获取用户详情
* @param {number} id 用户ID
* @returns {Promise}
*/
export function getUserDetail(id) {
return request({
url: `/backend/babyhealthUser/detail/${id}`,
method: "get",
});
}
/**
* 创建用户数据
* @param {Object} data 用户数据
* @returns {Promise}
*/
export function createUser(data) {
return request({
url: "/backend/babyhealthUser/create",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
/**
* 更新用户数据
* @param {number} id 用户ID
* @param {Object} data 更新的数据
* @returns {Promise}
*/
export function updateUser(id, data) {
return request({
url: `/backend/babyhealthUser/update/${id}`,
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
/**
* 删除用户数据
* @param {number} id 用户ID
* @returns {Promise}
*/
export function deleteUser(id) {
return request({
url: `/backend/babyhealthUser/delete/${id}`,
method: "delete",
});
}
/*************************************************
****************** 仪表盘相关接口 ******************
*************************************************/
/**
* dashborad总体输出
* @returns {Promise}
*/
export function getDashborad() {
return request({
url: "/backend/babyhealthDashborad/dashborad",
method: "get",
});
}
+4 -4
View File
@@ -6,7 +6,7 @@ import request from "@/utils/request";
*/
export function getBanners() {
return request({
url: "/admin/allbanners",
url: "/backend/allbanners",
method: "get",
});
}
@@ -18,7 +18,7 @@ export function getBanners() {
*/
export function createBanner(formData) {
return request({
url: "/admin/createbanner",
url: "/backend/createbanner",
method: "post",
data: formData,
headers: {
@@ -35,7 +35,7 @@ export function createBanner(formData) {
*/
export function editBanner(id, bannerData) {
return request({
url: `/admin/editbanner/${id}`,
url: `/backend/editbanner/${id}`,
method: "post",
data: bannerData,
});
@@ -48,7 +48,7 @@ export function editBanner(id, bannerData) {
*/
export function deleteBanner(id) {
return request({
url: `/admin/deletebanner/${id}`,
url: `/backend/deletebanner/${id}`,
method: "delete",
});
}
+4 -4
View File
@@ -2,7 +2,7 @@ import request from '@/utils/request'
export function listContacts(params) {
return request({
url: '/admin/crm/contact/list',
url: '/backend/crm/contact/list',
method: 'get',
params,
})
@@ -10,7 +10,7 @@ export function listContacts(params) {
export function createContact(data) {
return request({
url: '/admin/crm/contact/add',
url: '/backend/crm/contact/add',
method: 'post',
data,
})
@@ -18,7 +18,7 @@ export function createContact(data) {
export function updateContact(data) {
return request({
url: '/admin/crm/contact/edit',
url: '/backend/crm/contact/edit',
method: 'post',
data,
})
@@ -26,7 +26,7 @@ export function updateContact(data) {
export function deleteContact(data) {
return request({
url: '/admin/crm/contact/delete',
url: '/backend/crm/contact/delete',
method: 'post',
data,
})
+3 -3
View File
@@ -6,7 +6,7 @@ import request from "@/utils/request";
*/
export function getPlatformStats() {
return request({
url: "/admin/dashboard/platform-stats",
url: "/backend/dashboard/platform-stats",
method: "get",
});
}
@@ -17,7 +17,7 @@ export function getPlatformStats() {
*/
export function getTenantStats() {
return request({
url: "/admin/dashboard/tenant-stats",
url: "/backend/dashboard/tenant-stats",
method: "get",
});
}
@@ -30,7 +30,7 @@ export function getTenantStats() {
*/
export function getActivityLogs(pageNum = 1, pageSize = 10) {
return request({
url: "/admin/dashboard/user-activity-logs",
url: "/backend/dashboard/user-activity-logs",
method: "get",
params: {
page_num: pageNum,
+51
View File
@@ -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",
});
}
+5 -5
View File
@@ -3,7 +3,7 @@ import request from '@/utils/request';
// 获取租户下的所有部门
export function getTenantDepartments(tenantId) {
return request({
url: `/admin/departments/tenant/${tenantId}`,
url: `/backend/departments/tenant/${tenantId}`,
method: 'get',
});
}
@@ -11,7 +11,7 @@ export function getTenantDepartments(tenantId) {
// 获取部门详情
export function getDepartmentInfo(departmentId) {
return request({
url: `/admin/departments/${departmentId}`,
url: `/backend/departments/${departmentId}`,
method: 'get',
});
}
@@ -19,7 +19,7 @@ export function getDepartmentInfo(departmentId) {
// 添加部门
export function addDepartment(data) {
return request({
url: '/admin/departments',
url: '/backend/departments',
method: 'post',
data,
});
@@ -28,7 +28,7 @@ export function addDepartment(data) {
// 更新部门信息
export function editDepartment(departmentId, data) {
return request({
url: `/admin/departments/${departmentId}`,
url: `/backend/departments/${departmentId}`,
method: 'put',
data,
});
@@ -37,7 +37,7 @@ export function editDepartment(departmentId, data) {
// 删除部门
export function deleteDepartment(departmentId) {
return request({
url: `/admin/departments/${departmentId}`,
url: `/backend/departments/${departmentId}`,
method: 'delete',
});
}
+12 -12
View File
@@ -3,7 +3,7 @@ import request from '@/utils/request'
// 获取字典类型列表
export function getDictTypes(params) {
return request({
url: '/admin/dict/types',
url: '/backend/dict/types',
method: 'get',
params
})
@@ -12,7 +12,7 @@ export function getDictTypes(params) {
// 根据ID获取字典类型
export function getDictTypeById(id) {
return request({
url: `/admin/dict/types/${id}`,
url: `/backend/dict/types/${id}`,
method: 'get'
})
}
@@ -20,7 +20,7 @@ export function getDictTypeById(id) {
// 添加字典类型
export function addDictType(data) {
return request({
url: '/admin/dict/types',
url: '/backend/dict/types',
method: 'post',
data: {
...data,
@@ -32,7 +32,7 @@ export function addDictType(data) {
// 更新字典类型
export function updateDictType(id, data) {
return request({
url: `/admin/dict/types/${id}`,
url: `/backend/dict/types/${id}`,
method: 'put',
data: {
...data,
@@ -44,7 +44,7 @@ export function updateDictType(id, data) {
// 删除字典类型
export function deleteDictType(id) {
return request({
url: `/admin/dict/types/${id}`,
url: `/backend/dict/types/${id}`,
method: 'delete'
})
}
@@ -52,7 +52,7 @@ export function deleteDictType(id) {
// 获取字典项列表
export function getDictItems(params) {
return request({
url: '/admin/dict/items',
url: '/backend/dict/items',
method: 'get',
params
})
@@ -61,7 +61,7 @@ export function getDictItems(params) {
// 根据ID获取字典项
export function getDictItemById(id) {
return request({
url: `/admin/dict/items/${id}`,
url: `/backend/dict/items/${id}`,
method: 'get'
})
}
@@ -69,7 +69,7 @@ export function getDictItemById(id) {
// 添加字典项
export function addDictItem(data) {
return request({
url: '/admin/dict/items',
url: '/backend/dict/items',
method: 'post',
data
})
@@ -78,7 +78,7 @@ export function addDictItem(data) {
// 更新字典项
export function updateDictItem(id, data) {
return request({
url: `/admin/dict/items/${id}`,
url: `/backend/dict/items/${id}`,
method: 'put',
data
})
@@ -87,7 +87,7 @@ export function updateDictItem(id, data) {
// 删除字典项
export function deleteDictItem(id) {
return request({
url: `/admin/dict/items/${id}`,
url: `/backend/dict/items/${id}`,
method: 'delete'
})
}
@@ -95,7 +95,7 @@ export function deleteDictItem(id) {
// 根据字典编码获取字典项(用于业务查询)
export function getDictItemsByCode(code, includeDisabled = false) {
return request({
url: `/admin/dict/items/code/${code}`,
url: `/backend/dict/items/code/${code}`,
method: 'get',
params: {
include_disabled: includeDisabled ? '1' : '0'
@@ -106,7 +106,7 @@ export function getDictItemsByCode(code, includeDisabled = false) {
// 批量更新字典项排序
export function batchUpdateDictItemSort(data) {
return request({
url: '/admin/dict/items/sort',
url: '/backend/dict/items/sort',
method: 'put',
data
})
+110
View File
@@ -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'
})
}
+36
View File
@@ -0,0 +1,36 @@
import request from "@/utils/request";
/**
* 获取邮箱信息
* @returns {Promise}
*/
export function getEmailInfo() {
return request({
url: "/backend/email/info",
method: "get",
});
}
/**
* 编辑邮箱信息
* @returns {Promise}
*/
export function editEmailInfo(data) {
return request({
url: "/backend/email/editinfo",
method: "post",
data,
});
}
/**
* 发送测试邮件
* @returns {Promise}
*/
export function sendTestEmail(data) {
return request({
url: "/backend/email/sendtestemail",
method: "post",
data,
});
}
+155
View File
@@ -0,0 +1,155 @@
import request from "@/utils/request";
/*************************************************
****************** 组织机构相关接口 ******************
*************************************************/
/**
* 获取组织机构列表
* @returns {Promise}
*/
export function getOrganizationList() {
return request({
url: '/backend/erp/getOrganization',
method: 'get'
});
}
/**
* 获取组织机构详情
* @param {number} id 组织机构ID
* @returns {Promise}
*/
export function getOrganizationDetail(id) {
return request({
url: `/backend/erp/getOrganizationDetail/${id}`,
method: "get",
});
}
/**
* 创建组织机构数据
* @param {Object} data 组织机构数据
* @returns {Promise}
*/
export function createOrganization(data) {
return request({
url: "/backend/erp/createOrganization",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
// 更新组织机构信息
export function editOrganization(id, data) {
return request({
url: `/backend/erp/editOrganization/${id}`,
method: 'post',
data: data
});
}
/**
* 删除组织机构数据
* @param {number} id 组织机构ID
* @returns {Promise}
*/
export function deleteOrganization(id) {
return request({
url: `/backend/erp/deleteOrganization/${id}`,
method: "delete",
});
}
/**
* 获取企业单位列表
* @returns {Promise}
*/
export function getCompanys() {
return request({
url: '/backend/erp/getCompanys',
method: 'get'
});
}
/**
* 获取部门列表
* @param {number} parentId 隶属单位ID
* @returns {Promise}
*/
export function getDepartments(parentId) {
return request({
url: '/backend/erp/getDepartments',
method: 'get',
params: parentId ? { parent_id: parentId } : {}
});
}
/*************************************************
****************** 员工相关接口 ******************
*************************************************/
/**
* 获取员工列表
* @param {number} tenantId 租户ID
* @returns {Promise}
*/
export function getEmployeeList(tenantId) {
return request({
url: '/backend/erp/getEmployee',
method: 'get',
params: { tid: tenantId }
});
}
/**
* 获取员工详情
* @param {number} id 员工ID
* @returns {Promise}
*/
export function getEmployeeDetail(id) {
return request({
url: `/backend/erp/getEmployeeDetail/${id}`,
method: "get",
});
}
/**
* 创建员工数据
* @param {Object} data 员工数据
* @returns {Promise}
*/
export function createEmployee(data) {
return request({
url: "/backend/erp/createEmployee",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data"
}
});
}
// 更新员工信息
export function editEmployee(id, data) {
return request({
url: `/backend/erp/editEmployee/${id}`,
method: 'post',
data: data
});
}
/**
* 删除员工数据
* @param {number} id 员工ID
* @returns {Promise}
*/
export function deleteEmployee(id) {
return request({
url: `/backend/erp/deleteEmployee/${id}`,
method: "delete",
});
}
+63 -11
View File
@@ -6,7 +6,7 @@ import request from "@/utils/request";
*/
export function getUserCate() {
return request({
url: `/admin/usercate`,
url: `/backend/usercate`,
method: "get",
});
}
@@ -17,7 +17,7 @@ export function getUserCate() {
*/
export function getAllFiles() {
return request({
url: "/admin/allfiles",
url: "/backend/allfiles",
method: "get",
});
}
@@ -29,7 +29,7 @@ export function getAllFiles() {
*/
export function createFileCate(data) {
return request({
url: "/admin/createfilecate",
url: "/backend/createfilecate",
method: "post",
data,
});
@@ -43,7 +43,7 @@ export function createFileCate(data) {
*/
export function renameFileCate(id, data) {
return request({
url: `/admin/renamefilecate/${id}`,
url: `/backend/renamefilecate/${id}`,
method: "post",
data,
});
@@ -56,7 +56,7 @@ export function renameFileCate(id, data) {
*/
export function deleteFileCate(id) {
return request({
url: `/admin/deletefilecate/${id}`,
url: `/backend/deletefilecate/${id}`,
method: "delete",
});
}
@@ -78,7 +78,7 @@ export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
params.keyword = keyword;
}
return request({
url: `/admin/catefiles/${id}`,
url: `/backend/catefiles/${id}`,
method: "get",
params,
});
@@ -91,7 +91,7 @@ export function getCateFiles(id, page = 1, pageSize = 24, keyword = "") {
*/
export function getFileById(id) {
return request({
url: `/admin/catefiles`,
url: `/backend/catefiles`,
method: "get",
});
}
@@ -109,7 +109,7 @@ export function uploadFile(formData, options = {}) {
}
return request({
url: "/admin/uploadfile",
url: "/backend/uploadfile",
method: "post",
data: formData,
headers: {
@@ -126,7 +126,7 @@ export function uploadFile(formData, options = {}) {
*/
export function updateFile(id, fileData) {
return request({
url: `/admin/updatefile/${id}`,
url: `/backend/updatefile/${id}`,
method: "post",
data: fileData,
});
@@ -139,7 +139,19 @@ export function updateFile(id, fileData) {
*/
export function deleteFile(id) {
return request({
url: `/admin/deletefile/${id}`,
url: `/backend/deletefile/${id}`,
method: "delete",
});
}
/**
* 删除文件
* @param {number|string} id 文件ID
* @returns {Promise}
*/
export function deleteFilePermanently(id) {
return request({
url: `/backend/deleteFilePermanently/${id}`,
method: "delete",
});
}
@@ -152,8 +164,48 @@ export function deleteFile(id) {
*/
export function moveFile(id, cate) {
return request({
url: `/admin/movefile/${id}`,
url: `/backend/movefile/${id}`,
method: "get",
params: { cate },
});
}
/**
* 批量删除文件
* @param {Array} ids 文件ID数组
* @returns {Promise}
*/
export function batchDeleteFiles(ids) {
return request({
url: "/backend/batchDeleteFiles",
method: "post",
data: { ids },
});
}
/**
* 批量彻底删除文件
* @param {Array} ids 文件ID数组
* @returns {Promise}
*/
export function batchDeleteFilesPermanently(ids) {
return request({
url: "/backend/batchDeleteFilesPermanently",
method: "post",
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 },
});
}
+77
View File
@@ -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 }
})
}
+4 -4
View File
@@ -6,7 +6,7 @@ import request from "@/utils/request";
*/
export function getFrontMenus() {
return request({
url: "/admin/frontmenus",
url: "/backend/frontmenus",
method: "get",
});
}
@@ -18,7 +18,7 @@ export function getFrontMenus() {
*/
export function createFrontMenu(formData, options = {}) {
return request({
url: "/admin/createfrontmenu",
url: "/backend/createfrontmenu",
method: "post",
data: formData,
headers: {
@@ -35,7 +35,7 @@ export function createFrontMenu(formData, options = {}) {
*/
export function editFrontMenu(id, frontMenuData) {
return request({
url: `/admin/editfrontmenu/${id}`,
url: `/backend/editfrontmenu/${id}`,
method: "post",
data: frontMenuData,
});
@@ -48,7 +48,7 @@ export function editFrontMenu(id, frontMenuData) {
*/
export function deleteFrontMenu(id) {
return request({
url: `/admin/deletefrontmenu/${id}`,
url: `/backend/deletefrontmenu/${id}`,
method: "delete",
});
}
+91 -7
View File
@@ -1,14 +1,29 @@
import request from "@/utils/request";
// 登录(使用租户名称)
export function login(account, password) {
export function login(data) {
return request({
url: `/admin/login`,
url: `/backend/login`,
method: "post",
data: {
account: account,
password: password,
},
data,
});
}
// 发送登录验证码(手机号)
export function sendLoginCode(data) {
return request({
url: "/backend/sendLoginCode",
method: "post",
data,
});
}
// 手机号验证码登录
export function loginBySms(data) {
return request({
url: "/backend/loginBySms",
method: "post",
data,
});
}
// 登出
@@ -26,8 +41,77 @@ export function logout(userInfo = null) {
}
return request({
url: `/admin/logout`,
url: `/backend/logout`,
method: "post",
data: userInfo ? { userInfo: userInfo } : {},
});
}
/**
* 获取极验3.0数据
* @returns {Promise}
*/
export function getGeetest3Infos() {
return request({
url: '/backend/login/getGeetest3Infos',
method: 'get'
});
}
/**
* 获取极验4.0数据
* @returns {Promise}
*/
export function getGeetest4Infos() {
return request({
url: '/backend/login/getGeetest4Infos',
method: 'get'
});
}
/**
* 判断是否开启验证
* @returns {Promise}
*/
export function getOpenVerify() {
return request({
url: '/backend/login/getOpenVerify',
method: 'get'
});
}
// 注册
export function register(data) {
return request({
url: "/backend/register",
method: "post",
data,
});
}
// 发送注册验证码
export function sendRegisterCode(data) {
return request({
url: "/backend/sendRegisterCode",
method: "post",
data,
});
}
// 忘记密码重置
export function resetPassword(data) {
return request({
url: "/backend/resetPassword",
method: "post",
data,
});
}
// 发送找回密码验证码
export function sendResetCode(data) {
return request({
url: "/backend/sendResetCode",
method: "post",
data,
});
}
+6 -6
View File
@@ -3,7 +3,7 @@ import request from "@/utils/request";
// 获取所有菜单
export function getAllMenus() {
return request({
url: `/admin/allmenu`,
url: `/backend/allmenu`,
method: "get",
});
}
@@ -11,7 +11,7 @@ export function getAllMenus() {
//获取用户菜单
export function getMenus(id){
return request({
url: `/admin/menu/${parseInt(id)}`,
url: `/backend/menu/${parseInt(id)}`,
method: "get",
});
}
@@ -19,7 +19,7 @@ export function getMenus(id){
// 更新菜单状态
export function updateMenuStatus(menuId, status) {
return request({
url: `/admin/menu/status/${menuId}`,
url: `/backend/menu/status/${menuId}`,
method: "patch",
data: { status },
});
@@ -28,7 +28,7 @@ export function updateMenuStatus(menuId, status) {
// 创建菜单
export function createMenu(menuData) {
return request({
url: `/admin/createmenu`,
url: `/backend/createmenu`,
method: "post",
data: menuData,
});
@@ -37,7 +37,7 @@ export function createMenu(menuData) {
// 更新菜单
export function updateMenu(menuId, menuData) {
return request({
url: `/admin/updatemenu/${menuId}`,
url: `/backend/updatemenu/${menuId}`,
method: "put",
data: menuData,
});
@@ -46,7 +46,7 @@ export function updateMenu(menuId, menuData) {
// 删除菜单
export function deleteMenu(menuId) {
return request({
url: `/admin/deletemenu/${menuId}`,
url: `/backend/deletemenu/${menuId}`,
method: "delete",
});
}
+57
View File
@@ -0,0 +1,57 @@
import request from "@/utils/request";
/**
* 获取模块中心分类
* @returns {Promise}
*/
export function getModuleCategory() {
return request({
url: "/backend/moduleCategory",
method: "get",
});
}
/**
* 获取模块中心列表
* @param {number} cid 分类id
* @returns {Promise}
*/
export function getModules(cid) {
return request({
url: "/backend/moduleCenter/modules",
method: "get",
params: { cid }
});
}
/**
* 编辑模块分类
* @param {Object} data 分类数据
* @param {number} data.id 分类id(编辑时必填,新增时不填)
* @param {string} data.title 分类名称
* @param {number} data.status 分类状态
* @returns {Promise}
*/
export function editModuleCategory(data) {
return request({
url: "/backend/moduleCenter/editCategory",
method: "post",
data
});
}
/**
* 编辑模块
* @param {Object} data 模块数据
* @param {number} data.id 模块id(编辑时必填,新增时不填)
* @param {string} data.title 模块名称
* @param {number} data.status 模块状态
* @returns {Promise}
*/
export function editModules(data) {
return request({
url: "/backend/moduleCenter/editModules",
method: "post",
data
});
}
+15 -8
View File
@@ -2,21 +2,28 @@ import request from '@/utils/request';
export function getModulesList() {
return request({
url: '/admin/modules/list',
url: '/backend/modules/list',
method: 'get',
});
}
export function getTenantList() {
return request({
url: '/backend/modules/getTenantList',
method: 'get',
});
}
export function getModuleDetail(id) {
return request({
url: `/admin/modules/${id}`,
url: `/backend/modules/${id}`,
method: 'get',
});
}
export function addModule(data) {
return request({
url: '/admin/modules',
url: '/backend/modules',
method: 'post',
data,
});
@@ -24,7 +31,7 @@ export function addModule(data) {
export function editModule(id, data) {
return request({
url: `/admin/modules/${id}`,
url: `/backend/modules/${id}`,
method: 'put',
data,
});
@@ -32,14 +39,14 @@ export function editModule(id, data) {
export function deleteModule(id) {
return request({
url: `/admin/modules/${id}`,
url: `/backend/modules/${id}`,
method: 'delete',
});
}
export function batchDeleteModules(ids) {
return request({
url: '/admin/modules/batchDelete',
url: '/backend/modules/batchDelete',
method: 'post',
data: { ids },
});
@@ -47,7 +54,7 @@ export function batchDeleteModules(ids) {
export function changeModuleStatus(id, status) {
return request({
url: '/admin/modules/status',
url: '/backend/modules/status',
method: 'post',
data: { id, status },
});
@@ -55,7 +62,7 @@ export function changeModuleStatus(id, status) {
export function getModulesSelectList() {
return request({
url: '/admin/modules/select/list',
url: '/backend/modules/select/list',
method: 'get',
});
}
+4 -4
View File
@@ -6,7 +6,7 @@ import request from "@/utils/request";
*/
export function getOnePages() {
return request({
url: "/admin/allonepages",
url: "/backend/allonepages",
method: "get",
});
}
@@ -18,7 +18,7 @@ export function getOnePages() {
*/
export function createOnePage(formData) {
return request({
url: "/admin/createonepage",
url: "/backend/createonepage",
method: "post",
data: formData,
});
@@ -32,7 +32,7 @@ export function createOnePage(formData) {
*/
export function editOnePage(id, onePageData) {
return request({
url: `/admin/editonepage/${id}`,
url: `/backend/editonepage/${id}`,
method: "post",
data: onePageData,
});
@@ -45,7 +45,7 @@ export function editOnePage(id, onePageData) {
*/
export function deleteOnePage(id) {
return request({
url: `/admin/deleteonepage/${id}`,
url: `/backend/deleteonepage/${id}`,
method: "delete",
});
}
+5 -5
View File
@@ -15,7 +15,7 @@ import request from "@/utils/request";
*/
export function getOperationLogs(params) {
return request({
url: "/admin/operationLogs",
url: "/backend/operationLogs",
method: "get",
params,
});
@@ -28,7 +28,7 @@ export function getOperationLogs(params) {
*/
export function getOperationLogDetail(id) {
return request({
url: `/admin/operationLogs/${id}`,
url: `/backend/operationLogs/${id}`,
method: "get",
});
}
@@ -40,7 +40,7 @@ export function getOperationLogDetail(id) {
*/
export function deleteOperationLog(id) {
return request({
url: `/admin/operationLogs/${id}`,
url: `/backend/operationLogs/${id}`,
method: "delete",
});
}
@@ -52,7 +52,7 @@ export function deleteOperationLog(id) {
*/
export function batchDeleteOperationLogs(ids) {
return request({
url: "/admin/operationLogs/batchDelete",
url: "/backend/operationLogs/batchDelete",
method: "post",
data: { ids },
});
@@ -64,7 +64,7 @@ export function batchDeleteOperationLogs(ids) {
*/
export function getOperationStatistics() {
return request({
url: "/admin/operationLogs/statistics",
url: "/backend/operationLogs/statistics",
method: "get",
});
}
+3 -3
View File
@@ -2,7 +2,7 @@ import request from '@/utils/request';
export function getAllMenuPermissions(params = {}) {
return request({
url: '/admin/allmenupermissions',
url: '/backend/allmenupermissions',
method: 'get',
params
});
@@ -10,14 +10,14 @@ export function getAllMenuPermissions(params = {}) {
export function getRolePermissions(roleId) {
return request({
url: `/admin/rolepermissions/${roleId}`,
url: `/backend/rolepermissions/${roleId}`,
method: 'get'
});
}
export function assignRolePermissions(roleId, permissions) {
return request({
url: `/admin/assignrolepermissions/${roleId}`,
url: `/backend/assignrolepermissions/${roleId}`,
method: 'post',
data: { permissions }
});
+6 -6
View File
@@ -3,7 +3,7 @@ import request from '@/utils/request';
// 获取租户下的所有职位
export function getTenantPositions(tenantId) {
return request({
url: `/admin/positions/tenant/${tenantId}`,
url: `/backend/positions/tenant/${tenantId}`,
method: 'get',
});
}
@@ -11,7 +11,7 @@ export function getTenantPositions(tenantId) {
// 根据部门ID获取职位列表
export function getPositionsByDepartment(departmentId) {
return request({
url: `/admin/positions/department/${departmentId}`,
url: `/backend/positions/department/${departmentId}`,
method: 'get',
});
}
@@ -19,7 +19,7 @@ export function getPositionsByDepartment(departmentId) {
// 获取职位详情
export function getPositionInfo(positionId) {
return request({
url: `/admin/positions/${positionId}`,
url: `/backend/positions/${positionId}`,
method: 'get',
});
}
@@ -27,7 +27,7 @@ export function getPositionInfo(positionId) {
// 添加职位
export function addPosition(data) {
return request({
url: '/admin/positions',
url: '/backend/positions',
method: 'post',
data,
});
@@ -36,7 +36,7 @@ export function addPosition(data) {
// 更新职位信息
export function editPosition(positionId, data) {
return request({
url: `/admin/positions/${positionId}`,
url: `/backend/positions/${positionId}`,
method: 'put',
data,
});
@@ -45,7 +45,7 @@ export function editPosition(positionId, data) {
// 删除职位
export function deletePosition(positionId) {
return request({
url: `/admin/positions/${positionId}`,
url: `/backend/positions/${positionId}`,
method: 'delete',
});
}
+105
View File
@@ -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
View File
@@ -2,21 +2,21 @@ import request from '@/utils/request'
export function getAllRoles() {
return request({
url: '/admin/allRoles',
url: '/backend/allRoles',
method: 'get'
})
}
export function getRoleById(id) {
return request({
url: `/admin/roles/${id}`,
url: `/backend/roles/${id}`,
method: 'get'
})
}
export function createRole(data) {
return request({
url: '/admin/roles',
url: '/backend/roles',
method: 'post',
data
})
@@ -24,7 +24,7 @@ export function createRole(data) {
export function updateRole(id, data) {
return request({
url: `/admin/roles/${id}`,
url: `/backend/roles/${id}`,
method: 'put',
data
})
@@ -32,7 +32,7 @@ export function updateRole(id, data) {
export function deleteRole(id) {
return request({
url: `/admin/roles/${id}`,
url: `/backend/roles/${id}`,
method: 'delete'
})
}
+53
View File
@@ -0,0 +1,53 @@
import request from '@/utils/request'
/**
* 获取特色服务列表
* @param {Object} params - 查询参数
* @returns {Promise}
*/
export function getServiceList(params) {
return request({
url: '/backend/servicesList',
method: 'get',
params
})
}
/**
* 添加特色服务
* @param {Object} data - 服务数据
* @returns {Promise}
*/
export function addService(data) {
return request({
url: '/backend/addServices',
method: 'post',
data
})
}
/**
* 更新特色服务
* @param {number} id - 服务ID
* @param {Object} data - 服务数据
* @returns {Promise}
*/
export function updateService(id, data) {
return request({
url: `/backend/editServices/${id}`,
method: 'put',
data
})
}
/**
* 删除特色服务
* @param {number} id - 服务ID
* @returns {Promise}
*/
export function deleteService(id) {
return request({
url: `/backend/deleteServices/${id}`,
method: 'delete'
})
}
+88 -12
View File
@@ -2,12 +2,14 @@ import request from "@/utils/request";
/**
* 获取基本信息
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getNormalInfos() {
export function getNormalInfos(tid) {
return request({
url: "/admin/normalInfos",
url: "/backend/normalInfos",
method: "get",
params: { tid }
});
}
@@ -18,22 +20,46 @@ export function getNormalInfos() {
*/
export function saveNormalInfos(data) {
return request({
url: "/admin/saveNormalInfos",
url: "/backend/saveNormalInfos",
method: "post",
data: {
data: JSON.stringify(data)
},
data: data,
});
}
/**
* 获取登录验证数据
* @returns {Promise}
*/
export function getVerifyInfos() {
return request({
url: "/backend/loginVerifyInfos",
method: "get",
});
}
/**
* 保存登录验证数据
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveVerifyInfos(data) {
return request({
url: "/backend/saveloginVerifyInfos",
method: "post",
data: data,
});
}
/**
* 获取法律声明和隐私条款
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getLegalInfos() {
export function getLegalInfos(tid) {
return request({
url: "/admin/legalInfos",
url: "/backend/legalInfos",
method: "get",
params: { tid }
});
}
@@ -44,10 +70,60 @@ export function getLegalInfos() {
*/
export function saveLegalInfos(data) {
return request({
url: "/admin/saveLegalInfos",
url: "/backend/saveLegalInfos",
method: "post",
data: {
data: JSON.stringify(data)
},
data: data,
});
}
/**
* 获取企业信息
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getCompanyInfos(tid) {
return request({
url: "/backend/companyInfos",
method: "get",
params: { tid }
});
}
/**
* 保存企业信息
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveCompanyInfos(data) {
return request({
url: "/backend/saveCompanyInfos",
method: "post",
data: data,
});
}
/**
* 获取企业SEO
* @param {number} tid 租户ID
* @returns {Promise}
*/
export function getCompanySeo(tid) {
return request({
url: "/backend/companySeo",
method: "get",
params: { tid }
});
}
/**
* 保存企业SEO
* @param {Object} data 要保存的数据
* @returns {Promise}
*/
export function saveCompanySeo(data) {
return request({
url: "/backend/saveCompanySeo",
method: "post",
data: data,
});
}
+56
View File
@@ -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,
});
}
+84
View File
@@ -0,0 +1,84 @@
import request from "@/utils/request";
/*************************************************
****************** 租户相关接口 ******************
*************************************************/
/**
* 获取租户列表
* @param {Object} params 包含 page 和 pageSize
* @returns {Promise}
*/
export function getTenantList(params) {
return request({
url: "/backend/tenant/getTenant",
method: "get",
params: params,
});
}
/**
* 获取租户详情
* @param {number} id 租户ID
* @returns {Promise}
*/
export function getTenantDetail(id) {
return request({
url: `/backend/tenant/getTenantDetail/${id}`,
method: "get",
});
}
/**
* 创建租户数据
* @param {Object} data 租户数据
* @returns {Promise}
*/
export function createTenant(data) {
return request({
url: "/backend/tenant/createTenant",
method: "post",
data: data,
headers: {
"Content-Type": "multipart/form-data",
},
});
}
/**
* 更新租户数据
* @param {Object} data 租户数据
* @returns {Promise}
*/
export function editTenant(id, data) {
return request({
url: `/backend/tenant/editTenant/${id}`,
method: "post",
data: data,
});
}
/**
* 删除租户数据
* @param {number} id 租户ID
* @returns {Promise}
*/
export function deleteTenant(id) {
return request({
url: `/backend/tenant/deleteTenant/${id}`,
method: "delete",
});
}
/**
* 校验租户编码是否重复
* @param {string} tenant_code 编码
* @param {number} id 可选,当前编辑的租户ID
*/
export function checkTenantCode(tenant_code) {
return request({
url: '/backend/tenant/findTenantCode',
method: 'get',
params: { tenant_code }
});
}
+36
View File
@@ -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
})
}
+32 -8
View File
@@ -1,9 +1,18 @@
import request from '@/utils/request';
//获取所有用户信息
export function getAllUsers() {
export function getAllUsers(params) {
return request({
url: '/admin/getAllUsers',
url: '/backend/getAllUsers',
method: 'get',
params,
});
}
//获取租户用户
export function getTenantUsers(tenantId) {
return request({
url: `/backend/getTenantUsers/${tenantId}`,
method: 'get',
});
}
@@ -11,7 +20,7 @@ export function getAllUsers() {
// 获取用户信息
export function getUserInfo(userId) {
return request({
url: `/admin/getUserInfo/${userId}`,
url: `/backend/getUserInfo/${userId}`,
method: 'get',
});
}
@@ -19,7 +28,7 @@ export function getUserInfo(userId) {
// 添加用户
export function addUser(data) {
return request({
url: '/admin/addUser',
url: '/backend/addUser',
method: 'post',
data,
});
@@ -28,7 +37,7 @@ export function addUser(data) {
// 编辑用户信息
export function editUser(userId, data) {
return request({
url: `/admin/editUser/${userId}`,
url: `/backend/editUser/${userId}`,
method: 'post',
data,
});
@@ -39,10 +48,25 @@ export function updateUserInfo(userId, data) {
return editUser(userId, data);
}
// 更新个人资料
export function updateUserProfile(userId, data) {
return editUser(userId, data);
}
// 绑定/更换手机号
export function bindPhone(userId, phone) {
return editUser(userId, { phone });
}
// 绑定/更换邮箱
export function bindEmail(userId, email) {
return editUser(userId, { email });
}
// 删除用户
export function deleteUser(userId) {
return request({
url: `/admin/deleteUser/${userId}`,
url: `/backend/deleteUser/${userId}`,
method: 'delete',
});
}
@@ -50,11 +74,11 @@ export function deleteUser(userId) {
// 修改密码
export function changePassword(userId, data) {
return request({
url: '/admin/changePassword',
url: '/backend/changePassword',
method: 'post',
data: {
id: userId,
password: data.newPassword
},
});
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ import request from "@/utils/request";
// 获取文章列表
export function GetCRMWorkbench(params) {
return request({
url: `/admin/workbench/crm`,
url: `/backend/workbench/crm`,
method: "get",
params,
});
+353
View File
@@ -0,0 +1,353 @@
"v0.5.0 Geetest Inc.";
(function (window) {
"use strict";
if (typeof window === 'undefined') {
throw new Error('Geetest requires browser environment');
}
var document = window.document;
var Math = window.Math;
var head = document.getElementsByTagName("head")[0];
function _Object(obj) {
this._obj = obj;
}
_Object.prototype = {
_each: function (process) {
var _obj = this._obj;
for (var k in _obj) {
if (_obj.hasOwnProperty(k)) {
process(k, _obj[k]);
}
}
return this;
}
};
function Config(config) {
var self = this;
new _Object(config)._each(function (key, value) {
self[key] = value;
});
}
Config.prototype = {
api_server: 'api.geetest.com',
protocol: 'http://',
typePath: '/gettype.php',
fallback_config: {
slide: {
static_servers: ["static.geetest.com", "static.geevisit.com"],
type: 'slide',
slide: '/static/js/geetest.0.0.0.js'
},
fullpage: {
static_servers: ["static.geetest.com", "static.geevisit.com"],
type: 'fullpage',
fullpage: '/static/js/fullpage.0.0.0.js'
}
},
_get_fallback_config: function () {
var self = this;
if (isString(self.type)) {
return self.fallback_config[self.type];
} else if (self.new_captcha) {
return self.fallback_config.fullpage;
} else {
return self.fallback_config.slide;
}
},
_extend: function (obj) {
var self = this;
new _Object(obj)._each(function (key, value) {
self[key] = value;
})
}
};
var isNumber = function (value) {
return (typeof value === 'number');
};
var isString = function (value) {
return (typeof value === 'string');
};
var isBoolean = function (value) {
return (typeof value === 'boolean');
};
var isObject = function (value) {
return (typeof value === 'object' && value !== null);
};
var isFunction = function (value) {
return (typeof value === 'function');
};
var MOBILE = /Mobi/i.test(navigator.userAgent);
var pt = MOBILE ? 3 : 0;
var callbacks = {};
var status = {};
var nowDate = function () {
var date = new Date();
var year = date.getFullYear();
var month = date.getMonth() + 1;
var day = date.getDate();
var hours = date.getHours();
var minutes = date.getMinutes();
var seconds = date.getSeconds();
if (month >= 1 && month <= 9) {
month = '0' + month;
}
if (day >= 0 && day <= 9) {
day = '0' + day;
}
if (hours >= 0 && hours <= 9) {
hours = '0' + hours;
}
if (minutes >= 0 && minutes <= 9) {
minutes = '0' + minutes;
}
if (seconds >= 0 && seconds <= 9) {
seconds = '0' + seconds;
}
var currentdate = year + '-' + month + '-' + day + " " + hours + ":" + minutes + ":" + seconds;
return currentdate;
}
var random = function () {
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
};
var loadScript = function (url, cb) {
var script = document.createElement("script");
script.charset = "UTF-8";
script.async = true;
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
if ( /static\.geetest\.com/g.test(url)) {
script.crossOrigin = "anonymous";
}
script.onerror = function () {
cb(true);
};
var loaded = false;
script.onload = script.onreadystatechange = function () {
if (!loaded &&
(!script.readyState ||
"loaded" === script.readyState ||
"complete" === script.readyState)) {
loaded = true;
setTimeout(function () {
cb(false);
}, 0);
}
};
script.src = url;
head.appendChild(script);
};
var normalizeDomain = function (domain) {
// special domain: uems.sysu.edu.cn/jwxt/geetest/
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
};
var normalizePath = function (path) {
path = path.replace(/\/+/g, '/');
if (path.indexOf('/') !== 0) {
path = '/' + path;
}
return path;
};
var normalizeQuery = function (query) {
if (!query) {
return '';
}
var q = '?';
new _Object(query)._each(function (key, value) {
if (isString(value) || isNumber(value) || isBoolean(value)) {
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
}
});
if (q === '?') {
q = '';
}
return q.replace(/&$/, '');
};
var makeURL = function (protocol, domain, path, query) {
domain = normalizeDomain(domain);
var url = normalizePath(path) + normalizeQuery(query);
if (domain) {
url = protocol + domain + url;
}
return url;
};
var load = function (config, send, protocol, domains, path, query, cb) {
var tryRequest = function (at) {
var url = makeURL(protocol, domains[at], path, query);
loadScript(url, function (err) {
if (err) {
if (at >= domains.length - 1) {
cb(true);
// report gettype error
if (send) {
config.error_code = 508;
var url = protocol + domains[at] + path;
reportError(config, url);
}
} else {
tryRequest(at + 1);
}
} else {
cb(false);
}
});
};
tryRequest(0);
};
var jsonp = function (domains, path, config, callback) {
if (isObject(config.getLib)) {
config._extend(config.getLib);
callback(config);
return;
}
if (config.offline) {
callback(config._get_fallback_config());
return;
}
var cb = "geetest_" + random();
window[cb] = function (data) {
if (data.status == 'success') {
callback(data.data);
} else if (!data.status) {
callback(data);
} else {
callback(config._get_fallback_config());
}
window[cb] = undefined;
try {
delete window[cb];
} catch (e) {
}
};
load(config, true, config.protocol, domains, path, {
gt: config.gt,
callback: cb
}, function (err) {
if (err) {
callback(config._get_fallback_config());
}
});
};
var reportError = function (config, url) {
load(config, false, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
time: nowDate(),
captcha_id: config.gt,
challenge: config.challenge,
pt: pt,
exception_url: url,
error_code: config.error_code
}, function (err) {})
}
var throwError = function (errorType, config) {
var errors = {
networkError: '网络错误',
gtTypeError: 'gt字段不是字符串类型'
};
if (typeof config.onError === 'function') {
config.onError(errors[errorType]);
} else {
throw new Error(errors[errorType]);
}
};
var detect = function () {
return window.Geetest || document.getElementById("gt_lib");
};
if (detect()) {
status.slide = "loaded";
}
window.initGeetest = function (userConfig, callback) {
var config = new Config(userConfig);
if (userConfig.https) {
config.protocol = 'https://';
} else if (!userConfig.protocol) {
config.protocol = window.location.protocol + '//';
}
// for KFC
if (userConfig.gt === '050cffef4ae57b5d5e529fea9540b0d1' ||
userConfig.gt === '3bd38408ae4af923ed36e13819b14d42') {
config.apiserver = 'yumchina.geetest.com/'; // for old js
config.api_server = 'yumchina.geetest.com';
}
if(userConfig.gt){
window.GeeGT = userConfig.gt
}
if(userConfig.challenge){
window.GeeChallenge = userConfig.challenge
}
if (isObject(userConfig.getType)) {
config._extend(userConfig.getType);
}
jsonp((config.api_server_v3 || [config.api_server || config.apiserver]), config.typePath, config, function (newConfig) {
var type = newConfig.type;
var init = function () {
config._extend(newConfig);
callback(new window.Geetest(config));
};
callbacks[type] = callbacks[type] || [];
var s = status[type] || 'init';
if (s === 'init') {
status[type] = 'loading';
callbacks[type].push(init);
load(config, true, config.protocol, newConfig.static_servers || newConfig.domains, newConfig[type] || newConfig.path, null, function (err) {
if (err) {
status[type] = 'fail';
throwError('networkError', config);
} else {
status[type] = 'loaded';
var cbs = callbacks[type];
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
var cb = cbs[i];
if (isFunction(cb)) {
cb();
}
}
callbacks[type] = [];
}
});
} else if (s === "loaded") {
init();
} else if (s === "fail") {
throwError('networkError', config);
} else if (s === "loading") {
callbacks[type].push(init);
}
});
};
})(window);
+487
View File
@@ -0,0 +1,487 @@
"v4.2.0 Geetest Inc.";
(function (window) {
"use strict";
if (typeof window === 'undefined') {
throw new Error('Geetest requires browser environment');
}
var document = window.document;
var Math = window.Math;
var head = document.getElementsByTagName("head")[0];
var TIMEOUT = 10000;
function _Object(obj) {
this._obj = obj;
}
_Object.prototype = {
_each: function (process) {
var _obj = this._obj;
for (var k in _obj) {
if (_obj.hasOwnProperty(k)) {
process(k, _obj[k]);
}
}
return this;
},
_extend: function (obj){
var self = this;
new _Object(obj)._each(function (key, value){
self._obj[key] = value;
})
}
};
var uuid = function () {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0;
var v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
function Config(config) {
var self = this;
new _Object(config)._each(function (key, value) {
self[key] = value;
});
}
Config.prototype = {
apiServers: ['gcaptcha4.geetest.com','gcaptcha4.geevisit.com','gcaptcha4.gsensebot.com'],
staticServers: ["static.geetest.com",'static.geevisit.com'],
protocol: 'http://',
typePath: '/load',
fallback_config: {
bypass: {
staticServers: ["static.geetest.com",'static.geevisit.com'],
type: 'bypass',
bypass: '/v4/bypass.js'
}
},
_get_fallback_config: function () {
var self = this;
if (isString(self.type)) {
return self.fallback_config[self.type];
} else {
return self.fallback_config.bypass;
}
},
_extend: function (obj) {
var self = this;
new _Object(obj)._each(function (key, value) {
self[key] = value;
})
}
};
var isNumber = function (value) {
return (typeof value === 'number');
};
var isString = function (value) {
return (typeof value === 'string');
};
var isBoolean = function (value) {
return (typeof value === 'boolean');
};
var isObject = function (value) {
return (typeof value === 'object' && value !== null);
};
var isFunction = function (value) {
return (typeof value === 'function');
};
var MOBILE = /Mobi/i.test(navigator.userAgent);
var callbacks = {};
var status = {};
var random = function () {
return parseInt(Math.random() * 10000) + (new Date()).valueOf();
};
// bind 函数polify, 不带new功能的bind
var bind = function(target,context){
if(typeof target !== 'function'){
return;
}
var args = Array.prototype.slice.call(arguments,2);
if(Function.prototype.bind){
return target.bind(context, args);
}else {
return function(){
var _args = Array.prototype.slice.call(arguments);
return target.apply(context,args.concat(_args));
}
}
}
var toString = Object.prototype.toString;
var _isFunction = function(obj) {
return typeof(obj) === 'function';
};
var _isObject = function(obj) {
return obj === Object(obj);
};
var _isArray = function(obj) {
return toString.call(obj) == '[object Array]';
};
var _isDate = function(obj) {
return toString.call(obj) == '[object Date]';
};
var _isRegExp = function(obj) {
return toString.call(obj) == '[object RegExp]';
};
var _isBoolean = function(obj) {
return toString.call(obj) == '[object Boolean]';
};
function resolveKey(input){
return input.replace(/(\S)(_([a-zA-Z]))/g, function(match, $1, $2, $3){
return $1 + $3.toUpperCase() || "";
})
}
function camelizeKeys(input, convert){
if(!_isObject(input) || _isDate(input) || _isRegExp(input) || _isBoolean(input) || _isFunction(input)){
return convert ? resolveKey(input) : input;
}
if(_isArray(input)){
var temp = [];
for(var i = 0; i < input.length; i++){
temp.push(camelizeKeys(input[i]));
}
}else {
var temp = {};
for(var prop in input){
if(input.hasOwnProperty(prop)){
temp[camelizeKeys(prop, true)] = camelizeKeys(input[prop]);
}
}
}
return temp;
}
var loadScript = function (url, cb, timeout) {
var script = document.createElement("script");
script.charset = "UTF-8";
script.async = true;
// 对geetestçš„é™æ€èµ„æºæ·»åŠ crossOrigin
if ( /static\.geetest\.com/g.test(url)) {
script.crossOrigin = "anonymous";
}
script.onerror = function () {
cb(true);
// 错误触发了,超时逻辑就不用了
loaded = true;
};
var loaded = false;
script.onload = script.onreadystatechange = function () {
if (!loaded &&
(!script.readyState ||
"loaded" === script.readyState ||
"complete" === script.readyState)) {
loaded = true;
setTimeout(function () {
cb(false);
}, 0);
}
};
script.src = url;
head.appendChild(script);
setTimeout(function () {
if (!loaded) {
script.onerror = script.onload = null;
script.remove && script.remove();
cb(true);
}
}, timeout || TIMEOUT);
};
var normalizeDomain = function (domain) {
// special domain: uems.sysu.edu.cn/jwxt/geetest/
// return domain.replace(/^https?:\/\/|\/.*$/g, ''); uems.sysu.edu.cn
return domain.replace(/^https?:\/\/|\/$/g, ''); // uems.sysu.edu.cn/jwxt/geetest
};
var normalizePath = function (path) {
path = path && path.replace(/\/+/g, '/');
if (path.indexOf('/') !== 0) {
path = '/' + path;
}
return path;
};
var normalizeQuery = function (query) {
if (!query) {
return '';
}
var q = '?';
new _Object(query)._each(function (key, value) {
if (isString(value) || isNumber(value) || isBoolean(value)) {
q = q + encodeURIComponent(key) + '=' + encodeURIComponent(value) + '&';
}
});
if (q === '?') {
q = '';
}
return q.replace(/&$/, '');
};
var makeURL = function (protocol, domain, path, query) {
domain = normalizeDomain(domain);
var url = normalizePath(path) + normalizeQuery(query);
if (domain) {
url = protocol + domain + url;
}
return url;
};
var load = function (config, protocol, domains, path, query, cb, handleCb) {
var tryRequest = function (at) {
// 处理jsonp回调,这里为了保证每个不同jsonp都有唯一的回调函数
if(handleCb){
var cbName = "geetest_" + random();
// 需要与预先定义好cbnameå‚æ•°ï¼Œåˆ é™¤å¯¹è±¡
window[cbName] = bind(handleCb, null, cbName);
query.callback = cbName;
}
var url = makeURL(protocol, domains[at], path, query);
loadScript(url, function (err) {
if (err) {
// 超时或者出错的时候 移除回调
if(cbName){
try {
window[cbName] = function(){
window[cbName] = null;
}
} catch (e) {}
}
if (at >= domains.length - 1) {
cb(true);
// report gettype error
} else {
tryRequest(at + 1);
}
} else {
cb(false);
}
}, config.timeout);
};
tryRequest(0);
};
var jsonp = function (domains, path, config, callback) {
var handleCb = function (cbName, data) {
// 保证只执行一次,全部超时的情况下不会再触发;
if (data.status == 'success') {
callback(data.data);
} else if (!data.status) {
callback(data);
} else {
//接口有返回,但是返回了错误状态,进入报错逻辑
callback(data);
}
window[cbName] = undefined;
try {
delete window[cbName];
} catch (e) {
}
};
load(config, config.protocol, domains, path, {
callback: '',
captcha_id: config.captchaId,
challenge: config.challenge || uuid(),
client_type: config.clientType ? config.clientType : (MOBILE? 'h5':'web'),
risk_type: config.riskType,
user_info: config.userInfo,
call_type: config.callType,
lang: config.language? config.language : navigator.appName === 'Netscape' ? navigator.language.toLowerCase() : navigator.userLanguage.toLowerCase()
}, function (err) {
// ç½‘ç»œé—®é¢˜æŽ¥å£æ²¡æœ‰è¿”å›žï¼Œç›´æŽ¥ä½¿ç”¨æœ¬åœ°éªŒè¯ç ï¼Œèµ°å®•æœºæ¨¡å¼
// è¿™é‡Œå¯ä»¥æ·»åŠ ç”¨æˆ·çš„é€»è¾‘
if(err && typeof config.offlineCb === 'function'){
// 执行自己的宕机
config.offlineCb();
return;
}
if(err){
callback(config._get_fallback_config());
}
}, handleCb);
};
var reportError = function (config, url) {
load(config, config.protocol, ['monitor.geetest.com'], '/monitor/send', {
time: Date.now().getTime(),
captcha_id: config.gt,
challenge: config.challenge,
exception_url: url,
error_code: config.error_code
}, function (err) {})
}
var throwError = function (errorType, config, errObj) {
var errors = {
networkError: '网络错误',
gtTypeError: 'gt字段不是字符串类型'
};
if (typeof config.onError === 'function') {
config.onError({
desc: errObj.desc,
msg: errObj.msg,
code: errObj.code
});
} else {
throw new Error(errors[errorType]);
}
};
var detect = function () {
return window.Geetest || document.getElementById("gt_lib");
};
if (detect()) {
status.slide = "loaded";
}
var GeetestIsLoad = function (fname) {
var GeetestIsLoad = false;
var tags = { js: 'script', css: 'link' };
var tagname = fname && tags[fname.split('.').pop()];
if (tagname !== undefined) {
var elts = document.getElementsByTagName(tagname);
for (var i in elts) {
if ((elts[i].href && elts[i].href.toString().indexOf(fname) > 0)
|| (elts[i].src && elts[i].src.toString().indexOf(fname) > 0)) {
GeetestIsLoad = true;
}
}
}
return GeetestIsLoad;
};
window.initGeetest4 = function (userConfig,callback) {
var config = new Config(userConfig);
if (userConfig.https) {
config.protocol = 'https://';
} else if (!userConfig.protocol) {
config.protocol = window.location.protocol + '//';
}
if (isObject(userConfig.getType)) {
config._extend(userConfig.getType);
}
jsonp(config.apiServers , config.typePath, config, function (newConfig) {
//错误捕获,第一个load请求可能直接报错
var newConfig = camelizeKeys(newConfig);
if(newConfig.status === 'error'){
return throwError('networkError', config, newConfig);
}
var type = newConfig.type;
if(config.debug){
new _Object(newConfig)._extend(config.debug)
}
var init = function () {
config._extend(newConfig);
callback(new window.Geetest4(config));
};
callbacks[type] = callbacks[type] || [];
var s = status[type] || 'init';
if (s === 'init') {
status[type] = 'loading';
callbacks[type].push(init);
if(newConfig.gctPath){
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
if(err){
throwError('networkError', config, {
code: '60205',
msg: 'Network failure',
desc: {
detail: 'gct resource load timeout'
}
});
}
})
}
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers, newConfig.bypass || (newConfig.staticPath + newConfig.js), null, function (err) {
if (err) {
status[type] = 'fail';
throwError('networkError', config, {
code: '60204',
msg: 'Network failure',
desc: {
detail: 'js resource load timeout'
}
});
} else {
status[type] = 'loaded';
var cbs = callbacks[type];
for (var i = 0, len = cbs.length; i < len; i = i + 1) {
var cb = cbs[i];
if (isFunction(cb)) {
cb();
}
}
callbacks[type] = [];
status[type] = 'init';
}
});
} else if (s === "loaded") {
// 判断gctæ˜¯å¦éœ€è¦é‡æ–°åŠ è½½
if(newConfig.gctPath && !GeetestIsLoad(newConfig.gctPath)){
load(config, config.protocol, Object.hasOwnProperty.call(config, 'staticServers') ? config.staticServers : newConfig.staticServers || config.staticServers , newConfig.gctPath, null, function (err){
if(err){
throwError('networkError', config, {
code: '60205',
msg: 'Network failure',
desc: {
detail: 'gct resource load timeout'
}
});
}
})
}
return init();
} else if (s === "fail") {
throwError('networkError', config, {
code: '60204',
msg: 'Network failure',
desc: {
detail: 'js resource load timeout'
}
});
} else if (s === "loading") {
callbacks[type].push(init);
}
});
};
})(window);
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0 0 400 400" fill="none"><path d="M77.08 90L15 90L89.64 239.51L89.64 310L145.07 310L145.07 239.51L196.406 137.69L275.435 137.69L203.574 276.92L203.574 310L386.005 310L386.005 262.31L272.464 262.31L345.085 123.08L345.085 90L157.64 90L116.99 180.63L77.08 90Z" fill-rule="evenodd" fill="#FFFFFF" ></path></svg>

After

Width:  |  Height:  |  Size: 425 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" height="400" viewBox="0 0 400 400" fill="none"><path d="M77.08 90L15 90L89.64 239.51L89.64 310L145.07 310L145.07 239.51L196.406 137.69L275.435 137.69L203.574 276.92L203.574 310L386.005 310L386.005 262.31L272.464 262.31L345.085 123.08L345.085 90L157.64 90L116.99 180.63L77.08 90Z" fill-rule="evenodd" fill="#000000" ></path></svg>

After

Width:  |  Height:  |  Size: 425 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

+21 -3
View File
@@ -27,7 +27,7 @@
:default-active="route.path"
>
<!-- 菜单标题 -->
<h3>{{ isCollapse ? "管理" : currentModule?.title || "子菜单" }}</h3>
<h3>{{ isCollapse ? "管理" : asideTitle }}</h3>
<!-- 无模块时显示提示 -->
<el-menu-item v-if="!currentModule" index="/home">
@@ -248,12 +248,30 @@ const displayMenus = computed(() => {
if (!currentModule.value) {
return [];
}
const currentPath = route.path;
// 访问“父级 index 页面”时不展开子菜单;只有进入“子页面”后才展示 children。
// 例如:/apps/cms/products(父级)时不展示其 children(如 /apps/cms/products/types)。
if (currentPath === currentModule.value.path) {
return [];
}
return currentModule.value.children || [];
});
const asideTitle = computed(() => {
if (isCollapse.value) return "管理";
if (!currentModule.value) return "子菜单";
return displayMenus.value.length > 0 ? (currentModule.value.title || "子菜单") : "子菜单";
});
const processMenus = (menus) => {
return menus
.filter((menu) => {
// is_visible 控制“侧边栏是否展示”,不参与动态路由与缓存层面的过滤
if (menu.is_visible !== undefined && Number(menu.is_visible) === 0) {
return false;
}
if (menu.path && menu.path.trim() !== "") return true;
if (menu.children && menu.children.length > 0) return true;
return false;
@@ -445,7 +463,7 @@ h3 {
background-color: rgba(60, 60, 60, 0.8) !important;
}
color: #ffffff !important;
border-left: 3px solid #4f84ff;
/* border-left: 3px solid #4f84ff; */
margin-left: -3px;
.menu-icon {
@@ -486,7 +504,7 @@ h3 {
.el-menu-item.is-active {
background: rgba(219, 148, 148, 0.8) !important;
color: var(--el-color-primary-light-3) !important;
border-left-color: var(--el-color-primary);
/* border-left-color: var(--el-color-primary); */
.menu-icon {
color: var(--el-color-primary);
+4 -11
View File
@@ -4,15 +4,6 @@
<el-button size="small" @click="handleCollapse">
<i class="fa fa-bars"></i>
</el-button>
<!-- <el-breadcrumb separator="/" class="bread">
<el-breadcrumb-item :to="{ path: '/' }"
style="position: relative !important; top: 1px !important;">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="index"
:to="(item.label === '首页' || index === breadcrumbs.length - 1) ? { path: item.path } : null"
style="position: relative !important; top: 1px !important;">
{{ item.label }}
</el-breadcrumb-item>
</el-breadcrumb> -->
</div>
<div class="r-content">
<!-- 返回首页按钮 -->
@@ -223,6 +214,8 @@ const handleCommand = async (command) => {
localStorage.removeItem('tabs_list');
localStorage.removeItem('active_tab');
sessionStorage.removeItem('tabs_list');
localStorage.removeItem('tabs_list');
// 清除菜单缓存
menuStore.resetMenus();
@@ -231,7 +224,7 @@ const handleCommand = async (command) => {
// 遍历 localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith('menu_cache_')) {
if (key && key.startsWith('menu_')) {
menuCacheKeys.push(key);
}
}
@@ -244,7 +237,7 @@ const handleCommand = async (command) => {
const sessionMenuCacheKeys: string[] = [];
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key && key.startsWith('menu_cache_')) {
if (key && key.startsWith('menu_')) {
sessionMenuCacheKeys.push(key);
}
}
+47
View File
@@ -0,0 +1,47 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
declare module '@/*' {
import type { ComponentOptions } from 'vue';
const component: ComponentOptions;
export default component;
}
declare module '@/api/erp' {
export function getOrganizationList(): Promise<any>;
export function getOrganizationDetail(id: number | string): Promise<any>;
export function createOrganization(data: any): Promise<any>;
export function editOrganization(id: number | string, data: any): Promise<any>;
export function deleteOrganization(id: number | string): Promise<any>;
export function getCompanys(): Promise<any>;
export function getDepartments(parentId?: number | string): Promise<any>;
export function getEmployeeList(tenantId?: number | string): Promise<any>;
export function getEmployeeDetail(id: number | string): Promise<any>;
export function createEmployee(data: any): Promise<any>;
export function editEmployee(id: number | string, data: any): Promise<any>;
export function deleteEmployee(id: number | string): Promise<any>;
}
declare module '@/stores/auth' {
export function useAuthStore(): any;
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
// 添加其他环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
declare module 'vue-cropper' {
import { DefineComponent } from 'vue';
const VueCropper: DefineComponent<{}, {}, any>;
export default VueCropper;
}
+13 -3
View File
@@ -7,6 +7,7 @@ import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import '@/assets/less/index.less'
import '@/assets/css/all.min.css'
import '@/assets/js/all.min.js'
import router from './router'
import { loadAndAddDynamicRoutes } from './router'
import { createPinia } from 'pinia'
@@ -36,9 +37,18 @@ authStore.checkAuth()
// 如果用户已登录,在应用启动时加载动态路由
if (authStore.isLoggedIn) {
loadAndAddDynamicRoutes().catch(err => {
console.error('应用启动时加载动态路由失败:', err);
});
loadAndAddDynamicRoutes()
.catch(err => {
console.error('应用启动时加载动态路由失败:', err);
})
.then(() => {
// 检查是否因为 token 无效而导致路由加载失败
const token = localStorage.getItem('token');
if (!token) {
authStore.clearToken();
window.location.href = '#/login';
}
});
}
app.mount('#app')
+154 -35
View File
@@ -1,55 +1,174 @@
import { createComponentLoader } from '@/utils/pathResolver';
import { h, resolveComponent as resolveVueComponent } from 'vue';
function computeFullPath(menuPath, parentPath) {
if (!menuPath) return parentPath || '';
if (menuPath.startsWith('/')) {
return menuPath.replace(/\/+/g, '/');
}
const base = (parentPath || '').replace(/\/$/, '');
return `${base}/${menuPath}`.replace(/\/+/g, '/');
}
/** 将子路由的绝对路径转为相对父布局的路径,供 Vue Router 嵌套使用 */
function toRelativeChildPath(parentAbs, childAbs) {
const base = (parentAbs || '').replace(/\/$/, '');
const target = (childAbs || '').replace(/\/$/, '');
if (!target) return '';
if (target === base) return '';
const prefix = `${base}/`;
if (target.startsWith(prefix)) {
return target.slice(prefix.length);
}
// 兜底:取最后一段(菜单 path 配置异常时)
const parts = target.split('/').filter(Boolean);
return parts.length ? parts[parts.length - 1] : '';
}
function hasPageComponent(menu) {
return menu.type === 4 || (menu.component_path && String(menu.component_path).trim() !== '');
}
function resolvePageComponent(menu) {
if (menu.type === 4) {
return () => import('@/views/onepage/index.vue');
}
if (menu.component_path && String(menu.component_path).trim() !== '') {
return createComponentLoader(menu.component_path);
}
return () => import('@/views/404/404.vue');
}
/**
* 菜单子节点 -> 嵌套路由path 相对 layoutAbsPath
*/
function convertNestedMenuChildren(children, layoutAbsPath) {
if (!children || children.length === 0) return [];
return children.map((child) => nestedMenuToRoute(child, layoutAbsPath));
}
function nestedMenuToRoute(menu, layoutAbsPath) {
const childAbs = computeFullPath(menu.path, layoutAbsPath);
const relPath = toRelativeChildPath(layoutAbsPath, childAbs);
const hasChildren = menu.children && menu.children.length > 0;
const ownPage = hasPageComponent(menu);
const meta = {
title: menu.title,
icon: menu.icon,
id: menu.id,
componentPath: menu.component_path,
};
// 既有自己的页面又有子菜单:套一层 EmptyLayout,避免父页面组件里没有 <router-view> 导致子路由无法渲染
if (hasChildren && ownPage) {
return {
path: relPath,
name: `menu_${menu.id}`,
meta,
component: () => import('@/views/layouts/EmptyLayout.vue'),
children: [
{
path: '',
name: `menu_${menu.id}_index`,
meta: { ...meta },
component: resolvePageComponent(menu),
},
...convertNestedMenuChildren(menu.children, childAbs),
],
};
}
// 纯目录 + 子节点
if (hasChildren && !ownPage) {
const route = {
path: relPath,
name: `menu_${menu.id}`,
meta,
component: () => import('@/views/layouts/EmptyLayout.vue'),
children: convertNestedMenuChildren(menu.children, childAbs),
};
const firstChild = menu.children[0];
if (firstChild && firstChild.path) {
const firstAbs = computeFullPath(firstChild.path, childAbs);
const firstRel = toRelativeChildPath(childAbs, firstAbs);
if (firstRel) {
route.redirect = firstRel;
}
}
return route;
}
// 叶子页面
return {
path: relPath,
name: `menu_${menu.id}`,
meta,
component: resolvePageComponent(menu),
};
}
// 递归转换嵌套菜单为嵌套路由
export function convertMenusToRoutes(menus, parentPath = '') {
if (!menus || menus.length === 0) return [];
return menus.map(menu => {
// 拼接完整的路由路径(处理相对路径)
const fullPath = menu.path ?
(menu.path.startsWith('/') ? menu.path : `${parentPath}/${menu.path}`)
return menus.map((menu) => {
const fullPath = menu.path
? menu.path.startsWith('/')
? menu.path.replace(/\/+/g, '/')
: `${(parentPath || '').replace(/\/$/, '')}/${menu.path}`.replace(/\/+/g, '/')
: '';
const route = {
path: fullPath || menu.path || '',
name: `menu_${menu.id}`,
meta: {
title: menu.title,
icon: menu.icon,
id: menu.id,
componentPath: menu.component_path
}
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,
};
// 1. 处理组件加载
if (menu.type === 4) {
// 单页类型
route.component = () => import('@/views/onepage/index.vue');
} else if (menu.component_path && menu.component_path.trim() !== '') {
// 正常页面
route.component = createComponentLoader(menu.component_path);
} else if (menu.children && menu.children.length > 0) {
// 目录节点:必须给组件,否则父级无法渲染子级
route.component = () => import('@/views/layouts/EmptyLayout.vue');
} else {
// 异常:既没路径也没子菜单
route.component = () => import('@/views/404/404.vue');
// 顶层:有页面 + 有子菜单 -> 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),
],
};
}
// 2. 递归子路由(传递当前完整路径作为父路径)
if (menu.children && menu.children.length > 0) {
const route = {
path: fullPath || menu.path || '',
name: `menu_${menu.id}`,
meta,
};
if (menu.type === 4) {
route.component = () => import('@/views/onepage/index.vue');
} else if (menu.component_path && menu.component_path.trim() !== '') {
route.component = createComponentLoader(menu.component_path);
} else if (hasChildren) {
route.component = () => import('@/views/layouts/EmptyLayout.vue');
route.children = convertMenusToRoutes(menu.children, fullPath);
// 目录节点添加重定向,防止点击父菜单页面空白
const firstChild = menu.children[0];
if (firstChild && firstChild.path) {
// 计算第一个子路由的完整路径
const childFullPath = firstChild.path.startsWith('/') ?
firstChild.path :
`${fullPath}/${firstChild.path}`;
const childFullPath = firstChild.path.startsWith('/')
? firstChild.path
: `${fullPath}/${firstChild.path}`;
route.redirect = childFullPath;
}
} else {
route.component = () => import('@/views/404/404.vue');
}
return route;
+41 -22
View File
@@ -1,7 +1,22 @@
import { createRouter, createWebHashHistory } from "vue-router";
import { convertMenusToRoutes } from "./dynamicRoutes";
// 静态路由:登录页独立,404 页面独立,home 导航门户独立
// 静态路由:需要在 Main 框架内显示的页面
const staticMainChildren = [
{
path: "/user/userProfile",
name: "userProfile",
component: () => import("@/views/user/userProfile.vue"),
meta: { requiresAuth: true, title: "用户中心" }
},
// 兼容拼写错误的路径重定向
{
path: "/apps/erp/dashborad",
redirect: "/apps/erp/dashboard"
}
];
// 静态路由:登录页独立、home 导航门户独立、404 页面独立
const staticRoutes = [
{
path: "/login",
@@ -10,11 +25,16 @@ const staticRoutes = [
meta: { requiresAuth: false }
},
{
path: "/",
name: "Main",
component: () => import("@/views/Main.vue"),
redirect: "/dashboard",
meta: { requiresAuth: true }
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",
@@ -22,6 +42,11 @@ const staticRoutes = [
component: () => import("@/views/home/index.vue"),
meta: { requiresAuth: true, title: "系统导航", isStandalone: true }
},
// 兼容路径拼写错误:dashborad -> dashboard
{
path: "/apps/erp/dashborad",
redirect: "/apps/erp/dashboard"
},
{
path: "/:pathMatch(.*)*",
name: "NotFound",
@@ -48,17 +73,17 @@ export async function loadAndAddDynamicRoutes() {
if (routesLoadingPromise) {
return routesLoadingPromise;
}
if (dynamicRoutesAdded) {
return Promise.resolve();
}
routesLoadingPromise = (async () => {
try {
const { useMenuStore } = await import("@/stores/menu");
const menuStore = useMenuStore();
const menuData = await menuStore.fetchMenus();
if (menuData && menuData.length > 0) {
addDynamicRoutes(menuData);
dynamicRoutesAdded = true;
@@ -73,10 +98,10 @@ export async function loadAndAddDynamicRoutes() {
console.error('加载动态路由失败:', error);
dynamicRoutesAdded = true;
routesLoadingPromise = null;
return Promise.resolve();
throw error;
}
})();
return routesLoadingPromise;
}
@@ -93,18 +118,17 @@ function addDynamicRoutes(menus) {
router.removeRoute('Main');
}
// 重新添加主路由
// 重新添加主路由,合并静态子路由和动态路由
router.addRoute({
path: "/",
name: "Main",
component: () => import("@/views/Main.vue"),
redirect: "/dashboard",
meta: { requiresAuth: true },
children: dynamicRoutes // 嵌套路由直接加入 children
children: [...staticMainChildren, ...dynamicRoutes] // 合并静态和动态子路由
});
dynamicRoutesAdded = true;
// console.log('动态路由添加完成:', router.getRoutes().map(r => r.path));
}
function findRouteByName(routes, routeName) {
@@ -139,8 +163,9 @@ function findFirstValidRoute(routes) {
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem("token");
const publicPaths = ["/login", "/register", "/forget"];
if (to.path === "/login") {
if (publicPaths.includes(to.path)) {
if (token) {
if (!dynamicRoutesAdded) {
await loadAndAddDynamicRoutes();
@@ -159,17 +184,11 @@ router.beforeEach(async (to, from, next) => {
if (!dynamicRoutesAdded) {
await loadAndAddDynamicRoutes();
// 路由加载后重新导航确保路由匹配正确
// 路由加载后重新导航,确保路由匹配正确
next({ path: to.path, replace: true });
return;
}
// 如果匹配不到路由,跳转到首页导航门户
if (to.matched.length === 0) {
next({ path: "/home" });
return;
}
next();
});
+3
View File
@@ -7,6 +7,7 @@ const defaultUser = {
account: '',
name: '',
group_id: '',
type: 'backend',
avatar: ''
}
@@ -40,6 +41,8 @@ export const useAuthStore = defineStore('auth', () => {
account: userInfo.account || '',
name: userInfo.name || '',
group_id: userInfo.group_id || '',
type: 'backend',
tid: userInfo.tid || '',
avatar: userInfo.avatar || ''
}
-179
View File
@@ -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,
}
})
+11 -8
View File
@@ -21,7 +21,7 @@ export const useMenuStore = defineStore('menu', () => {
try {
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}');
const loginType = userInfo.type || 'user';
const roleId = userInfo.role || 0;
const roleId = userInfo.group_id || 0;
return `menu_cache_${loginType}_${roleId}`;
} catch (e) {
return 'menu_cache_default';
@@ -92,10 +92,6 @@ export const useMenuStore = defineStore('menu', () => {
const cachedMenus = loadFromCache();
if (cachedMenus && cachedMenus.length > 0) {
menus.value = cachedMenus;
// 注释掉后台更新缓存的逻辑,避免重复请求
// fetchMenus(true).catch(err => {
// console.warn('后台更新菜单失败:', err);
// });
return Promise.resolve(cachedMenus);
}
}
@@ -113,7 +109,7 @@ export const useMenuStore = defineStore('menu', () => {
try {
const userInfo = getUserInfo();
const loginType = userInfo.type || 'user';
const roleId = userInfo.role || 0;
const roleId = userInfo.group_id || 0;
let res;
@@ -174,7 +170,14 @@ export const useMenuStore = defineStore('menu', () => {
response: err.response,
stack: err.stack
});
// 如果是 token 无效错误,不使用缓存,直接抛出
if (err.message === 'token无效' || err.response?.status === 401) {
clearCache();
menus.value = [];
throw err;
}
// 如果出错,尝试使用缓存数据
const cachedMenus = loadFromCache();
if (cachedMenus && cachedMenus.length > 0) {
@@ -182,7 +185,7 @@ export const useMenuStore = defineStore('menu', () => {
menus.value = cachedMenus;
return cachedMenus;
}
// 如果连缓存都没有,设置空数组,避免页面崩溃
menus.value = [];
throw err;
View File
+4
View File
@@ -0,0 +1,4 @@
declare module 'vue-cropper' {
import { Component } from 'vue'
export const VueCropper: Component
}
+7 -2
View File
@@ -15,15 +15,20 @@ Object.keys(viewsModules).forEach(relativePath => {
// 统一去掉扩展名进行存储,方便各种格式匹配
const baseNoExt = relativePath.replace('../views/', '').replace('.vue', '');
const baseWithExt = relativePath.replace('../views/', '');
// 1. 存储标准路径
pathMap.set(relativePath, viewsModules[relativePath]);
// 2. 存储 @/views 路径
pathMap.set(relativePath.replace('../views', '@/views'), viewsModules[relativePath]);
// 3. 存储 /system/users 格式
// 3. 存储 /system/users 格式(不带扩展名)
pathMap.set(`/${baseNoExt}`, viewsModules[relativePath]);
// 4. 存储 system/users 格式
// 4. 存储 system/users 格式(不带扩展名)
pathMap.set(baseNoExt, viewsModules[relativePath]);
// 5. 存储 /system/users.vue 格式(带扩展名)
pathMap.set(`/${baseWithExt}`, viewsModules[relativePath]);
// 6. 存储 system/users.vue 格式(带扩展名)
pathMap.set(baseWithExt, viewsModules[relativePath]);
});
/**
+7 -22
View File
@@ -1,8 +1,11 @@
import axios from 'axios';
// 获取API基础URL,添加调试信息
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
baseURL: apiBaseURL,
timeout: 10000,
withCredentials: false // JWT 不需要 Cookie
});
@@ -14,23 +17,7 @@ service.interceptors.request.use(
if (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
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
@@ -53,16 +40,14 @@ service.interceptors.response.use(
if (error.response) {
switch (error.response.status) {
case 401:
// 处理未授权的错误 - 清除token并跳转登录页
console.error('未授权,请重新登录');
localStorage.removeItem('token');
// 避免循环导入,直接使用路由
localStorage.removeItem('userInfo');
if (window.location.hash !== '#/login') {
window.location.href = '#/login';
}
break;
return Promise.reject(new Error('token无效'));
case 404:
// 处理资源不存在的错误
console.error('请求的资源不存在');
break;
default:
+15 -6
View File
@@ -4,13 +4,22 @@
<div class="not-found-big">404</div>
<div class="not-found-message">页面未找到</div>
<div class="not-found-desc">很抱歉您访问的页面不存在或已被删除</div>
<router-link to="/" class="back-home-btn">返回首页</router-link>
<a href="javascript:void(0)" class="back-home-btn" @click.prevent="backToHome">
返回首页
</a>
</div>
</div>
</template>
<script setup>
//
<script lang="ts" setup>
import { useRouter } from "vue-router";
const router = useRouter();
const backToHome = () => {
// 使 /home Hash
router.push("/home");
};
</script>
<style scoped>
@@ -26,9 +35,9 @@
background: #fff;
padding: 60px 48px 40px;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0,0,0,0.10);
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
text-align: center;
animation: float-in 0.8s cubic-bezier(.7,.13,.45,.81) both;
animation: float-in 0.8s cubic-bezier(0.7, 0.13, 0.45, 0.81) both;
}
.not-found-big {
@@ -64,7 +73,7 @@
text-decoration: none;
font-weight: 500;
transition: background 0.2s;
box-shadow: 0 2px 8px rgba(252,92,125,0.07);
box-shadow: 0 2px 8px rgba(252, 92, 125, 0.07);
}
.back-home-btn:hover {
+1 -1
View File
@@ -132,7 +132,7 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
<style scoped lang="less">
.statistics-container {
padding: 20px;
min-height: 100vh;
+13 -16
View File
@@ -5,11 +5,8 @@
<el-card shadow="never" class="stat-card">
<div class="label">{{ card.label }}</div>
<div class="count">{{ card.count.toLocaleString() }}</div>
<div class="sub-info">
昨日
<span :class="card.trend >= 0 ? 'plus' : 'minus'">
{{ card.trend >= 0 ? "+" : "" }}{{ card.trend }}
</span>
<div class="sub-info" v-if="card.yesterday !== undefined">
昨日 {{ card.yesterday || 0 }}
</div>
</el-card>
</el-col>
@@ -72,7 +69,7 @@ import { getContentStats } from "@/api/analytics";
interface TopCard {
label: string;
count: number;
trend: number;
yesterday?: number;
}
interface HotArticle {
@@ -92,10 +89,10 @@ const barChart = shallowRef<echarts.ECharts | null>(null);
const categoryChart = shallowRef<echarts.ECharts | null>(null);
const topCards = ref<TopCard[]>([
{ label: "总发布量", count: 0, trend: 0 },
{ label: "本月新增", count: 0, trend: 0 },
{ label: "总点赞量", count: 0, trend: 0 },
{ label: "总访问量", count: 0, trend: 0 },
{ label: "总发布量", count: 0 },
{ label: "本月新增", count: 0 },
{ label: "总点赞量", count: 0 },
{ label: "总访问量", count: 0 },
]);
const hotContent = ref<HotArticle[]>([]);
@@ -103,13 +100,13 @@ const hotContent = ref<HotArticle[]>([]);
async function fetchContentStats() {
const res = await getContentStats();
if (res.code === 200 && res.data) {
const { overview, hot_articles } = res.data;
const { total_articles, month_articles, total_likes, total_views, hot_articles } = res.data;
topCards.value = [
{ label: "总发布量", count: overview.total_articles.value, trend: overview.total_articles.growth },
{ label: "本月新增", count: overview.month_new.value, trend: overview.month_new.growth },
{ label: "总点赞量", count: overview.total_likes.value, trend: overview.total_likes.growth },
{ label: "总访问量", count: overview.total_views.value, trend: overview.total_views.growth },
{ label: "总发布量", count: total_articles || 0 },
{ label: "本月新增", count: month_articles || 0 },
{ label: "总点赞量", count: total_likes || 0 },
{ label: "总访问量", count: total_views || 0 },
];
hotContent.value = hot_articles || [];
@@ -188,7 +185,7 @@ onUnmounted(() => {
});
</script>
<style scoped lang="scss">
<style scoped lang="less">
.content-stats-container {
min-height: 100vh;
+27 -2
View File
@@ -1,7 +1,23 @@
<template>
<div class="category-manager">
<!-- 页面头部 -->
<div class="page-header">
<div class="page-title">
<h3>文章分类</h3>
<p> {{ totalCount }} 个分类</p>
</div>
<div class="page-actions">
<el-button type="primary" :icon="Plus" @click="handleAdd">
新增分类
</el-button>
<el-button :icon="Refresh" @click="handleRefresh">刷新</el-button>
</div>
</div>
<div class="category-list" v-loading="loading">
<div v-if="filteredCategories.length === 0" class="empty-state"></div>
<div v-if="filteredCategories.length === 0" class="empty-state">
<el-empty description="暂无文章分类" />
</div>
<div v-else class="category-tree">
<category-node
@@ -53,6 +69,13 @@ const currentEdit = ref(null);
const searchText = ref("");
const categories = ref([]);
//
const addCategory = ref(null);
const handleAdd = () => {
addCategory.value = { parentId: 0 };
dialogVisible.value = true;
}
//
const totalCount = computed(() => categories.value.length);
@@ -173,9 +196,11 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.category-manager {
min-height: 100vh;
padding: 20px;
background-color: var(--el-bg-color-page);
//
.page-header {
@@ -99,7 +99,7 @@ const handleRowClick = () => {
};
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.category-node-wrapper {
.category-item {
border-bottom: 1px solid var(--el-border-color-lighter);
@@ -205,7 +205,7 @@ const cateOptions = ref([]);
const form = reactive({
title: "",
author: "云泽网",
author: "",
cate: "",
content: "",
image: "",
@@ -503,7 +503,7 @@ function resetForm() {
//
Object.assign(form, {
title: "",
author: "云泽网",
author: "",
cate: "",
content: "",
image: "",
@@ -525,7 +525,7 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.editor-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
@@ -118,7 +118,7 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.article-preview {
padding: 20px;
+2 -2
View File
@@ -8,7 +8,7 @@
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索文章标题/作者"
placeholder="搜索文章标题"
clearable
@clear="handleSearch"
>
@@ -630,7 +630,7 @@ onMounted(async () => {
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.cms-articles {
padding: 20px;
border-radius: 8px;
@@ -0,0 +1,150 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleClose"
>
<el-form :model="form" label-width="100px">
<el-form-item label="需求标题" required>
<el-input v-model="form.title" placeholder="请输入需求标题" />
</el-form-item>
<el-form-item label="需求描述" required>
<el-input
v-model="form.desc"
type="textarea"
rows="4"
placeholder="请输入需求描述"
/>
</el-form-item>
<el-form-item label="申请人">
<el-input v-model="form.applicant" placeholder="请输入申请人" />
</el-form-item>
<el-form-item label="电话">
<el-input v-model="form.phone" placeholder="请输入电话" />
</el-form-item>
<el-form-item label="需求状态">
<el-select v-model="form.status" placeholder="请选择状态">
<el-option label="待处理" :value="1" />
<el-option label="处理中" :value="2" />
<el-option label="已完成" :value="3" />
<el-option label="已拒绝" :value="4" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch } from "vue";
import { ElMessage } from "element-plus";
import { addDemand, editDemand } from "@/api/demand";
// Props
const props = defineProps({
visible: {
type: Boolean,
default: false,
},
title: {
type: String,
default: "新增需求",
},
demand: {
type: Object,
default: () => ({
id: "",
title: "",
desc: "",
applicant: "",
status: 1,
}),
},
});
// Emits
const emit = defineEmits(["close", "submit"]);
//
const dialogVisible = ref(props.visible);
const dialogTitle = ref(props.title);
//
const form = reactive({
id: "",
title: "",
desc: "",
applicant: "",
status: 1,
});
// props
watch(
() => props.visible,
(newVal) => {
dialogVisible.value = newVal;
},
);
watch(
() => props.title,
(newVal) => {
dialogTitle.value = newVal;
},
);
watch(
() => props.demand,
(newVal) => {
Object.assign(form, newVal);
},
{ deep: true },
);
//
const handleClose = () => {
emit("close");
};
//
const handleSubmit = async () => {
//
if (!form.title || !form.desc) {
ElMessage.warning("请填写必填项");
return;
}
try {
let result;
if (form.id) {
//
result = await editDemand(form.id, form);
} else {
//
result = await addDemand(form);
}
if (result.code === 200) {
ElMessage.success(form.id ? "编辑成功" : "新增成功");
emit("submit", { ...form });
} else {
ElMessage.error(result.msg || "操作失败");
}
} catch (error) {
ElMessage.error("网络错误,请稍后重试");
console.error("提交表单失败:", error);
}
};
</script>
<style lang="less" scoped>
.dialog-footer {
text-align: right;
}
</style>
+333
View File
@@ -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>
+185
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
<template></template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>
+287
View File
@@ -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支持 jpgpnggif 格式大小不超过 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>
+324
View File
@@ -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>
+6 -7
View File
@@ -1,8 +1,7 @@
<template>
<router-view />
</template>
<script setup></script>
<style lang="scss" scoped></style>
<router-view />
</template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>
@@ -0,0 +1,321 @@
<template>
<el-dialog
v-model="visible"
:title="title"
width="600px"
destroy-on-close
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="产品名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入产品名称" />
</el-form-item>
<el-form-item label="产品描述" prop="desc">
<el-input
v-model="formData.desc"
type="textarea"
:rows="5"
placeholder="请输入产品描述"
/>
</el-form-item>
<el-form-item label="产品内容" prop="content">
<div class="editor-container">
<WangEditor v-model="formData.content" style="height: 450px;" />
</div>
</el-form-item>
<el-form-item label="产品图片" prop="thumb">
<div class="flex-direction">
<el-upload
class="image-uploader"
:action="uploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleImageSuccess"
:on-error="handleImageError"
:before-upload="beforeImageUpload"
accept="image/*"
>
<img
v-if="formData.thumb"
:src="getImageUrl(formData.thumb)"
class="image-preview"
/>
<div v-else class="upload-placeholder">
<el-icon class="image-uploader-icon"><Plus /></el-icon>
<div class="el-upload__text">点击上传产品图片</div>
</div>
</el-upload>
<div
style="
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
"
>
建议尺寸200x200支持 jpgpnggif 格式大小不超过 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>
+295
View File
@@ -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>
+240
View File
@@ -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,3 @@
<template>这是cate</template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>
@@ -0,0 +1,3 @@
<template>这是list</template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>
@@ -0,0 +1,315 @@
<template>
<el-dialog
v-model="visible"
:title="title"
width="600px"
destroy-on-close
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="服务名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入服务名称" />
</el-form-item>
<el-form-item label="服务描述" prop="desc">
<el-input
v-model="formData.desc"
type="textarea"
:rows="5"
placeholder="请输入服务描述"
/>
</el-form-item>
<el-form-item label="Logo" prop="thumb">
<div class="flex-direction">
<el-upload
class="image-uploader"
:action="uploadUrl"
:headers="uploadHeaders"
:show-file-list="false"
:on-success="handleImageSuccess"
:on-error="handleImageError"
:before-upload="beforeImageUpload"
accept="image/*"
>
<img
v-if="formData.thumb"
:src="getImageUrl(formData.thumb)"
class="image-preview"
/>
<div v-else class="upload-placeholder">
<el-icon class="image-uploader-icon"><Plus /></el-icon>
<div class="el-upload__text">点击上传服务图标</div>
</div>
</el-upload>
<div
style="
margin-top: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
"
>
建议尺寸200x200支持 jpgpnggif 格式大小不超过 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>
+281
View File
@@ -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>
+281
View File
@@ -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>
+1 -1
View File
@@ -164,7 +164,7 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.exam-workbench {
padding: 20px;
+329
View File
@@ -0,0 +1,329 @@
<template>
<div class="erp-dashboard">
<!-- 第一行统计卡片 -->
<el-row :gutter="16" class="stat-row">
<el-col :span="4" v-for="(item, index) in firstRowStats" :key="'first-' + index">
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">{{ item.label }}</span>
<el-icon class="stat-icon"><Info-Filled /></el-icon>
</div>
<div class="stat-value">
<span class="currency">¥</span>
<span class="number">{{ formatMoney(item.value) }}</span>
</div>
</div>
</el-col>
</el-row>
<!-- 第二行统计卡片 -->
<el-row :gutter="16" class="stat-row" style="margin-top: 16px;">
<el-col :span="4" v-for="(item, index) in secondRowStats" :key="'second-' + index">
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">{{ item.label }}</span>
<el-icon class="stat-icon"><Info-Filled /></el-icon>
</div>
<div class="stat-value">
<span class="currency">¥</span>
<span class="number">{{ formatMoney(item.value) }}</span>
</div>
</div>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="16" class="chart-row" style="margin-top: 16px;">
<el-col :span="8">
<div class="chart-card">
<div class="chart-title">销售统计</div>
<div ref="salesChartRef" class="chart-container"></div>
</div>
</el-col>
<el-col :span="8">
<div class="chart-card">
<div class="chart-title">零售统计</div>
<div ref="retailChartRef" class="chart-container"></div>
</div>
</el-col>
<el-col :span="8">
<div class="chart-card">
<div class="chart-title">采购统计</div>
<div ref="purchaseChartRef" class="chart-container"></div>
</div>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, shallowRef, nextTick } from 'vue';
import * as echarts from 'echarts';
import { InfoFilled } from '@element-plus/icons-vue';
// -
const firstRowStats = ref([
{ label: '今日销售', value: 0 },
{ label: '今日零售', value: 0 },
{ label: '今日采购', value: 0 },
{ label: '本月累计销售', value: 0 },
{ label: '本月累计零售', value: 0 },
{ label: '本月累计采购', value: 0 },
]);
// -
const secondRowStats = ref([
{ label: '昨日销售', value: 0 },
{ label: '昨日零售', value: 0 },
{ label: '昨日采购', value: 0 },
{ label: '今年累计销售', value: 0 },
{ label: '今年累计零售', value: 0 },
{ label: '今年累计采购', value: 0 },
]);
//
const salesChartRef = ref<HTMLElement | null>(null);
const retailChartRef = ref<HTMLElement | null>(null);
const purchaseChartRef = ref<HTMLElement | null>(null);
const salesChartInstance = shallowRef<echarts.ECharts | null>(null);
const retailChartInstance = shallowRef<echarts.ECharts | null>(null);
const purchaseChartInstance = shallowRef<echarts.ECharts | null>(null);
//
const formatMoney = (value: number): string => {
return value.toLocaleString();
};
// 6
const getMonthLabels = (): string[] => {
const labels: string[] = [];
const now = new Date();
for (let i = 5; i >= 0; i--) {
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
labels.push(`${year}-${month}`);
}
return labels;
};
//
const initChart = (refEl: HTMLElement, title: string, color: string) => {
const chart = echarts.init(refEl);
const months = getMonthLabels();
chart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: months,
axisLine: { lineStyle: { color: '#E0E6ED' } },
axisLabel: { color: '#606266', fontSize: 11 }
},
yAxis: {
type: 'value',
axisLine: { show: false },
axisTick: { show: false },
splitLine: { lineStyle: { color: '#F2F6FC' } },
axisLabel: { color: '#606266', fontSize: 11 }
},
series: [
{
name: title,
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 6,
lineStyle: { color: color, width: 2 },
itemStyle: { color: color },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: color + '40' },
{ offset: 1, color: color + '10' }
])
},
data: [0, 0, 0, 0, 0, 0]
}
]
});
return chart;
};
//
const initCharts = () => {
if (salesChartRef.value) {
salesChartInstance.value = initChart(salesChartRef.value, '销售', '#409EFF');
}
if (retailChartRef.value) {
retailChartInstance.value = initChart(retailChartRef.value, '零售', '#67C23A');
}
if (purchaseChartRef.value) {
purchaseChartInstance.value = initChart(purchaseChartRef.value, '采购', '#E6A23C');
}
};
//
const loadData = async () => {
// TODO:
// const res = await getErpDashboard();
//
firstRowStats.value = [
{ label: '今日销售', value: 12580 },
{ label: '今日零售', value: 8560 },
{ label: '今日采购', value: 23400 },
{ label: '本月累计销售', value: 358600 },
{ label: '本月累计零售', value: 245800 },
{ label: '本月累计采购', value: 568900 },
];
secondRowStats.value = [
{ label: '昨日销售', value: 15230 },
{ label: '昨日零售', value: 9870 },
{ label: '昨日采购', value: 18900 },
{ label: '今年累计销售', value: 2856000 },
{ label: '今年累计零售', value: 1985000 },
{ label: '今年累计采购', value: 4568000 },
];
//
updateCharts();
};
//
const updateCharts = () => {
const salesData = [28000, 32000, 35000, 38000, 42000, 358600];
const retailData = [18000, 21000, 22000, 25000, 28000, 245800];
const purchaseData = [45000, 48000, 52000, 55000, 58000, 568900];
if (salesChartInstance.value) {
salesChartInstance.value.setOption({ series: [{ data: salesData }] });
}
if (retailChartInstance.value) {
retailChartInstance.value.setOption({ series: [{ data: retailData }] });
}
if (purchaseChartInstance.value) {
purchaseChartInstance.value.setOption({ series: [{ data: purchaseData }] });
}
};
//
const handleResize = () => {
salesChartInstance.value?.resize();
retailChartInstance.value?.resize();
purchaseChartInstance.value?.resize();
};
onMounted(async () => {
initCharts();
await nextTick();
await loadData();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
salesChartInstance.value?.dispose();
retailChartInstance.value?.dispose();
purchaseChartInstance.value?.dispose();
});
</script>
<style lang="less" scoped>
.erp-dashboard {
background-color: #f5f7fa;
min-height: calc(100vh - 84px);
}
.stat-row {
.el-col {
margin-bottom: 0;
}
}
.stat-card {
background: #fff;
border-radius: 4px;
padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
.stat-label {
font-size: 14px;
color: #606266;
}
.stat-icon {
font-size: 14px;
color: #c0c4cc;
cursor: help;
}
}
.stat-value {
display: flex;
align-items: baseline;
.currency {
font-size: 14px;
color: #303133;
font-weight: 500;
margin-right: 4px;
}
.number {
font-size: 24px;
font-weight: 600;
color: #303133;
font-family: 'Roboto', 'Helvetica Neue', Arial, sans-serif;
}
}
}
.chart-row {
.el-col {
margin-bottom: 0;
}
}
.chart-card {
background: #fff;
border-radius: 4px;
padding: 16px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
height: 320px;
display: flex;
flex-direction: column;
.chart-title {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #ebeef5;
}
.chart-container {
flex: 1;
min-height: 0;
}
}
</style>
@@ -0,0 +1,122 @@
<template>
<el-dialog v-model="visible" title="修改密码" width="420px" :close-on-click-modal="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="用户">
<span>{{ currentUser.name || currentUser.account || "-" }}</span>
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="请输入新密码"
show-password
clearable
/>
</el-form-item>
<el-form-item label="确认密码" prop="confirmPassword">
<el-input
v-model="form.confirmPassword"
type="password"
placeholder="请再次输入新密码"
show-password
clearable
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import { changePassword } from "@/api/user";
const emit = defineEmits<{
(e: "submit"): void;
}>();
const visible = ref(false);
const submitLoading = ref(false);
const formRef = ref<FormInstance>();
const currentUser = ref<any>({});
const form = reactive({
password: "",
confirmPassword: "",
});
const validateConfirmPassword = (_rule: any, value: string, callback: (error?: Error) => void) => {
if (!value) {
callback(new Error("请再次输入新密码"));
return;
}
if (value !== form.password) {
callback(new Error("两次输入的密码不一致"));
return;
}
callback();
};
const rules: FormRules = {
password: [
{ required: true, message: "请输入新密码", trigger: "blur" },
{ min: 6, message: "密码长度不能少于 6 位", trigger: "blur" },
],
confirmPassword: [
{ required: true, message: "请再次输入新密码", trigger: "blur" },
{ validator: validateConfirmPassword, trigger: "blur" },
],
};
const resetForm = () => {
form.password = "";
form.confirmPassword = "";
formRef.value?.clearValidate();
};
const openChangePass = (row: any) => {
currentUser.value = row || {};
resetForm();
visible.value = true;
};
const handleSubmit = async () => {
if (!currentUser.value?.id) {
ElMessage.error("用户 ID 不存在");
return;
}
await formRef.value?.validate();
submitLoading.value = true;
try {
await changePassword(currentUser.value.id, {
newPassword: form.password,
});
ElMessage.success("密码修改成功");
visible.value = false;
emit("submit");
} catch (e: any) {
ElMessage.error(e?.response?.data?.message || e?.message || "密码修改失败");
} finally {
submitLoading.value = false;
}
};
defineExpose({
openChangePass,
open: openChangePass,
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,583 @@
<template>
<el-drawer v-model="visible" :title="dialogTitle" width="500px">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<div class="form-title">账号信息</div>
<!-- 账号 -->
<el-form-item label="账号" required>
<el-input v-model="form.account" :disabled="!isAdd" placeholder="请输入账号" />
</el-form-item>
<!-- 密码 -->
<el-form-item label="密码" prop="password" v-if="isAdd" required>
<el-input
v-model="form.password"
type="password"
autocomplete="new-password"
show-password
:placeholder="isAdd ? '请输入密码(至少6位)' : '留空则不修改密码'"
/>
</el-form-item>
<!-- 确认密码 -->
<el-form-item label="确认密码" prop="confirmPassword" v-if="isAdd" required>
<el-input
v-model="form.confirmPassword"
type="password"
autocomplete="new-password"
show-password
:placeholder="isAdd ? '请再次输入密码' : '留空则不修改密码'"
/>
</el-form-item>
<el-divider></el-divider>
<div class="form-title">个人信息</div>
<!-- 姓名 -->
<el-form-item label="姓名" required>
<el-input v-model="form.name" placeholder="请输入姓名" />
</el-form-item>
<!-- 性别 -->
<el-form-item label="性别" required>
<el-radio-group v-model="form.sex" placeholder="请选择性别">
<el-radio-button label="未知" :value="0" />
<el-radio-button label="男" :value="1" />
<el-radio-button label="女" :value="2" />
</el-radio-group>
</el-form-item>
<!-- 生日 -->
<el-form-item label="生日" required>
<el-date-picker
v-model="form.birthday"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择生日"
/>
</el-form-item>
<!-- 隶属单位 -->
<el-form-item label="隶属单位" required>
<el-select v-model="form.affiliate_unit" placeholder="请选择隶属单位" clearable style="width: 100%">
<el-option v-for="item in companyList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
</el-select>
</el-form-item>
<!-- 部门 -->
<el-form-item label="部门" required>
<el-select v-model="form.department" placeholder="请先选择隶属单位" clearable style="width: 100%" :disabled="!form.affiliate_unit">
<el-option v-for="item in departmentList" :key="item.id" :label="item.org_name" :value="String(item.id)" />
</el-select>
</el-form-item>
<!-- 职位 -->
<el-form-item label="职位" required>
<el-input v-model="form.position" placeholder="请输入职位" />
</el-form-item>
<!-- 学历 -->
<el-form-item label="学历">
<el-input v-model="form.education" placeholder="请输入学历" />
</el-form-item>
<!-- 民族 -->
<el-form-item label="民族" required>
<el-input v-model="form.nation" placeholder="请输入民族" />
</el-form-item>
<!-- 手机 -->
<el-form-item label="手机" required>
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<!-- 微信 -->
<el-form-item label="微信">
<el-input v-model="form.wechat" placeholder="请输入微信" />
</el-form-item>
<!-- 邮箱 -->
<el-form-item label="邮箱" required>
<el-input v-model="form.email" placeholder="请输入邮箱" />
</el-form-item>
<!-- 家庭住址 -->
<el-form-item label="家庭住址" required>
<el-input v-model="form.home_address" placeholder="请输入家庭住址" />
</el-form-item>
<!-- 状态 -->
<el-form-item label="状态">
<el-select
v-model="form.account_status"
placeholder="请选择状态"
style="width: 100%"
>
<el-option label="离职" :value="2" v-if="!isAdd" />
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
</el-form>
<!-- 对话框脚部 -->
<template #footer>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSubmit">保存</el-button>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { ref, computed, watch, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { createEmployee, editEmployee, getEmployeeDetail, getCompanys, getDepartments } from "@/api/erp";
const props = defineProps({
modelValue: {
type: Boolean,
default: false,
},
isEdit: {
type: Boolean,
default: false,
},
statusDict: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue", "submit", "close"]);
const visible = ref(false);
const formRef = ref<any>(null);
const isAdd = ref(false);
const companyList = ref<any[]>([]);
const departmentList = ref<any[]>([]);
const isHydratingUserData = ref(false);
//
const loadCompanyList = async () => {
try {
const res = await getCompanys();
companyList.value = res.data || res || [];
} catch (e) {
console.error("获取公司列表失败", e);
}
};
//
const loadDepartments = async (parentId?: number) => {
try {
const res = await getDepartments(parentId);
departmentList.value = res.data || res || [];
} catch (e) {
console.error("获取部门列表失败", e);
}
};
const form = ref<any>({
id: null,
tid: 1,
tenant_id: 1,
account: "",
name: "",
phone: "",
qq: "",
sex: 0,
birthday: "",
affiliate_unit: "",
department: "",
position: "",
education: "",
nation: "",
wechat: "",
home_address: "",
password: "",
confirmPassword: "",
email: "",
account_status: 1,
status: 1,
});
//
watch(() => form.value.affiliate_unit, (newVal) => {
if (isHydratingUserData.value) {
return;
}
form.value.department = "";
departmentList.value = [];
if (newVal) {
loadDepartments(Number(newVal));
}
});
const dialogTitle = computed(() => {
return isAdd.value ? "添加用户" : "编辑用户";
});
//
const validatePassword = (rule: any, value: any, callback: any) => {
if (isAdd.value) {
//
if (!value) {
callback(new Error("请输入密码"));
return;
}
if (value.length < 6) {
callback(new Error("密码长度不能少于6位"));
return;
}
} else {
//
if (value && value.length < 6) {
callback(new Error("密码长度不能少于6位"));
return;
}
}
callback();
};
//
const validateConfirmPassword = (rule: any, value: any, callback: any) => {
if (isAdd.value) {
//
if (!value) {
callback(new Error("请再次输入密码"));
return;
}
if (value !== form.value.password) {
callback(new Error("两次输入的密码不一致"));
return;
}
} else {
//
if (form.value.password && value !== form.value.password) {
callback(new Error("两次输入的密码不一致"));
return;
}
//
if (value && !form.value.password) {
callback(new Error("请先输入密码"));
return;
}
}
callback();
};
//
const rules = {
account: [
{ required: true, message: "请输入账号", trigger: "blur" },
{ min: 3, max: 20, message: "账号长度在 3 到 20 个字符", trigger: "blur" },
],
name: [{ required: true, message: "请输入姓名", trigger: "blur" }],
password: [{ validator: validatePassword, trigger: "blur" }],
confirmPassword: [{ validator: validateConfirmPassword, trigger: "blur" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
};
// modelValue
watch(
() => props.modelValue,
(newVal) => {
visible.value = newVal;
}
);
// visible
watch(visible, (newVal) => {
if (!newVal) {
emit("update:modelValue", false);
}
});
// statusDict
watch(
() => props.statusDict,
(newVal) => {},
{ immediate: true, deep: true }
);
const loadUserData = async (user: any) => {
try {
// ID
const userId = typeof user === "number" ? user : user?.id || user?.userId;
if (!userId) {
throw new Error("未提供有效的用户 ID");
}
const res = await getEmployeeDetail(userId);
const data = res.data || res;
// sex status
const sexValue =
data.sex !== undefined && data.sex !== null
? Number(data.sex)
: data.gender !== undefined && data.gender !== null
? Number(data.gender)
: 0;
const statusValue =
data.account_status !== undefined && data.account_status !== null
? Number(data.account_status)
: data.status !== undefined && data.status !== null
? Number(data.status)
: 1;
const affiliateUnitValue =
data.affiliate_unit !== undefined && data.affiliate_unit !== null
? String(data.affiliate_unit)
: "";
const departmentValue =
data.department !== undefined && data.department !== null
? String(data.department)
: "";
isHydratingUserData.value = true;
form.value = {
id: data.id,
tid: data.tid || data.tenant_id || 1,
tenant_id: data.tenant_id || data.tid || 1,
account: data.account || "",
name: data.name || "",
phone: data.phone || "",
qq: data.qq || "",
sex: sexValue,
birthday: data.birthday || "",
affiliate_unit: affiliateUnitValue,
department: departmentValue,
position: data.position || "",
education: data.education || "",
nation: data.nation || data.nationality || "",
wechat: data.wechat || "",
home_address: data.home_address || "",
password: "",
confirmPassword: "",
email: data.email || "",
account_status: statusValue,
status: statusValue,
};
await nextTick();
departmentList.value = [];
if (form.value.affiliate_unit) {
await loadDepartments(Number(form.value.affiliate_unit));
}
form.value.department = departmentValue;
isHydratingUserData.value = false;
} catch (e: any) {
console.error("Failed to load user data:", e);
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
ElMessage.error(errorMsg);
throw e;
}
};
const handleCancel = () => {
visible.value = false;
};
const handleSubmit = async () => {
//
if (!formRef.value) {
return;
}
try {
await formRef.value.validate();
} catch (error) {
ElMessage.warning("请检查表单填写是否正确");
return;
}
//
if (isAdd.value) {
//
if (!form.value.password) {
ElMessage.error("请输入密码");
return;
}
if (form.value.password !== form.value.confirmPassword) {
ElMessage.error("两次输入的密码不一致");
return;
}
} else {
//
if (form.value.password) {
if (form.value.password !== form.value.confirmPassword) {
ElMessage.error("两次输入的密码不一致");
return;
}
}
}
try {
if (isAdd.value) {
//
const submitData: any = {
tid: form.value.tid,
tenant_id: form.value.tenant_id,
account: form.value.account,
name: form.value.name,
phone: form.value.phone,
qq: form.value.qq,
sex: form.value.sex,
birthday: form.value.birthday,
affiliate_unit: form.value.affiliate_unit,
department: form.value.department,
position: form.value.position,
education: form.value.education,
nation: form.value.nation,
wechat: form.value.wechat,
email: form.value.email,
home_address: form.value.home_address,
account_status: form.value.account_status,
status: form.value.account_status,
password: form.value.password,
};
await createEmployee(submitData);
ElMessage.success("添加成功");
} else {
//
if (!form.value.id || form.value.id === 0) {
ElMessage.error("用户ID不能为空");
return;
}
const submitData: any = {
id: form.value.id,
tid: form.value.tid,
tenant_id: form.value.tenant_id,
account: form.value.account,
name: form.value.name,
phone: form.value.phone,
qq: form.value.qq,
sex: form.value.sex,
birthday: form.value.birthday,
affiliate_unit: form.value.affiliate_unit,
department: form.value.department,
position: form.value.position,
education: form.value.education,
nation: form.value.nation,
wechat: form.value.wechat,
email: form.value.email,
home_address: form.value.home_address,
account_status: form.value.account_status,
status: form.value.account_status,
};
//
if (form.value.password) {
submitData.password = form.value.password;
}
await editEmployee(form.value.id, submitData);
ElMessage.success("更新成功");
}
visible.value = false;
emit("submit");
} catch (e: any) {
const errorMsg = e?.response?.data?.message || e?.message || "操作失败";
ElMessage.error(errorMsg);
}
};
//
defineExpose({
loadUserData,
openAdd: () => {
isAdd.value = true;
form.value = {
id: 0,
tid: 1,
tenant_id: 1,
account: "",
name: "",
phone: "",
qq: "",
sex: 0,
birthday: "",
affiliate_unit: "",
department: "",
position: "",
education: "",
nation: "",
wechat: "",
home_address: "",
password: "",
confirmPassword: "",
email: "",
account_status: 1,
status: 1,
};
loadCompanyList();
visible.value = true;
//
if (formRef.value) {
formRef.value.clearValidate();
}
},
openEdit: (user: any) => {
isAdd.value = false;
loadCompanyList();
visible.value = true;
//
if (formRef.value) {
formRef.value.clearValidate();
}
//
loadUserData(user);
},
open: (user?: any) => {
loadCompanyList();
if (user) {
isAdd.value = false;
loadUserData(user);
} else {
isAdd.value = true;
form.value = {
id: 0,
tid: 1,
tenant_id: 1,
account: "",
name: "",
phone: "",
qq: "",
sex: 0,
birthday: "",
affiliate_unit: "",
department: "",
position: "",
education: "",
nation: "",
wechat: "",
home_address: "",
password: "",
confirmPassword: "",
email: "",
account_status: 1,
status: 1,
};
}
visible.value = true;
//
if (formRef.value) {
formRef.value.clearValidate();
}
},
});
</script>
<style lang="less" scoped>
.form-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 20px;
}
</style>
@@ -0,0 +1,97 @@
<template>
<el-drawer v-model="visible" title="查看用户" width="500px">
<el-descriptions :column="1" border>
<el-descriptions-item label="ID">{{ detail.id || "-" }}</el-descriptions-item>
<el-descriptions-item label="账号">{{ detail.account || "-" }}</el-descriptions-item>
<el-descriptions-item label="姓名">{{ detail.name || "-" }}</el-descriptions-item>
<el-descriptions-item label="性别">{{ genderText }}</el-descriptions-item>
<el-descriptions-item label="生日">{{ detail.birthday || "-" }}</el-descriptions-item>
<el-descriptions-item label="隶属单位">{{ detail.affiliate_unit_name || detail.affiliate_unit || "-" }}</el-descriptions-item>
<el-descriptions-item label="部门">{{ detail.department_name || detail.department || "-" }}</el-descriptions-item>
<el-descriptions-item label="职位">{{ detail.position || "-" }}</el-descriptions-item>
<el-descriptions-item label="学历">{{ detail.education || "-" }}</el-descriptions-item>
<el-descriptions-item label="民族">{{ detail.nation || "-" }}</el-descriptions-item>
<el-descriptions-item label="手机">{{ detail.phone || "-" }}</el-descriptions-item>
<el-descriptions-item label="微信">{{ detail.wechat || "-" }}</el-descriptions-item>
<el-descriptions-item label="邮箱">{{ detail.email || "-" }}</el-descriptions-item>
<el-descriptions-item label="家庭住址">{{ detail.home_address || "-" }}</el-descriptions-item>
<el-descriptions-item label="账号状态">
<el-tag :type="statusValue === 1 ? 'success' : 'danger'">
{{ statusText }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
</template>
</el-drawer>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
import { ElMessage } from "element-plus";
import { getEmployeeDetail } from "@/api/erp";
const visible = ref(false);
const detail = ref<any>({});
const genderValue = computed(() => {
const value = detail.value.sex ?? detail.value.gender ?? 0;
return Number(value);
});
const genderText = computed(() => {
if (genderValue.value === 1) {
return "男";
}
if (genderValue.value === 2) {
return "女";
}
return "未知";
});
const statusValue = computed(() => {
const value = detail.value.account_status ?? detail.value.status ?? 0;
return Number(value);
});
const statusText = computed(() => {
if (statusValue.value === 1) {
return "启用";
}
if (statusValue.value === 2) {
return "离职";
}
return "禁用";
});
const loadUserData = async (user: any) => {
try {
const userId = typeof user === "number" ? user : user?.id || user?.userId;
if (!userId) {
throw new Error("未提供有效的用户 ID");
}
const res = await getEmployeeDetail(userId);
detail.value = res.data || res || {};
} catch (e: any) {
const errorMsg = e?.response?.data?.message || e?.message || "加载用户失败";
ElMessage.error(errorMsg);
throw e;
}
};
const openView = async (user: any) => {
visible.value = true;
await loadUserData(user);
};
defineExpose({
loadUserData,
openView,
open: openView,
});
</script>
<style lang="less" scoped></style>
+201
View File
@@ -0,0 +1,201 @@
<template>
<div class="container-box">
<div class="header-bar">
<h2>用户管理</h2>
<div class="header-actions">
<el-button type="primary" @click="handleAddUser">
<el-icon>
<Plus />
</el-icon>
添加用户
</el-button>
<el-button @click="refresh">
<el-icon>
<Refresh />
</el-icon>
刷新
</el-button>
</div>
</div>
<el-divider></el-divider>
<!-- 用户列表表格 -->
<el-table :data="users" style="width: 100%" v-loading="loading">
<el-table-column prop="id" label="ID" align="center" fixed="left" width="80" />
<el-table-column prop="account" label="账号" align="center" width="120" />
<el-table-column prop="name" label="姓名" align="center" width="120">
<template #default="scope">
<span class="name-link" @click="handlePreview(scope.row)">{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label="隶属单位" align="center">
<template #default="scope">
{{ scope.row.affiliate_unit_name || scope.row.affiliate_unit || "-" }}
</template>
</el-table-column>
<el-table-column label="部门" align="center">
<template #default="scope">
{{ scope.row.department_name || scope.row.department || "-" }}
</template>
</el-table-column>
<el-table-column prop="position" label="职位" align="center" />
<el-table-column prop="account_status" label="账号状态" width="80" align="center">
<template #default="scope">
<el-tag :type="getStatusValue(scope.row) === 1 ? 'success' : 'danger'">{{
getStatusValue(scope.row) === 1 ? "启用" : getStatusValue(scope.row) === 2 ? "离职" : "禁用"
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="240" align="center" fixed="right">
<template #default="scope"><!-- -->
<el-button size="small" @click="handlePreview(scope.row)">查看</el-button>
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="warning" @click="handleChangePassword(scope.row)">
修改密码
</el-button>
<el-button v-if="scope.row.username !== 'admin' && scope.row.id !== 1" size="small" type="danger"
@click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-bar">
<el-pagination :current-page="page" :page-size="pageSize" :total="total" @current-change="handlePageChange"
layout="total, prev, pager, next" />
</div>
<!-- 查看弹窗 -->
<View ref="viewRef" />
<!-- 修改密码弹窗 -->
<ChangePass ref="changePassRef" @submit="fetchData" />
<!-- 编辑弹窗 -->
<Edit ref="editRef" @submit="fetchData" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Refresh } from "@element-plus/icons-vue";
import { getEmployeeList, deleteEmployee } from "@/api/erp";
import Edit from "./components/edit.vue";
import View from "./components/view.vue";
import ChangePass from "./components/changepass.vue";
import { useAuthStore } from "@/stores/auth";
const users = ref<any[]>([]);
const loading = ref(false);
const page = ref(1);
const pageSize = ref(10);
const total = ref(0);
const editRef = ref<any>(null);
const viewRef = ref<any>(null);
const changePassRef = ref<any>(null);
const getStatusValue = (row: any) => {
const status = row.account_status ?? row.status ?? 0;
return Number(status);
};
const authStore = useAuthStore();
const tenantId = (authStore.user as any)?.tid;
// console.log(tenantId);
//
const fetchData = async (currentTenantId = tenantId) => {
loading.value = true;
try {
const res = await getEmployeeList(currentTenantId);
users.value = res.data || res || [];
total.value = users.value.length;
} catch (e: any) {
ElMessage.error(e?.message || "获取列表失败");
} finally {
loading.value = false;
}
};
//
const refresh = () => {
fetchData(tenantId);
};
//
const handleAddUser = () => {
editRef.value?.openAdd();
};
//
const handleEdit = (row: any) => {
editRef.value?.openEdit(row);
};
const handleChangePassword = (row: any) => {
changePassRef.value?.openChangePass(row);
};
//
const handlePreview = (row: any) => {
viewRef.value?.openView(row);
};
//
const handleDelete = async (row: any) => {
try {
await ElMessageBox.confirm(`确定删除用户 "${row.name}" 吗?`, "提示", {
type: "warning",
});
await deleteEmployee(row.id);
ElMessage.success("删除成功");
fetchData(tenantId);
} catch (e: any) {
if (e !== "cancel") {
ElMessage.error(e?.message || "删除失败");
}
}
};
//
const handlePageChange = (val: number) => {
page.value = val;
fetchData(tenantId);
};
onMounted(() => {
fetchData(tenantId);
});
</script>
<style lang="less" scoped>
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
span {
font-size: 1.4rem;
font-weight: 700;
}
}
:deep(.el-alert__title) {
color: #f56c6c !important;
}
.name-link {
color: #3973ff;
cursor: pointer;
text-decoration: none;
transition: color 0.3s;
&:hover {
color: #66b1ff;
text-decoration: underline;
}
}
</style>
+8
View File
@@ -0,0 +1,8 @@
<template>
<router-view />
</template>
<script setup></script>
<style lang="less" scoped></style>
@@ -0,0 +1,252 @@
<template>
<el-dialog v-model="visible" :title="isEdit ? '编辑组织' : '添加组织'" width="500px" :close-on-click-modal="false">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<el-form-item label="上级组织" prop="parent_id">
<el-tree-select v-model="formData.parent_id" :data="extendedTreeData"
:props="props.treeProps || defaultTreeProps" check-strictly placeholder="请选择上级组织" clearable
style="width: 100%" />
</el-form-item>
<el-form-item label="组织名称" prop="org_name">
<el-input v-model="formData.org_name" placeholder="请输入组织名称,例如:连云港云泽集团" />
</el-form-item>
<el-form-item label="组织编码" prop="org_code">
<el-input v-model="formData.org_code" placeholder="请输入组织编码,例如:LYG-YZ-GROUP" />
</el-form-item>
<el-form-item label="负责人" prop="leader_id">
<el-select v-model="formData.leader_id" placeholder="请选择负责人" filterable clearable style="width: 100%">
<el-option
v-for="item in leaderList"
:key="item.id"
:label="item.name || item.account || String(item.id)"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="formData.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="是否是公司" prop="is_company">
<el-radio-group v-model="formData.is_company">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="0">禁用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="3" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
import { createOrganization, editOrganization } from '@/api/erp'
import { useAuthStore } from '@/stores/auth'
import { getTenantUsers } from '@/api/user'
interface TreeNode {
id: number
org_name: string
org_code?: string
parent_id?: number
parent_name?: string
leader_id?: number
sort?: number
status?: number
is_company?: number
remark?: string
}
const props = defineProps<{
treeData: TreeNode[]
treeProps?: {
label: string
children: string
value: string
}
}>()
// treeProps
const defaultTreeProps = {
label: 'org_name',
children: 'children',
value: 'id'
}
const emit = defineEmits<{
success: []
}>()
const visible = ref(false)
const loading = ref(false)
const formRef = ref<FormInstance>()
const isEdit = ref(false)
const authStore = useAuthStore()
const tenantId = (authStore.user as any)?.tid
const leaderList = ref<Array<{ id: number; name?: string; account?: string }>>([])
const formData = reactive({
id: 0,
parent_id: undefined as number | undefined,
org_name: '',
org_code: '',
leader_id: undefined as number | undefined,
sort: 0,
is_company: 0,
status: 1,
remark: ''
})
const formRules: FormRules = {
org_name: [{ required: true, message: '请输入组织名称', trigger: 'blur' }],
org_code: [{ required: true, message: '请输入组织编码', trigger: 'blur' }]
}
const treeOptions = computed(() => {
return [
{
id: 0,
org_name: '顶级组织', //
children: props.treeData //
}
]
})
//
const extendedTreeData = computed(() => {
const rootNode = { id: 0, org_name: '顶级组织' }
return [rootNode, ...props.treeData]
})
const getLeaderList = async () => {
if (!tenantId) {
leaderList.value = []
return
}
const res: any = await getTenantUsers(tenantId)
const responseData = res?.data?.code !== undefined ? res.data : res
if (responseData?.code === 200) {
const list = responseData.data?.list || responseData.data || []
leaderList.value = list.map((item: any) => ({
...item,
id: Number(item.id)
}))
}
}
//
const open = (data?: TreeNode, parentId?: number) => {
getLeaderList()
if (data) {
// children
isEdit.value = true
formData.id = data.id
formData.parent_id = data.parent_id ?? 0
formData.org_name = data.org_name
formData.org_code = data.org_code || ''
formData.leader_id = data.leader_id === undefined || data.leader_id === null ? undefined : Number(data.leader_id)
formData.is_company = data.is_company ?? 0
formData.sort = data.sort ?? 0
formData.status = data.status ?? 1
formData.remark = data.remark || ''
} else {
//
isEdit.value = false
formData.id = 0
formData.parent_id = parentId ?? 0
formData.org_name = ''
formData.org_code = ''
formData.leader_id = undefined
formData.is_company = 0
formData.sort = 0
formData.status = 1
formData.remark = ''
}
visible.value = true
}
//
const handleClose = () => {
visible.value = false
formRef.value?.resetFields()
}
//
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate()
loading.value = true
try {
let res
//
const allowedKeys = ['parent_id', 'org_name', 'org_code', 'leader_id', 'sort', 'is_company', 'status', 'remark']
if (isEdit.value && formData.id) {
// 使
const submitData: Record<string, any> = {}
allowedKeys.forEach(key => {
const value = (formData as any)[key]
if (value !== undefined && value !== null) {
submitData[key] = value
}
})
if (tenantId) {
submitData.tid = tenantId
}
res = await editOrganization(formData.id, submitData)
} else {
// 使 FormData multipart/form-data
const submitData = new FormData()
allowedKeys.forEach(key => {
const value = (formData as any)[key]
if (value !== undefined && value !== null) {
submitData.append(key, String(value))
}
})
if (tenantId) {
submitData.append('tid', String(tenantId))
}
res = await createOrganization(submitData)
}
if (res.code === 200) {
ElMessage.success(isEdit.value ? '编辑成功' : '添加成功')
emit('success')
handleClose()
} else {
ElMessage.error(res.msg || (isEdit.value ? '编辑失败' : '添加失败'))
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('提交失败')
} finally {
loading.value = false
}
}
onMounted(() => {
getLeaderList()
})
defineExpose({ open })
</script>
+358
View File
@@ -0,0 +1,358 @@
<template>
<div class="organization-container">
<el-row :gutter="16">
<!-- 左侧组织架构树 -->
<el-col :span="8">
<el-card shadow="hover" class="tree-card" v-loading="loading">
<template #header>
<div class="card-header">
<span>组织架构</span>
<el-button type="primary" link @click="handleAddRoot">
<el-icon><Plus /></el-icon>
添加
</el-button>
</div>
</template>
<el-tree
ref="treeRef"
:data="treeData"
:props="treeProps"
node-key="id"
default-expand-all
:expand-on-click-node="false"
@node-click="handleNodeClick"
>
<template #default="{ node, data }">
<div class="tree-node">
<span class="node-label">{{ node.label }}</span>
<div class="node-actions" @click.stop>
<el-button link type="primary" size="small" @click="handleAdd(data)">
<el-icon><Plus /></el-icon>
</el-button>
<el-button link type="primary" size="small" @click="handleEdit(data)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button link type="danger" size="small" @click="handleDelete(data)">
<el-icon><Delete /></el-icon>
</el-button>
</div>
</div>
</template>
</el-tree>
</el-card>
</el-col>
<!-- 右侧详情面板 -->
<el-col :span="16">
<el-card shadow="hover" class="detail-card">
<template #header>
<div class="card-header">
<span>{{ detailTitle }}</span>
</div>
</template>
<div v-if="selectedNode" class="detail-content">
<el-descriptions :column="2" border>
<el-descriptions-item label="组织名称">
{{ selectedNode.org_name }}
</el-descriptions-item>
<el-descriptions-item label="组织编码">
{{ selectedNode.org_code || '-' }}
</el-descriptions-item>
<el-descriptions-item label="上级组织">
{{ selectedNode.parent_name || '顶级组织' }}
</el-descriptions-item>
<el-descriptions-item label="负责人">
{{ selectedNode.leader_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="是否是公司">
<el-tag :type="selectedNode.is_company === 1 ? 'primary' : 'info'">
{{ selectedNode.is_company === 1 ? '是' : '否' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="状态">
<el-tag :type="selectedNode.status === 1 ? 'success' : 'danger'">
{{ selectedNode.status === 1 ? '启用' : '禁用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">
{{ selectedNode.remark || '-' }}
</el-descriptions-item>
</el-descriptions>
<div class="detail-actions">
<el-button type="primary" @click="handleEdit(selectedNode)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button type="danger" @click="handleDelete(selectedNode)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</div>
</div>
<el-empty v-else description="请选择左侧组织节点查看详情" />
</el-card>
</el-col>
</el-row>
<!-- 添加/编辑对话框 -->
<OrganizationEdit
ref="editRef"
:tree-data="treeData"
:tree-props="treeProps"
@success="handleSuccess"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete } from '@element-plus/icons-vue'
import OrganizationEdit from './components/edit.vue'
import { getOrganizationList, getOrganizationDetail, deleteOrganization } from '@/api/erp'
//
interface TreeNode {
id: number
tid?: number
org_name: string
org_code?: string
parent_id?: number
parent_name?: string
leader_id?: number
leader_name?: string
is_company?: number
sort?: number
status?: number
remark?: string
children?: TreeNode[]
}
//
const treeData = ref<TreeNode[]>([])
const treeProps = {
label: 'org_name',
children: 'children',
value: 'id'
}
const treeRef = ref()
const editRef = ref()
const selectedNode = ref<TreeNode | null>(null)
const loading = ref(false)
//
const buildTree = (list: any[]): TreeNode[] => {
const nodeMap = new Map<number, TreeNode>()
const roots: TreeNode[] = []
list.forEach(item => {
const node: TreeNode = {
id: item.id,
tid: item.tid,
org_name: item.org_name,
org_code: item.org_code,
parent_id: item.parent_id,
parent_name: item.parent_name,
leader_id: item.leader_id,
leader_name: item.leader_name,
is_company: item.is_company,
sort: item.sort,
status: item.status,
remark: item.remark,
children: []
}
nodeMap.set(node.id, node)
})
nodeMap.forEach(node => {
if (node.parent_id && nodeMap.has(node.parent_id)) {
const parent = nodeMap.get(node.parent_id)!
if (!parent.children) {
parent.children = []
}
parent.children.push(node)
} else {
roots.push(node)
}
})
return roots
}
//
const loadOrganizationDetail = async (id: number) => {
try {
const res = await getOrganizationDetail(id)
if (res.code === 200) {
const data = res.data || null
if (data) {
selectedNode.value = {
id: data.id,
tid: data.tid,
org_name: data.org_name,
org_code: data.org_code,
parent_id: data.parent_id,
parent_name: data.parent_name,
leader_id: data.leader_id,
leader_name: data.leader_name,
is_company: data.is_company,
sort: data.sort,
status: data.status,
remark: data.remark,
children: []
}
} else {
selectedNode.value = null
}
} else {
ElMessage.error(res.msg || '获取组织机构详情失败')
}
} catch (error) {
console.error('获取组织机构详情失败:', error)
ElMessage.error('获取组织机构详情失败')
}
}
//
const fetchOrganizationList = async () => {
loading.value = true
try {
const res = await getOrganizationList()
if (res.code === 200) {
const list = res.data || []
treeData.value = buildTree(list)
//
if (!selectedNode.value && list.length) {
await loadOrganizationDetail(list[0].id)
}
} else {
ElMessage.error(res.msg || '获取组织机构列表失败')
}
} catch (error) {
console.error('获取组织机构列表失败:', error)
ElMessage.error('获取组织机构列表失败')
} finally {
loading.value = false
}
}
//
const detailTitle = computed(() => {
return selectedNode.value ? `部门详情 - ${selectedNode.value.org_name}` : '部门详情'
})
//
const handleNodeClick = (data: TreeNode) => {
loadOrganizationDetail(data.id)
}
//
const handleAddRoot = () => {
editRef.value?.open(undefined, undefined)
}
//
const handleAdd = (data: TreeNode) => {
editRef.value?.open(undefined, data.id)
}
//
const handleEdit = (data: TreeNode) => {
editRef.value?.open(data)
}
//
const handleSuccess = () => {
fetchOrganizationList()
}
//
const handleDelete = (data: TreeNode) => {
ElMessageBox.confirm(`确定要删除部门「${data.org_name}」吗?`, '提示', {
type: 'warning'
})
.then(async () => {
try {
loading.value = true
const res = await deleteOrganization(data.id)
if (res.code === 200) {
ElMessage.success('删除成功')
fetchOrganizationList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
} finally {
loading.value = false
}
})
.catch(() => {})
}
onMounted(() => {
fetchOrganizationList()
})
</script>
<style lang="less" scoped>
.organization-container {
// padding: 16px;
}
.tree-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
}
.tree-node {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-right: 8px;
.node-label {
flex: 1;
}
.node-actions {
display: none;
gap: 4px;
}
&:hover .node-actions {
display: flex;
}
}
.detail-card {
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.detail-content {
.el-descriptions {
margin-bottom: 20px;
}
.detail-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}
}
</style>
+11
View File
@@ -0,0 +1,11 @@
<script setup>
</script>
<template>
<router-view />
</template>
<style scoped>
</style>
+136 -79
View File
@@ -1,18 +1,8 @@
<template>
<el-dialog
v-model="visible"
:title="isEdit ? '编辑角色' : '添加角色'"
width="600px"
@close="handleClose"
>
<el-dialog v-model="visible" :title="isEdit ? '编辑角色' : '添加角色'" width="600px" @close="handleClose">
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
<el-form-item label="角色名称" prop="name">
<el-input
v-model="form.name"
placeholder="请输入角色名称"
maxlength="50"
show-word-limit
/>
<el-input v-model="form.name" placeholder="请输入角色名称" maxlength="50" show-word-limit />
</el-form-item>
<el-form-item label="状态" prop="status">
@@ -23,15 +13,16 @@
</el-form-item>
<el-form-item label="权限设置" prop="rights">
<el-tree
ref="treeRef"
:data="menuTree"
show-checkbox
node-key="id"
:props="{ children: 'children', label: 'title' }"
:default-checked-keys="form.rights"
style="width: 100%; border: 1px solid var(--el-border-color); border-radius: 4px; padding: 10px; max-height: 400px; overflow-y: auto;"
/>
<div style="margin-bottom: 10px;">
<el-button size="small" @click="toggleSelectAll">
{{ isAllSelected ? '全不选' : '全选' }}
</el-button>
<el-button size="small" @click="handleExpandAll">展开/折叠</el-button>
</div>
<el-tree ref="treeRef" :data="menuTree" show-checkbox node-key="id"
:props="{ children: 'children', label: 'title' }" @check="updateSelectStatus"
style="width: 100%; border: 1px solid var(--el-border-color); border-radius: 4px; padding: 10px; max-height: 400px; overflow-y: auto;" />
</el-form-item>
</el-form>
@@ -62,12 +53,15 @@ const props = withDefaults(defineProps<Props>(), {
const emit = defineEmits(["update:modelValue", "success"]);
// --- ---
const visible = ref(false);
const isEdit = ref(false);
const submitting = ref(false);
const formRef = ref();
const treeRef = ref();
const menuTree = ref<any[]>([]);
const isAllSelected = ref(false);
const isExpandAll = ref(false);
const form = ref({
name: "",
@@ -83,40 +77,82 @@ const rules = {
status: [{ required: true, message: "请选择状态", trigger: "change" }],
};
// modelValue
watch(
() => props.modelValue,
(val) => {
visible.value = val;
if (val) {
loadMenus();
if (props.role) {
isEdit.value = true;
form.value = {
name: props.role.name,
status: props.role.status,
rights: parseRights(props.role.rights),
};
//
nextTick(() => {
if (treeRef.value) {
treeRef.value.setCheckedKeys(form.value.rights);
}
});
} else {
isEdit.value = false;
resetForm();
}
}
// --- ---
// ID
const getAllMenuIds = (data: any[]): number[] => {
const ids: number[] = [];
const traverse = (list: any[]) => {
list.forEach((item) => {
ids.push(item.id);
if (item.children && item.children.length > 0) traverse(item.children);
});
};
traverse(data);
return ids;
};
//
const handleExpandAll = () => {
if (!treeRef.value) return;
//
isExpandAll.value = !isExpandAll.value;
//
// nodesMap
const nodes = treeRef.value.store.nodesMap;
for (let id in nodes) {
// expanded
nodes[id].expanded = isExpandAll.value;
}
);
};
// visible
watch(visible, (val) => {
emit("update:modelValue", val);
});
// /
const updateSelectStatus = () => {
if (!treeRef.value || menuTree.value.length === 0) {
isAllSelected.value = false;
return;
}
const checkedKeys = treeRef.value.getCheckedKeys();
const allIds = getAllMenuIds(menuTree.value);
//
isAllSelected.value = allIds.length > 0 && checkedKeys.length === allIds.length;
};
//
// /
const toggleSelectAll = () => {
if (!treeRef.value) return;
if (isAllSelected.value) {
// ->
treeRef.value.setCheckedKeys([]);
isAllSelected.value = false;
} else {
// ->
const allIds = getAllMenuIds(menuTree.value);
treeRef.value.setCheckedKeys(allIds);
isAllSelected.value = true;
}
};
// --- ---
//
const loadMenus = async () => {
try {
const res = await getAllMenus();
if (res.code === 200) {
menuTree.value = res.data || [];
}
} catch (error) {
console.error("加载菜单失败:", error);
ElMessage.error("加载菜单失败");
}
};
//
const parseRights = (rights: any): number[] => {
if (!rights) return [];
if (Array.isArray(rights)) return rights;
@@ -131,58 +167,80 @@ const parseRights = (rights: any): number[] => {
return [];
};
//
const loadMenus = async () => {
try {
const res = await getAllMenus();
if (res.code === 200) {
menuTree.value = res.data || [];
}
} catch (error) {
console.error("加载菜单失败:", error);
ElMessage.error("加载菜单失败");
}
};
//
watch(
() => props.modelValue,
async (val) => {
visible.value = val;
if (val) {
//
await loadMenus();
//
if (props.role) {
isEdit.value = true;
form.value = {
name: props.role.name,
status: props.role.status,
rights: parseRights(props.role.rights),
};
//
nextTick(() => {
if (treeRef.value) {
treeRef.value.setCheckedKeys(form.value.rights);
updateSelectStatus(); //
}
});
} else {
isEdit.value = false;
isAllSelected.value = false;
resetForm();
}
}
}
);
watch(visible, (val) => {
emit("update:modelValue", val);
});
// --- ---
//
const resetForm = () => {
form.value = {
name: "",
status: 1,
rights: [],
};
if (formRef.value) {
formRef.value.clearValidate();
}
if (treeRef.value) {
treeRef.value.setCheckedKeys([]);
}
if (formRef.value) formRef.value.clearValidate();
if (treeRef.value) treeRef.value.setCheckedKeys([]);
};
//
const handleClose = () => {
visible.value = false;
resetForm();
};
//
const handleSubmit = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
// ID
//
const checkedKeys = treeRef.value.getCheckedKeys();
const halfCheckedKeys = treeRef.value.getHalfCheckedKeys();
const allCheckedKeys = [...checkedKeys, ...halfCheckedKeys];
const submitData = {
const submitData: any = {
name: form.value.name,
status: form.value.status,
rights: allCheckedKeys,
rights: checkedKeys,
};
// id
if (isEdit.value && props.role) {
submitData.id = props.role.id;
}
submitting.value = true;
let res;
@@ -201,7 +259,6 @@ const handleSubmit = async () => {
}
} catch (error: any) {
if (error !== false) {
//
ElMessage.error(error.message || "操作失败");
}
} finally {
+3 -3
View File
@@ -56,7 +56,7 @@
详情
</el-button>
<el-button
v-if="row.default !== 1 && row.default !== 2"
v-if="userInfo?.id === 2"
size="small"
type="primary"
link
@@ -66,7 +66,7 @@
编辑
</el-button>
<el-button
v-if="row.id !== 1"
v-if="userInfo?.id === 2"
size="small"
type="danger"
link
@@ -134,7 +134,7 @@ const fetchRoles = async () => {
const res = await getAllRoles();
if (res.code === 200) {
roles.value = res.data || [];
console.log('角色列表:', roles.value);
// console.log(':', roles.value);
} else {
error.value = res.msg || "获取角色列表失败";
ElMessage.error(error.value);
@@ -1,34 +1,16 @@
<template>
<el-form
ref="contactFormRef"
:model="contactForm"
:rules="contactRules"
label-width="120px"
style="max-width: 600px"
>
<el-form ref="contactFormRef" :model="contactForm" :rules="contactRules" label-width="120px" style="max-width: 600px">
<el-form-item label="联系电话" prop="phone">
<el-input
v-model="contactForm.phone"
placeholder="请输入联系电话"
/>
<el-input v-model="contactForm.phone" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="联系邮箱" prop="email">
<el-input
v-model="contactForm.email"
placeholder="请输入联系邮箱"
/>
<el-input v-model="contactForm.email" placeholder="请输入联系邮箱" />
</el-form-item>
<el-form-item label="公司地址" prop="address">
<el-input
v-model="contactForm.address"
placeholder="请输入公司地址"
/>
<el-input v-model="contactForm.address" placeholder="请输入公司地址" />
</el-form-item>
<el-form-item label="工作时间" prop="workTime">
<el-input
v-model="contactForm.workTime"
placeholder="如:周一至周五 9:00-18:00"
/>
<el-input v-model="contactForm.workTime" placeholder="如:周一至周五 9:00-18:00" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveContactSettings">保存设置</el-button>
@@ -38,10 +20,14 @@
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { ref, reactive, onMounted } from "vue";
import { ElMessage } 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 contactForm = reactive({
@@ -55,12 +41,35 @@ const contactRules: FormRules = {
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 () => {
if (!contactFormRef.value) return;
await contactFormRef.value.validate((valid) => {
await contactFormRef.value.validate(async (valid) => {
if (valid) {
// TODO:
ElMessage.success("联系方式保存成功");
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("联系方式保存成功");
} else {
ElMessage.error(res.msg || "保存失败");
}
}
});
};
@@ -75,4 +84,9 @@ const resetContactForm = () => {
defineExpose({
contactForm
});
//
onMounted(() => {
initCompanyInfos();
});
</script>
@@ -22,7 +22,7 @@
/>
</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-form-item>
</el-form>
@@ -34,7 +34,9 @@ import { ElMessage } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
import { getLegalInfos, saveLegalInfos } from "@/api/sitesettings";
import { useAuthStore } from "@/stores/auth";
const authStore = useAuthStore();
const legalNoticeFormRef = ref<FormInstance>();
const legalNotice = ref("");
@@ -64,12 +66,21 @@ const initLegalInfos = async () => {
}
};
const saveLegalInfos = async () => {
const handleSaveLegalInfos = async () => {
if (!legalNoticeFormRef.value) return;
await legalNoticeFormRef.value.validate((valid) => {
await legalNoticeFormRef.value.validate(async (valid) => {
if (valid) {
// TODO:
ElMessage.success("法律声明和隐私条款保存成功");
const data = {
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 || "保存失败");
}
}
});
};
@@ -0,0 +1,81 @@
<template>
<el-form ref="formRef" :model="form" label-width="120px">
<el-form-item label="开启验证" prop="openVerify">
<el-switch v-model="form.openVerify" />
</el-form-item>
<el-form-item label="验证模式" prop="verifyModel">
<el-radio-group v-model="form.verifyModel" :disabled="!form.openVerify">
<el-radio-button value="1">图形</el-radio-button>
<el-radio-button value="2">短信</el-radio-button>
<el-radio-button value="3">邮箱</el-radio-button>
<el-radio-button value="4">极验3.0</el-radio-button>
<el-radio-button value="5">极验4.0</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="form.verifyModel === '4'">
<el-form-item label="极验ID" prop="geetest3ID">
<el-input v-model="form.geetest3ID" />
</el-form-item>
<el-form-item label="极验KEY" prop="geetest3KEY">
<el-input v-model="form.geetest3KEY" />
</el-form-item>
</template>
<template v-if="form.verifyModel === '5'">
<el-form-item label="极验ID" prop="geetest4ID">
<el-input v-model="form.geetest4ID" />
</el-form-item>
<el-form-item label="极验KEY" prop="geetest4KEY">
<el-input v-model="form.geetest4KEY" />
</el-form-item>
</template>
<el-form-item>
<el-button type="primary" @click="handleSave">保存</el-button>
<el-button @click="formRef?.resetFields()">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ref, reactive } from "vue";
import { ElMessage } from "element-plus";
import { getVerifyInfos, saveVerifyInfos } from "@/api/sitesettings";
const formRef = ref();
const form = reactive({
openVerify: false,
verifyModel: "1",
geetest3ID: "",
geetest3KEY: "",
geetest4ID: "",
geetest4KEY: ""
});
const init = async () => {
const { code, data } = await getVerifyInfos();
if (code === 200 && Array.isArray(data)) {
const formData = {};
data.forEach(item => {
if (item.label === 'openVerify') {
formData[item.label] = item.value === '1';
} else {
formData[item.label] = item.value;
}
});
Object.assign(form, formData);
}
};
const handleSave = async () => {
const { code } = await saveVerifyInfos(form);
if (code === 200) ElMessage.success("保存成功");
};
init();
defineExpose({ form, init });
</script>
@@ -18,17 +18,51 @@
>
<img
v-if="logo"
:src="API_BASE_URL + logo.replace(/^\//, '/')"
:src="getPreviewUrl(logo)"
class="logo-image"
/>
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
</el-form-item>
<el-form-item label="站点Logo-浅色" prop="logow">
<el-upload
class="logo-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="handleLogowChange"
>
<img
v-if="logow"
:src="getPreviewUrl(logow)"
class="logo-image darkbg"
/>
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
</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-input v-model="companyname" placeholder="请输入企业名称" />
</el-form-item>
<el-form-item label="站点SEO描述" prop="description">
<el-input v-model="description" placeholder="请输入站点SEO描述" />
<el-form-item label="企业介绍" prop="companyintroduction">
<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 label="版权信息" prop="copyright">
<el-input v-model="copyright" placeholder="如:© 2026 公司名称" />
@@ -52,22 +86,30 @@ import { Plus } from "@element-plus/icons-vue";
import type { FormInstance, FormRules, UploadFile } from "element-plus";
import { getNormalInfos, saveNormalInfos } from "@/api/sitesettings";
import { uploadFile } from "@/api/file";
import { useAuthStore } from "@/stores/auth";
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
const authStore = useAuthStore();
const normalFormRef = ref<FormInstance>();
const sitename = ref("");
const companyname = ref("");
const companyintroduction = ref("");
const logo = ref("");
const logow = ref("");
const description = ref("");
const copyright = ref("");
const icp = ref("");
const ico = ref("");
const formData = {
sitename,
companyname,
companyintroduction,
logo,
logow,
ico,
description,
copyright,
icp,
@@ -77,21 +119,32 @@ const normalRules: FormRules = {
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 res = await getNormalInfos();
const res = await getNormalInfos(authStore.user.tid);
if (res.code === 200 && res.data) {
const data = res.data;
const dataMap: Record<string, string> = {};
data.forEach((item: any) => {
dataMap[item.label] = item.value;
});
sitename.value = dataMap["sitename"] || "";
logo.value = dataMap["logo"] || "";
description.value = dataMap["description"] || "";
copyright.value = dataMap["copyright"] || "";
icp.value = dataMap["icp"] || "";
companyname.value = dataMap["companyname"] || "";
sitename.value = data.sitename || "";
companyintroduction.value = data.companyintroduction || "";
logo.value = data.logo || "";
logow.value = data.logow || "";
ico.value = data.ico || "";
description.value = data.description || "";
copyright.value = data.copyright || "";
icp.value = data.icp || "";
companyname.value = data.companyname || "";
}
};
@@ -113,18 +166,40 @@ const handleLogoChange = (file: UploadFile) => {
});
};
const handleLogowChange = (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 &&
uploadRes.data.url
) {
logow.value = uploadRes.data.url.replace(/\\/g, "/");
} else {
ElMessage.error(uploadRes.msg || "上传失败");
}
});
};
const handleSaveNormalInfos = async () => {
if (!normalFormRef.value) return;
await normalFormRef.value.validate(async (valid) => {
if (valid) {
const data = [
{ label: "sitename", value: sitename.value },
{ label: "logo", value: logo.value },
{ label: "description", value: description.value },
{ label: "companyname", value: companyname.value },
{ label: "copyright", value: copyright.value },
{ label: "icp", value: icp.value },
];
const data = {
tid: authStore.user.tid,
sitename: sitename.value,
companyintroduction: companyintroduction.value,
logo: logo.value,
logow: logow.value,
ico: ico.value,
description: description.value,
companyname: companyname.value,
copyright: copyright.value,
icp: icp.value,
};
const res = await saveNormalInfos(data);
if (res.code === 200) {
ElMessage.success("保存成功");
@@ -135,19 +210,39 @@ 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 = () => {
sitename.value = "";
companyname.value = "";
companyintroduction.value = "";
logo.value = "";
logow.value = "";
ico.value = "";
description.value = "";
copyright.value = "";
copyright.value = "";
icp.value = "";
};
defineExpose({
sitename,
companyname,
companyintroduction,
logo,
logow,
ico,
description,
copyright,
icp,
@@ -159,7 +254,18 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
<style lang="less" scoped>
.darkbg {
background-color: #efefef;
}
.form-tip{
margin-left: 20px;
color: #999;
font-size: 12px;
margin-top: 5px;
}
.logo-uploader {
:deep(.el-upload) {
border: 1px dashed #d9d9d9;

Some files were not shown because too many files have changed in this diff Show More