更新需求模块
This commit is contained in:
parent
07d3614d9c
commit
e291e2c48f
51
src/api/demand.js
Normal file
51
src/api/demand.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取需求列表
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function getDemandList() {
|
||||||
|
return request({
|
||||||
|
url: "/admin/demandList",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新增需求
|
||||||
|
* @param {Object} data 需求数据
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function addDemand(data) {
|
||||||
|
return request({
|
||||||
|
url: "/admin/addDemand",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑需求
|
||||||
|
* @param {number} id 需求ID
|
||||||
|
* @param {Object} data 需求数据
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function editDemand(id, data) {
|
||||||
|
return request({
|
||||||
|
url: `/admin/editDemand/${id}`,
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除需求
|
||||||
|
* @param {number} id 需求ID
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
export function deleteDemand(id) {
|
||||||
|
return request({
|
||||||
|
url: `/admin/deleteDemand/${id}`,
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
}
|
||||||
36
src/api/theme.js
Normal file
36
src/api/theme.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
// 获取模板列表
|
||||||
|
export function getThemeList() {
|
||||||
|
return request({
|
||||||
|
url: '/admin/theme',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换模板
|
||||||
|
export function switchTheme(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/theme/switch',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取模板数据
|
||||||
|
export function getThemeData(params) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/theme/data',
|
||||||
|
method: 'get',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存模板数据
|
||||||
|
export function saveThemeData(data) {
|
||||||
|
return request({
|
||||||
|
url: '/admin/theme/data',
|
||||||
|
method: 'post',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import 'element-plus/dist/index.css'
|
|||||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
import '@/assets/less/index.less'
|
import '@/assets/less/index.less'
|
||||||
import '@/assets/css/all.min.css'
|
import '@/assets/css/all.min.css'
|
||||||
|
import '@/assets/js/all.min.js'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import { loadAndAddDynamicRoutes } from './router'
|
import { loadAndAddDynamicRoutes } from './router'
|
||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
|
|||||||
@ -24,22 +24,6 @@ service.interceptors.request.use(
|
|||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
config.headers['Authorization'] = `Bearer ${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 禁止 GET 请求缓存:添加时间戳参数到 query string
|
|
||||||
if (config.method === 'get') {
|
|
||||||
config.params = {
|
|
||||||
...config.params,
|
|
||||||
_t: Date.now()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST/PUT/PATCH 请求也添加时间戳到 query string 防止缓存
|
|
||||||
if (['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
|
||||||
config.params = {
|
|
||||||
...config.params,
|
|
||||||
_t: Date.now()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 对于有 body 的请求(POST、PUT、PATCH),确保设置 Content-Type
|
// 对于有 body 的请求(POST、PUT、PATCH),确保设置 Content-Type
|
||||||
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
if (config.data && ['post', 'put', 'patch'].includes(config.method?.toLowerCase())) {
|
||||||
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
if (!config.headers['Content-Type'] && !config.headers['content-type']) {
|
||||||
|
|||||||
150
src/views/apps/cms/demand/components/edit.vue
Normal file
150
src/views/apps/cms/demand/components/edit.vue
Normal file
@ -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
src/views/apps/cms/demand/index.vue
Normal file
333
src/views/apps/cms/demand/index.vue
Normal 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>
|
||||||
@ -2,7 +2,6 @@
|
|||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup></script>
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
<style lang="less" scoped></style>
|
<style lang="less" scoped></style>
|
||||||
|
|
||||||
379
src/views/apps/cms/templates/index.vue
Normal file
379
src/views/apps/cms/templates/index.vue
Normal file
@ -0,0 +1,379 @@
|
|||||||
|
<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="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>
|
||||||
|
<el-button size="small" @click="handlePreview(item)">预览</el-button>
|
||||||
|
<el-button size="small" @click="handleEdit(item)">编辑数据</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- 预览弹窗 -->
|
||||||
|
<el-dialog v-model="previewVisible" title="模板预览" width="90%" top="5vh">
|
||||||
|
<iframe :src="previewUrl" class="preview-iframe" frameborder="0"></iframe>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 编辑数据弹窗 -->
|
||||||
|
<el-dialog v-model="editVisible" title="编辑模板数据" width="800px">
|
||||||
|
<el-form :model="editForm" label-width="100px">
|
||||||
|
<el-form-item label="模板">
|
||||||
|
<el-input v-model="editForm.theme_key" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选择字段">
|
||||||
|
<el-select v-model="selectedField" placeholder="选择要编辑的字段" @change="handleFieldChange">
|
||||||
|
<el-option v-for="(label, key) in editForm.fields" :key="key" :label="label" :value="key" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="selectedField" label="字段值">
|
||||||
|
<el-input
|
||||||
|
v-model="fieldValue"
|
||||||
|
type="textarea"
|
||||||
|
:rows="10"
|
||||||
|
placeholder="输入 JSON 格式或普通文本"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSaveData" :loading="saving">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</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, getThemeData, saveThemeData } from '@/api/theme'
|
||||||
|
|
||||||
|
// 状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const switching = ref('')
|
||||||
|
const saving = ref(false)
|
||||||
|
const templateList = ref<any[]>([])
|
||||||
|
const currentTheme = ref('default')
|
||||||
|
const previewVisible = ref(false)
|
||||||
|
const previewUrl = ref('')
|
||||||
|
const editVisible = ref(false)
|
||||||
|
const editForm = ref<any>({})
|
||||||
|
const selectedField = ref('')
|
||||||
|
const fieldValue = ref('')
|
||||||
|
|
||||||
|
// 获取模板列表
|
||||||
|
const fetchTemplates = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getThemeList()
|
||||||
|
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) => {
|
||||||
|
switching.value = item.key
|
||||||
|
try {
|
||||||
|
const res = await switchTheme({ 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 handlePreview = (item: any) => {
|
||||||
|
previewUrl.value = item.path
|
||||||
|
previewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑模板数据
|
||||||
|
const handleEdit = async (item: any) => {
|
||||||
|
editForm.value = { ...item, fields: item.fields || {} }
|
||||||
|
selectedField.value = ''
|
||||||
|
fieldValue.value = ''
|
||||||
|
|
||||||
|
// 获取已保存的数据
|
||||||
|
try {
|
||||||
|
const res = await getThemeData({ theme_key: item.key })
|
||||||
|
if (res.code === 200 && res.data.data) {
|
||||||
|
// 预填充已有数据
|
||||||
|
const savedData = res.data.data
|
||||||
|
// 找到第一个有值的字段
|
||||||
|
for (const key in savedData) {
|
||||||
|
if (savedData[key]) {
|
||||||
|
selectedField.value = key
|
||||||
|
fieldValue.value = typeof savedData[key] === 'object'
|
||||||
|
? JSON.stringify(savedData[key], null, 2)
|
||||||
|
: savedData[key]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取模板数据失败', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
editVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 字段变更
|
||||||
|
const handleFieldChange = (field: string) => {
|
||||||
|
fieldValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存数据
|
||||||
|
const handleSaveData = async () => {
|
||||||
|
if (!selectedField.value) {
|
||||||
|
ElMessage.warning('请选择要编辑的字段')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
const res = await saveThemeData({
|
||||||
|
theme_key: editForm.value.key,
|
||||||
|
field_key: selectedField.value,
|
||||||
|
field_value: fieldValue.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.code === 200) {
|
||||||
|
ElMessage.success('保存成功')
|
||||||
|
editVisible.value = false
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.msg || '保存失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('保存失败')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片加载失败处理
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 70vh;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user