更新
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
# 获取缓存数据 - userinfo 实例
|
||||
|
||||
## 方法一:从 localStorage 获取
|
||||
|
||||
```javascript
|
||||
// 获取用户信息
|
||||
function getUserInfo() {
|
||||
try {
|
||||
const userInfoStr = localStorage.getItem('userinfo');
|
||||
if (userInfoStr) {
|
||||
const userInfo = JSON.parse(userInfoStr);
|
||||
return userInfo;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const userInfo = getUserInfo();
|
||||
if (userInfo) {
|
||||
console.log('用户名:', userInfo.username);
|
||||
console.log('用户ID:', userInfo.id);
|
||||
console.log('角色:', userInfo.role);
|
||||
} else {
|
||||
console.log('未找到用户信息');
|
||||
}
|
||||
```
|
||||
|
||||
## 方法二:在 Vue 组件中使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<h3>用户信息</h3>
|
||||
<p v-if="userInfo">欢迎, {{ userInfo.username }}</p>
|
||||
<p v-else>请先登录</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
const userInfo = ref(null);
|
||||
|
||||
// 获取用户信息
|
||||
function getUserInfo() {
|
||||
try {
|
||||
const userInfoStr = localStorage.getItem('userinfo');
|
||||
if (userInfoStr) {
|
||||
userInfo.value = JSON.parse(userInfoStr);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
## 方法三:使用工具函数
|
||||
|
||||
```javascript
|
||||
// utils/storage.js
|
||||
export class StorageUtil {
|
||||
// 获取用户信息
|
||||
static getUserInfo() {
|
||||
try {
|
||||
const userInfoStr = localStorage.getItem('userinfo');
|
||||
return userInfoStr ? JSON.parse(userInfoStr) : null;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
static setUserInfo(userInfo) {
|
||||
try {
|
||||
localStorage.setItem('userinfo', JSON.stringify(userInfo));
|
||||
} catch (error) {
|
||||
console.error('保存用户信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除用户信息
|
||||
static clearUserInfo() {
|
||||
localStorage.removeItem('userinfo');
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
static isLoggedIn() {
|
||||
const userInfo = this.getUserInfo();
|
||||
return userInfo && userInfo.id;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
import { StorageUtil } from '@/utils/storage';
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = StorageUtil.getUserInfo();
|
||||
console.log('用户信息:', userInfo);
|
||||
|
||||
// 检查登录状态
|
||||
if (StorageUtil.isLoggedIn()) {
|
||||
console.log('用户已登录');
|
||||
} else {
|
||||
console.log('用户未登录');
|
||||
}
|
||||
```
|
||||
|
||||
## 方法四:使用 Pinia store
|
||||
|
||||
```javascript
|
||||
// stores/user.js
|
||||
import { defineStore } from 'pinia';
|
||||
import { StorageUtil } from '@/utils/storage';
|
||||
|
||||
export const useUserStore = defineStore('user', {
|
||||
state: () => ({
|
||||
userInfo: null
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isLoggedIn: (state) => state.userInfo && state.userInfo.id,
|
||||
username: (state) => state.userInfo?.username || ''
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 初始化用户信息(从缓存加载)
|
||||
initUserInfo() {
|
||||
this.userInfo = StorageUtil.getUserInfo();
|
||||
},
|
||||
|
||||
// 设置用户信息
|
||||
setUserInfo(userInfo) {
|
||||
this.userInfo = userInfo;
|
||||
StorageUtil.setUserInfo(userInfo);
|
||||
},
|
||||
|
||||
// 清除用户信息
|
||||
clearUserInfo() {
|
||||
this.userInfo = null;
|
||||
StorageUtil.clearUserInfo();
|
||||
},
|
||||
|
||||
// 登录
|
||||
login(userInfo) {
|
||||
this.setUserInfo(userInfo);
|
||||
},
|
||||
|
||||
// 登出
|
||||
logout() {
|
||||
this.clearUserInfo();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 在组件中使用
|
||||
import { useUserStore } from '@/stores/user';
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 初始化时加载用户信息
|
||||
onMounted(() => {
|
||||
userStore.initUserInfo();
|
||||
});
|
||||
|
||||
return {
|
||||
userInfo: computed(() => userStore.userInfo),
|
||||
isLoggedIn: computed(() => userStore.isLoggedIn)
|
||||
};
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## 方法五:使用 sessionStorage
|
||||
|
||||
```javascript
|
||||
// 获取用户信息(仅在当前会话有效)
|
||||
function getUserInfoFromSession() {
|
||||
try {
|
||||
const userInfoStr = sessionStorage.getItem('userinfo');
|
||||
return userInfoStr ? JSON.parse(userInfoStr) : null;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户信息到 sessionStorage
|
||||
function setUserInfoToSession(userInfo) {
|
||||
try {
|
||||
sessionStorage.setItem('userinfo', JSON.stringify(userInfo));
|
||||
} catch (error) {
|
||||
console.error('保存用户信息失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除 sessionStorage 中的用户信息
|
||||
function clearUserInfoFromSession() {
|
||||
sessionStorage.removeItem('userinfo');
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据类型**: 缓存中存储的是字符串,需要使用 `JSON.parse()` 转换为对象
|
||||
2. **错误处理**: 务必使用 try-catch 处理 JSON 解析错误
|
||||
3. **数据验证**: 获取到数据后应该验证数据的完整性
|
||||
4. **安全性**: 敏感信息不要直接存储在前端缓存中
|
||||
5. **过期处理**: 可以添加时间戳来处理缓存过期逻辑
|
||||
|
||||
## 完整示例
|
||||
|
||||
```javascript
|
||||
// 获取用户信息,包含过期检查
|
||||
function getUserInfoWithExpiry() {
|
||||
try {
|
||||
const userInfoStr = localStorage.getItem('userinfo');
|
||||
if (!userInfoStr) return null;
|
||||
|
||||
const userInfo = JSON.parse(userInfoStr);
|
||||
|
||||
// 检查是否过期(可选)
|
||||
if (userInfo.expiry && Date.now() > userInfo.expiry) {
|
||||
localStorage.removeItem('userinfo');
|
||||
return null;
|
||||
}
|
||||
|
||||
return userInfo;
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
localStorage.removeItem('userinfo'); // 清除损坏的数据
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 设置用户信息,包含过期时间
|
||||
function setUserInfoWithExpiry(userInfo, expiryHours = 24) {
|
||||
try {
|
||||
const userInfoWithExpiry = {
|
||||
...userInfo,
|
||||
expiry: Date.now() + (expiryHours * 60 * 60 * 1000) // 过期时间
|
||||
};
|
||||
localStorage.setItem('userinfo', JSON.stringify(userInfoWithExpiry));
|
||||
} catch (error) {
|
||||
console.error('保存用户信息失败:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,27 @@
|
||||
````
|
||||
<template>
|
||||
{{ (xxxxxxxxDict.find(item => item.dict_value == String(model?.status)) || {}).dict_label || '-' }}
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
|
||||
// 字典store
|
||||
const dictStore = useDictStore();
|
||||
const xxxxxxxxDict = ref<any[]>([]);
|
||||
|
||||
// 获取审核状态字典
|
||||
const fetchxxxxxxxxDict = async () => {
|
||||
try {
|
||||
xxxxxxxxDict.value = await dictStore.getDictItems('article_status');
|
||||
console.log(xxxxxxxxDict.value);
|
||||
} catch (err) {
|
||||
console.error('获取文章状态字典失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchxxxxxxxxDict();
|
||||
});
|
||||
</script>
|
||||
````
|
||||
@@ -52,3 +52,56 @@ export function updateArticleStatus(id, status) {
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 获取分类列表
|
||||
export function listCategories(params) {
|
||||
return request({
|
||||
url: `/api/categories`,
|
||||
method: "get",
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取分类详情
|
||||
export function getCategory(id) {
|
||||
return request({
|
||||
url: `/api/categories/${id}`,
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
// 创建分类
|
||||
export function createCategory(data) {
|
||||
return request({
|
||||
url: `/api/categories`,
|
||||
method: "post",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新分类
|
||||
export function updateCategory(id, data) {
|
||||
return request({
|
||||
url: `/api/categories/${id}`,
|
||||
method: "put",
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除分类
|
||||
export function deleteCategory(id) {
|
||||
return request({
|
||||
url: `/api/categories/${id}`,
|
||||
method: "delete",
|
||||
});
|
||||
}
|
||||
|
||||
// 更新分类状态
|
||||
export function updateCategoryStatus(id, status) {
|
||||
return request({
|
||||
url: `/api/categories/${id}/status`,
|
||||
method: "patch",
|
||||
data: { status },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,581 @@
|
||||
<template>
|
||||
<div class="category-manager">
|
||||
<!-- 工具栏 -->
|
||||
<div class="toolbar">
|
||||
<el-button type="primary" @click="handleAdd">新增分类</el-button>
|
||||
<el-button @click="handleRefresh">刷新</el-button>
|
||||
<div class="search-wrapper">
|
||||
<el-input
|
||||
v-model="searchQuery"
|
||||
placeholder="搜索分类名称"
|
||||
clearable
|
||||
@clear="handleSearch"
|
||||
style="width: 200px"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类菜单 -->
|
||||
<div class="category-menu" v-loading="loading">
|
||||
<div v-if="treeData.length === 0" class="empty-state">
|
||||
<el-empty description="暂无分类数据" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<MenuItem
|
||||
v-for="category in treeData"
|
||||
:key="category.id"
|
||||
:category="category"
|
||||
:level="0"
|
||||
@edit="handleEdit"
|
||||
@add-child="handleAddChild"
|
||||
@toggle="handleToggle"
|
||||
@delete="handleDelete"
|
||||
@enable="handleEnable"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEdit ? '编辑分类' : '新增分类'"
|
||||
width="500px"
|
||||
>
|
||||
<el-form
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
ref="formRef"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="分类名称" prop="label">
|
||||
<el-input v-model="formData.label" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类值" prop="value">
|
||||
<el-input
|
||||
v-model="formData.value"
|
||||
placeholder="请输入分类值"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="父级分类" prop="cid">
|
||||
<el-select
|
||||
v-model="formData.cid"
|
||||
placeholder="选择父级分类"
|
||||
clearable
|
||||
filterable
|
||||
>
|
||||
<el-option
|
||||
v-for="category in categoryList.filter(c => c.id !== formData.id)"
|
||||
:key="category.id"
|
||||
:label="category.label"
|
||||
:value="category.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"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">启用</el-radio>
|
||||
<el-radio :label="0">禁用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
|
||||
确定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { Search, Plus, Edit, Delete, Check, ArrowDown, ArrowRight } from '@element-plus/icons-vue';
|
||||
import { listCategories, createCategory, updateCategory, updateCategoryStatus } from '@/api/article';
|
||||
|
||||
|
||||
// 菜单项组件
|
||||
const MenuItem = {
|
||||
name: 'MenuItem',
|
||||
props: {
|
||||
category: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
emits: ['edit', 'add-child', 'toggle', 'delete', 'enable'],
|
||||
data() {
|
||||
return {
|
||||
expanded: true
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasChildren() {
|
||||
return this.category.children && this.category.children.length > 0;
|
||||
},
|
||||
indentStyle() {
|
||||
return {
|
||||
paddingLeft: `${this.level * 20 + 12}px`
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleExpanded() {
|
||||
this.expanded = !this.expanded;
|
||||
this.$emit('toggle', this.category, this.expanded);
|
||||
},
|
||||
handleEdit() {
|
||||
this.$emit('edit', this.category);
|
||||
},
|
||||
handleAddChild() {
|
||||
this.$emit('add-child', this.category);
|
||||
},
|
||||
handleDelete() {
|
||||
this.$emit('delete', this.category);
|
||||
},
|
||||
handleEnable() {
|
||||
this.$emit('enable', this.category);
|
||||
}
|
||||
},
|
||||
template: `
|
||||
<div class="menu-item">
|
||||
<div class="menu-row" :style="indentStyle">
|
||||
<div class="menu-content">
|
||||
<div class="menu-header">
|
||||
<span
|
||||
v-if="hasChildren"
|
||||
class="expand-icon"
|
||||
@click="toggleExpanded"
|
||||
>
|
||||
<el-icon>
|
||||
<component :is="expanded ? 'ArrowDown' : 'ArrowRight'" />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span v-else class="expand-spacer"></span>
|
||||
|
||||
<span class="menu-label">{{ category.label }}</span>
|
||||
<span class="menu-value">({{ category.value }})</span>
|
||||
<el-tag
|
||||
:type="category.status === 1 ? 'success' : 'danger'"
|
||||
size="small"
|
||||
class="menu-status"
|
||||
>
|
||||
{{ category.status === 1 ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<div class="menu-actions">
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="handleEdit"
|
||||
>
|
||||
<el-icon><Edit /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
size="small"
|
||||
type="text"
|
||||
@click="handleAddChild"
|
||||
>
|
||||
<el-icon><Plus /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="category.status === 1"
|
||||
size="small"
|
||||
type="text"
|
||||
class="danger-btn"
|
||||
@click="handleDelete"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
<el-button
|
||||
v-else
|
||||
size="small"
|
||||
type="text"
|
||||
class="success-btn"
|
||||
@click="handleEnable"
|
||||
>
|
||||
<el-icon><Check /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasChildren && expanded" class="menu-children">
|
||||
<MenuItem
|
||||
v-for="child in category.children"
|
||||
:key="child.id"
|
||||
:category="child"
|
||||
:level="level + 1"
|
||||
@edit="$emit('edit', $event)"
|
||||
@add-child="$emit('add-child', $event)"
|
||||
@toggle="$emit('toggle', $event)"
|
||||
@delete="$emit('delete', $event)"
|
||||
@enable="$emit('enable', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
};
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const submitLoading = ref(false);
|
||||
const categoryList = ref([]);
|
||||
const treeData = ref([]);
|
||||
const searchQuery = ref('');
|
||||
const dialogVisible = ref(false);
|
||||
const isEdit = ref(false);
|
||||
const formRef = ref(null);
|
||||
|
||||
const formData = reactive({
|
||||
label: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
cid: 0
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
label: [
|
||||
{ required: true, message: '请输入分类名称', trigger: 'blur' },
|
||||
{ min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' }
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: '请输入分类值', trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: '分类值只能包含字母、数字和下划线', trigger: 'blur' }
|
||||
],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序值', trigger: 'change' }
|
||||
],
|
||||
cid: [
|
||||
{ required: false, message: '请选择父级分类', trigger: 'change' }
|
||||
]
|
||||
};
|
||||
|
||||
// 将平级数据转换为树形数据
|
||||
function buildTree(data) {
|
||||
const map = {};
|
||||
const roots = [];
|
||||
|
||||
// 建立映射表
|
||||
data.forEach(item => {
|
||||
map[item.id] = { ...item, children: [] };
|
||||
});
|
||||
|
||||
// 构建树形结构
|
||||
data.forEach(item => {
|
||||
const node = map[item.id];
|
||||
if (item.cid && item.cid !== 0 && item.cid !== null && map[item.cid]) {
|
||||
map[item.cid].children.push(node);
|
||||
} else {
|
||||
roots.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
return roots;
|
||||
}
|
||||
|
||||
// 获取分类列表
|
||||
function fetchCategories() {
|
||||
loading.value = true;
|
||||
const params = {
|
||||
keyword: searchQuery.value
|
||||
};
|
||||
|
||||
listCategories(params)
|
||||
.then((res) => {
|
||||
const resp = res?.data || res;
|
||||
if (resp?.code === 0 && resp?.data) {
|
||||
categoryList.value = resp.data.list || [];
|
||||
treeData.value = buildTree(resp.data.list || []);
|
||||
} else {
|
||||
categoryList.value = [];
|
||||
treeData.value = [];
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取分类列表失败:', error);
|
||||
ElMessage.error('获取分类列表失败');
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索
|
||||
function handleSearch() {
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
// 刷新
|
||||
function handleRefresh() {
|
||||
searchQuery.value = '';
|
||||
fetchCategories();
|
||||
}
|
||||
|
||||
// 新增分类
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
Object.assign(formData, {
|
||||
label: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
cid: null
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑分类
|
||||
function handleEdit(category) {
|
||||
isEdit.value = true;
|
||||
Object.assign(formData, {
|
||||
...category,
|
||||
cid: category.cid || null
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 添加子分类
|
||||
function handleAddChild(parent) {
|
||||
isEdit.value = false;
|
||||
Object.assign(formData, {
|
||||
label: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
status: 1,
|
||||
cid: parent.id
|
||||
});
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 切换展开状态
|
||||
function handleToggle(category, expanded) {
|
||||
// 这里可以添加展开状态的持久化逻辑
|
||||
console.log('Toggle:', category.label, expanded);
|
||||
}
|
||||
|
||||
// 删除(禁用)分类
|
||||
function handleDelete(category) {
|
||||
ElMessageBox.confirm(
|
||||
`确定要禁用分类"${category.label}"吗?`,
|
||||
'提示',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
updateCategoryStatus(category.id, 0)
|
||||
.then(() => {
|
||||
ElMessage.success('禁用成功');
|
||||
fetchCategories();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('禁用分类失败:', error);
|
||||
ElMessage.error('禁用分类失败');
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// 用户取消操作
|
||||
});
|
||||
}
|
||||
|
||||
// 启用分类
|
||||
function handleEnable(category) {
|
||||
updateCategoryStatus(category.id, 1)
|
||||
.then(() => {
|
||||
ElMessage.success('启用成功');
|
||||
fetchCategories();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('启用分类失败:', error);
|
||||
ElMessage.error('启用分类失败');
|
||||
});
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function handleSubmit() {
|
||||
formRef.value.validate((valid) => {
|
||||
if (valid) {
|
||||
submitLoading.value = true;
|
||||
const apiCall = isEdit.value
|
||||
? updateCategory(formData.id, formData)
|
||||
: createCategory(formData);
|
||||
|
||||
apiCall
|
||||
.then(() => {
|
||||
ElMessage.success(isEdit.value ? '编辑成功' : '新增成功');
|
||||
dialogVisible.value = false;
|
||||
fetchCategories();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(isEdit.value ? '编辑分类失败:' : '新增分类失败:', error);
|
||||
ElMessage.error(isEdit.value ? '编辑分类失败' : '新增分类失败');
|
||||
})
|
||||
.finally(() => {
|
||||
submitLoading.value = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchCategories();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.category-manager {
|
||||
padding: 20px;
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.search-wrapper {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.category-menu {
|
||||
background: #fff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 8px;
|
||||
min-height: 300px;
|
||||
|
||||
.empty-state {
|
||||
padding: 60px 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
.menu-row {
|
||||
padding: 8px 0;
|
||||
|
||||
.menu-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
|
||||
.expand-icon, .expand-spacer {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #c0c4cc;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-spacer {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-value {
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.menu-status {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.el-button {
|
||||
padding: 4px;
|
||||
color: #909399;
|
||||
|
||||
&:hover {
|
||||
color: #409eff;
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.danger-btn:hover {
|
||||
color: #f56c6c !important;
|
||||
background-color: rgba(245, 108, 108, 0.1) !important;
|
||||
}
|
||||
|
||||
.success-btn:hover {
|
||||
color: #67c23a !important;
|
||||
background-color: rgba(103, 194, 58, 0.1) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .menu-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-children {
|
||||
border-left: 2px solid #e4e7ed;
|
||||
margin-left: 16px;
|
||||
background-color: #fafbfc;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog) {
|
||||
.el-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -28,8 +28,8 @@
|
||||
{{ formatDate(model.publish_time) }}
|
||||
</span>
|
||||
<span class="meta-item">
|
||||
<el-tag :type="model?.status === 1 ? 'success' : model?.status === 2 ? 'danger' : 'info'">
|
||||
{{ model?.status === 1 ? '已发布' : model?.status === 2 ? '已下架' : '草稿' }}
|
||||
<el-tag>
|
||||
{{ (articleStatusDict.find(item => item.dict_value == String(model?.status)) || {}).dict_label || '-' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
@@ -45,8 +45,9 @@
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch } from 'vue';
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -63,6 +64,20 @@ const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
const visible = ref(false);
|
||||
|
||||
// 字典store
|
||||
const dictStore = useDictStore();
|
||||
const articleStatusDict = ref<any[]>([]);
|
||||
|
||||
// 获取审核状态字典
|
||||
const fetchArticleStatusDict = async () => {
|
||||
try {
|
||||
articleStatusDict.value = await dictStore.getDictItems('article_status');
|
||||
console.log(articleStatusDict.value);
|
||||
} catch (err) {
|
||||
console.error('获取文章状态字典失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听对话框显示状态
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal;
|
||||
@@ -107,6 +122,36 @@ function getCategoryLabel(category) {
|
||||
};
|
||||
return labels[category] || '其他';
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
function getStatusTagType(status) {
|
||||
const statusItem = articleStatusDict.value.find(item => item.dict_value === status);
|
||||
if (statusItem && statusItem.color) {
|
||||
// 如果字典项有颜色信息,根据颜色返回对应的标签类型
|
||||
const colorMap = {
|
||||
'blue': 'primary',
|
||||
'green': 'success',
|
||||
'orange': 'warning',
|
||||
'red': 'danger',
|
||||
'gray': 'info'
|
||||
};
|
||||
return colorMap[statusItem.color] || 'info';
|
||||
}
|
||||
|
||||
// 根据状态值设置默认的标签类型
|
||||
const statusTypes = {
|
||||
'0': 'info', // 草稿
|
||||
'1': 'warning', // 待审核
|
||||
'2': 'success', // 已发布
|
||||
'3': 'danger' // 隐藏
|
||||
};
|
||||
return statusTypes[status] || 'info';
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
fetchArticleStatusDict();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
{{ getLabel(customerStatusOptions, String(model.status ?? '')) || ((model.status===1||model.status==='1') ? '正常' : '停用') }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="经营范围" :span="2">{{ model.business_scope || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地址" :span="2">{{ model.address || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||
<el-form-item label="法人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入法人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
@@ -51,6 +51,14 @@
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input v-model="form.address" type="textarea" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="经营范围" prop="business_scope">
|
||||
<el-input
|
||||
v-model="form.business_scope"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入经营范围"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择客户状态" clearable>
|
||||
<el-option
|
||||
@@ -73,7 +81,7 @@
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createCustomer, updateCustomer } from '../../../../../api/customer.js'
|
||||
import { createCustomer, updateCustomer } from '@/api/customer.js'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, isEdit: boolean, model?: any }>()
|
||||
@@ -90,13 +98,14 @@ const form = reactive({
|
||||
id: null as string | null,
|
||||
name: '',
|
||||
customer_type: '',
|
||||
customer_level: '',
|
||||
customer_level: '3',
|
||||
industry: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
status: '' as any
|
||||
business_scope: '',
|
||||
status: '1'
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
@@ -104,8 +113,6 @@ const rules: FormRules = {
|
||||
customer_type: [{ required: true, message: '请选择客户类型', trigger: 'change' }],
|
||||
customer_level: [{ required: true, message: '请选择客户等级', trigger: 'change' }],
|
||||
industry: [{ required: true, message: '请选择所属行业', trigger: 'change' }],
|
||||
contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
|
||||
phone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const customerTypeOptions = ref<{label:string,value:string}[]>([])
|
||||
@@ -142,6 +149,7 @@ watch(() => props.model, (m) => {
|
||||
form.phone = (m.contact_phone ?? m.phone) ?? ''
|
||||
form.email = (m.contact_email ?? m.email) ?? ''
|
||||
form.address = m.address ?? ''
|
||||
form.business_scope = m.business_scope ?? ''
|
||||
form.status = m.status != null && m.status !== undefined ? String(m.status) : ''
|
||||
} else {
|
||||
resetForm()
|
||||
@@ -152,13 +160,14 @@ function resetForm() {
|
||||
form.id = null
|
||||
form.name = ''
|
||||
form.customer_type = ''
|
||||
form.customer_level = ''
|
||||
form.customer_level = '3'
|
||||
form.industry = ''
|
||||
form.contact = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.address = ''
|
||||
form.status = ''
|
||||
form.business_scope = ''
|
||||
form.status = '1'
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
@@ -187,6 +196,7 @@ async function onSubmit() {
|
||||
contact_phone: form.phone,
|
||||
contact_email: form.email,
|
||||
address: form.address,
|
||||
business_scope: form.business_scope,
|
||||
status: String(form.status),
|
||||
tenant_id: tenantId,
|
||||
}
|
||||
|
||||
@@ -16,22 +16,23 @@
|
||||
|
||||
<!-- 客户列表 -->
|
||||
<el-table :data="customerList" v-loading="loading" stripe border style="width: 100%">
|
||||
<el-table-column prop="name" label="客户名称" width="220" />
|
||||
<el-table-column prop="contact" label="联系人" width="100" />
|
||||
<el-table-column prop="phone" label="联系电话" min-width="120" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="150" />
|
||||
<el-table-column prop="address" label="地址" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<el-table-column prop="name" label="客户名称" width="360" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
|
||||
{{ row.status === 1 ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
<el-link type="primary" @click="handleView(row)">{{ row.name }}</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right" align="center">
|
||||
<el-table-column prop="contact" label="法人" width="100" />
|
||||
<el-table-column prop="phone" label="联系电话" min-width="160" />
|
||||
<el-table-column prop="email" label="邮箱" min-width="180" />
|
||||
<el-table-column prop="address" label="地址" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="250" fixed="right" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="info" @click="handleContactView(row)">联系人</el-button>
|
||||
<el-button link type="info" @click="handleView(row)">详情</el-button>
|
||||
<el-button link type="info" @click="handleInvoiceView(row)">开票信息</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
{{ getDictLabel('supplier_status', model.status) || ((model.status===1||model.status==='1') ? '正常' : '停用') }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="经营范围" :span="2">{{ model.business_scope || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地址" :span="2">{{ model.address || '-' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template #footer>
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
<el-option v-for="item in industryOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入联系人" />
|
||||
<el-form-item label="法人" prop="contact">
|
||||
<el-input v-model="form.contact" placeholder="请输入法人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" />
|
||||
@@ -36,6 +36,14 @@
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input v-model="form.address" type="textarea" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="经营范围" prop="business_scope">
|
||||
<el-input
|
||||
v-model="form.business_scope"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="请输入经营范围"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商状态" prop="status">
|
||||
<el-select v-model="form.status" placeholder="请选择供应商状态" clearable>
|
||||
<el-option v-for="item in supplierStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
@@ -53,7 +61,7 @@
|
||||
import { ref, reactive, watch, computed, onMounted } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { createSupplier, updateSupplier } from '../../../../../api/supplier.js'
|
||||
import { createSupplier, updateSupplier } from '@/api/supplier.js'
|
||||
import request from '@/utils/request'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean, isEdit: boolean, model?: any }>()
|
||||
@@ -70,13 +78,14 @@ const form = reactive({
|
||||
id: null as string | null,
|
||||
name: '',
|
||||
supplier_type: '',
|
||||
supplier_level: '',
|
||||
supplier_level: '3',
|
||||
industry: '',
|
||||
contact: '',
|
||||
phone: '',
|
||||
email: '',
|
||||
address: '',
|
||||
status: '' as any
|
||||
business_scope: '',
|
||||
status: '1'
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
@@ -122,6 +131,7 @@ watch(() => props.model, (m) => {
|
||||
form.phone = (m.contact_phone ?? m.phone) ?? ''
|
||||
form.email = (m.contact_email ?? m.email) ?? ''
|
||||
form.address = m.address ?? ''
|
||||
form.business_scope = m.business_scope ?? ''
|
||||
form.status = m.status != null && m.status !== undefined ? String(m.status) : ''
|
||||
} else {
|
||||
resetForm()
|
||||
@@ -132,13 +142,14 @@ function resetForm() {
|
||||
form.id = null
|
||||
form.name = ''
|
||||
form.supplier_type = ''
|
||||
form.supplier_level = ''
|
||||
form.supplier_level = '3'
|
||||
form.industry = ''
|
||||
form.contact = ''
|
||||
form.phone = ''
|
||||
form.email = ''
|
||||
form.address = ''
|
||||
form.status = ''
|
||||
form.business_scope = ''
|
||||
form.status = '1'
|
||||
formRef.value?.clearValidate()
|
||||
}
|
||||
|
||||
@@ -167,6 +178,7 @@ async function onSubmit() {
|
||||
contact_phone: form.phone,
|
||||
contact_email: form.email,
|
||||
address: form.address,
|
||||
business_scope: form.business_scope,
|
||||
status: String(form.status),
|
||||
tenant_id: tenantId,
|
||||
}
|
||||
|
||||
@@ -27,22 +27,23 @@
|
||||
border
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column prop="supplier_name" label="供应商名称" width="220" />
|
||||
<el-table-column prop="contact_person" label="联系人" width="140" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" width="160" />
|
||||
<el-table-column prop="contact_email" label="邮箱" width="220" />
|
||||
<el-table-column prop="address" label="地址" show-overflow-tooltip />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<el-table-column prop="supplier_name" label="供应商名称" width="360" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<el-tag :type="row.status === '1' ? 'success' : 'info'" size="small">
|
||||
{{ row.status === '1' ? '正常' : '停用' }}
|
||||
</el-tag>
|
||||
<el-link type="primary" @click="handleView(row)">{{ row.supplier_name }}</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<el-table-column prop="contact_person" label="法人" width="100" />
|
||||
<el-table-column prop="contact_phone" label="联系电话" width="160" />
|
||||
<el-table-column prop="contact_email" label="邮箱" min-width="180" />
|
||||
<el-table-column prop="address" label="地址" min-width="300" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="250" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="info" @click="handleContactView(row)">联系人</el-button>
|
||||
<el-button link type="info" @click="handleView(row)">详情</el-button>
|
||||
<el-button link type="info" @click="handleInvoiceView(row)">开票信息</el-button>
|
||||
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
|
||||
Reference in New Issue
Block a user