This commit is contained in:
Your Name
2026-04-15 12:00:09 +08:00
parent 2962da0907
commit 845d68110b
426 changed files with 43961 additions and 34179 deletions

View File

@@ -1,91 +1,30 @@
<template>
<div class="table-box">
<ProTable
ref="proTable"
highlight-current-row
:columns="columns"
:request-api="getTableList"
:data-callback="dataCallback"
>
</ProTable>
<div class="page-box">
<el-alert
title="当前测试服未部署小说章节管理"
type="warning"
:closable="false"
show-icon
/>
<div class="page-box__body">
<p>当前页面依赖的 `/xiaoshuo/zhangjie/list``/admin/novel/chapter/save``/admin/novel/chapter/del` 都不是测试服现有后端主线</p>
<p>如果后续要恢复这一页需要先补齐章节接口再重新接回列表保存和删除动作</p>
</div>
</div>
</template>
<script setup lang="tsx" name="complexProTable">
import { reactive, ref } from "vue";
import ProTable from "@/components/ProTable/index.vue";
import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import { ChapterList } from "@/api/interface/novel/chapter";
import { getList } from "@/api/modules/novel/chapter";
// ProTable 实例
const proTable = ref<ProTableInstance>();
const initParam = reactive({
limit: 15,
page: 1
});
const dataCallback = (data: any) => {
return {
items: data.items,
total: data.total,
total_page: data.total_pages,
page: initParam.page,
limit: initParam.limit
};
};
const getTableList = (params: any) => {
let newParams = JSON.parse(JSON.stringify(params));
initParam.page = newParams.page;
initParam.limit = newParams.limit;
return getList(newParams);
};
// 表格配置项
const columns = reactive<ColumnProps<ChapterList.ResList>[]>([
{ type: "selection", fixed: "left", width: 50 },
{
prop: "key",
label: "搜索内容",
isShow: false,
search: {
el: "input"
}
},
{
prop: "xszj_id",
label: "ID",
width: 80
},
{
prop: "xszj_pai_xu",
label: "章节序号",
width: 100
},
{
prop: "xszj_ming_zi",
label: "章节标题"
},
{
prop: "xszj_nei_rong",
label: "章节内容",
showOverflowTooltip: true,
render(scope) {
return (
<div class="fixed-width" v-html={scope.row.xszj_nei_rong}>
{scope.row.xszj_nei_rong}
</div>
);
}
},
{ prop: "created_at", label: "添加时间", width: 170 }
]);
</script>
<style lang="scss">
.el-upload--picture-card {
display: none;
.page-box {
display: flex;
flex-direction: column;
gap: 16px;
}
.page-box__body {
padding: 20px 24px;
border-radius: 8px;
background: var(--el-fill-color-light);
color: var(--el-text-color-regular);
line-height: 1.8;
}
</style>

View File

@@ -1,229 +0,0 @@
<template>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" size="720" :title="`${drawerProps.title}`">
<el-form
ref="ruleFormRef"
label-width="90px"
label-suffix=" :"
:rules="rules"
:disabled="drawerProps.isView"
:model="drawerProps.row"
:hide-required-asterisk="drawerProps.isView"
>
<el-form-item v-for="e in formColumns" :key="e.label" :label="e.label" :prop="e.prop" :required="e.required">
<template v-if="e.type === 'switch'">
<el-switch
v-model="e.value"
:active-value="`${1}`"
:inactive-value="`${0}`"
active-text="上架"
inactive-text="下架"
/>
</template>
<template v-if="e.type === 'switch-index'">
<el-switch
v-model="e.value"
:active-value="`${1}`"
:inactive-value="`${0}`"
active-text=""
inactive-text=""
/>
</template>
<template v-if="e.type === 'radio'">
<el-radio-group v-model="e.value" class="ml-4">
<el-radio v-for="item in e.options" :key="item.value" :label="item.value" size="large">{{
item.label
}}</el-radio>
</el-radio-group>
</template>
<template v-if="e.type === 'select'">
<el-select filterable v-model="e.value" :placeholder="`请选择${e.label}`" clearable>
<el-option
v-for="(item, index) in e.options"
:key="index"
:label="getField(item, e.fieldNames?.label)"
:value="getField(item, e.fieldNames?.value)"
/>
</el-select>
</template>
<template v-if="e.type === 'text'">
<el-input v-model="e.value" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'text-i'">
<el-input v-model="e.value" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'text-num'">
<el-input v-model="e.value" type="number" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'textarea'">
<el-input
type="textarea"
v-model="e.value"
maxlength="36500"
:autosize="{ minRows: 25, maxRows: 32 }"
:placeholder="`请输入${e.label}`"
show-word-limit
/>
</template>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="drawerVisible = false">取消</el-button>
<el-button v-show="!drawerProps.isView" type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-drawer>
</template>
<script setup lang="ts" name="UserDrawer">
import { ref, computed } from "vue";
import { ElMessage, FormInstance } from "element-plus";
import { ChapterList } from "@/api/interface/novel/chapter";
type Options = {
label: string;
value: string | number;
};
const getField = (item: any, fieldName: string | undefined) => {
return fieldName ? item[fieldName] : "";
};
type Fields = {
label: string;
prop: string;
type: string;
value: any;
required: boolean;
options?: Options[];
fieldNames?: { label: string; value: string };
};
const generateRules = (fields: Fields[]) => {
return fields.reduce(
(acc, field) => {
const { type, required, label, prop } = field;
let message = "";
if (type === "select") {
message = `请选择${label}`;
}
if (type === "tag") {
message = `请选择${label}`;
}
if (type === "radio") {
message = `请选择${label}`;
}
if (type === "text") {
message = `请输入${label}`;
}
if (type === "text-i") {
message = `请输入${label}`;
}
if (type === "text-num") {
message = `请输入${label}`;
}
if (type === "image") {
message = `请上传${label}`;
}
if (type === "image-more") {
message = `请上传${label}`;
}
acc[prop] = [{ required, message }];
return acc;
},
{} as Record<string, any>
);
};
const formColumns = computed((): Fields[] => {
return [
{
label: "章节标题",
prop: "nc_title",
type: "text",
get value() {
return drawerProps.value.row!.nc_title;
},
set value(val) {
drawerProps.value.row!.nc_title = val;
},
required: true
},
{
label: "章节序号",
prop: "nc_index",
type: "text-num",
get value() {
return drawerProps.value.row!.nc_index;
},
set value(val) {
drawerProps.value.row!.nc_index = val;
},
required: true
},
{
label: "阅读次数",
prop: "nc_hits",
type: "text-num",
get value() {
return drawerProps.value.row!.nc_hits;
},
set value(val) {
drawerProps.value.row!.nc_hits = val;
},
required: false
},
{
label: "章节内容",
prop: "nc_content",
type: "textarea",
get value() {
return drawerProps.value.row!.nc_content;
},
set value(val) {
drawerProps.value.row!.nc_content = val;
},
required: true
}
];
});
const rules = ref();
interface DrawerProps {
title: string;
isView: boolean;
row: Partial<ChapterList.ResList>;
api?: (params: any) => Promise<any>;
getTableList?: () => void;
}
const drawerVisible = ref(false);
const drawerProps = ref<DrawerProps>({
isView: false,
title: "",
row: {}
});
// 接收父组件传过来的参数
const acceptParams = (params: DrawerProps) => {
drawerProps.value = params;
rules.value = generateRules(formColumns.value);
drawerVisible.value = true;
};
// 提交数据(新增/编辑)
const ruleFormRef = ref<FormInstance>();
const handleSubmit = () => {
ruleFormRef.value!.validate(async valid => {
if (!valid) return;
try {
const params = { ...drawerProps.value.row };
await drawerProps.value.api!(params);
ElMessage.success({ message: `${drawerProps.value.title}成功!` });
drawerProps.value.getTableList!();
drawerVisible.value = false;
} catch (error) {
console.log(error);
}
});
};
defineExpose({
acceptParams
});
</script>

View File

@@ -1,181 +0,0 @@
<template>
<el-drawer v-model="drawerVisible" size="80%" title="章节列表" :destroy-on-close="true">
<ProTable
ref="proTable"
highlight-current-row
:columns="columns"
:request-api="getTableList"
:data-callback="dataCallback"
:init-param="initParam"
:is-displayed="isDisplayed"
:is-search-location="isSearchLocation"
>
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<el-button type="primary" style="display: none" :icon="CirclePlus" @click="openDrawer(true)">
新增小说章节
</el-button>
<el-button
style="display: none"
type="danger"
:icon="Delete"
plain
:disabled="!scope.isSelected"
@click="batchDelete(scope.selectedList)"
>
批量删除章节
</el-button>
</template>
<!-- 表格操作 -->
<template #operation="scope">
<el-button type="primary" link :icon="EditPen" @click="openDrawer(false, scope.row)">编辑</el-button>
<el-button type="danger" link :icon="Delete" @click="deleteAccount(scope.row)">删除</el-button>
</template>
</ProTable>
<template #footer>
<el-button @click="drawerVisible = false">取消</el-button>
<el-button @click="drawerVisible = false" type="primary">确定</el-button>
</template>
<ChapterDrawer ref="chapterDrawerRef" />
</el-drawer>
</template>
<script setup lang="tsx" name="UserDrawer">
import { ref, reactive } from "vue";
import ProTable from "@/components/ProTable/index.vue";
import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
import { useHandleData } from "@/hooks/useHandleData";
import { Delete, EditPen, CirclePlus } from "@element-plus/icons-vue";
import { ChapterList } from "@/api/interface/novel/chapter";
import { getList, saveData, deleteItem } from "@/api/modules/novel/chapter";
import ChapterDrawer from "../component/chapterDrawer.vue";
// ProTable 实例
const proTable = ref<ProTableInstance>();
// 如果表格需要初始化请求参数,直接定义传给 ProTable(之后每次请求都会自动带上该参数,此参数更改之后也会一直带上,改变此参数会自动刷新表格数据)
const initParam = reactive({
xsxq_id: 0,
limit: 15,
page: 1
});
const dataCallback = (data: any) => {
return {
items: data.items,
total: data.total,
total_page: data.total_pages,
page: initParam.page,
limit: initParam.limit
};
};
const getTableList = (params: any) => {
let newParams = JSON.parse(JSON.stringify(params));
initParam.page = newParams.page;
initParam.limit = newParams.limit;
return getList(newParams);
};
const isDisplayed = ref<any>(false);
const isSearchLocation = ref<any>(false);
// 表格配置项
const columns = reactive<ColumnProps<ChapterList.ResList>[]>([
{ type: "selection", fixed: "left", width: 50 },
{
prop: "key",
label: "搜索内容",
isShow: false,
search: {
el: "input"
}
},
{
prop: "xszj_id",
label: "ID",
width: 80
},
{
prop: "xszj_pai_xu",
label: "章节序号",
width: 100
},
{
prop: "xszj_ming_zi",
label: "章节标题"
},
{
prop: "xszj_nei_rong",
label: "章节内容",
showOverflowTooltip: true,
render(scope) {
return (
<div class="fixed-width" v-html={scope.row.xszj_nei_rong}>
{scope.row.xszj_nei_rong}
</div>
);
}
},
{ prop: "created_at", label: "添加时间", width: 170 }
// { prop: "operation", label: "操作", fixed: "right", width: 200 }
]);
const drawerVisible = ref(false);
// 接收父组件传过来的参数
const acceptParams = (xsxq_id: number) => {
drawerVisible.value = true;
initParam.xsxq_id = xsxq_id;
};
// 删除章节
const deleteAccount = async (params: ChapterList.ResList) => {
await useHandleData(deleteItem, { xszj_id: params.xszj_id, xsxq_id: initParam.xsxq_id }, "删除所选信息");
proTable.value?.getTableList();
};
// 批量删除章节
const batchDelete = async (id: any[]) => {
const ids = id.map((item: ChapterList.ResList) => item.xszj_id);
await useHandleData(deleteItem, { xszj_id: ids.join(","), xsxq_id: initParam.xsxq_id }, "删除所选信息");
proTable.value?.clearSelection();
proTable.value?.getTableList();
};
// 打开 drawer(新增、编辑)
const chapterDrawerRef = ref<InstanceType<typeof ChapterDrawer> | null>(null);
const openDrawer = (isEdit: boolean, row: Partial<ChapterList.ResList> = {}) => {
row.xsxq_id = initParam.xsxq_id;
const params = {
title: isEdit ? "新增小说章节" : "编辑小说章节",
row: { ...row },
api: saveData,
getTableList: proTable.value?.getTableList
};
chapterDrawerRef.value?.acceptParams(params as any);
};
defineExpose({
acceptParams
});
</script>
<style lang="scss" scoped>
.demo-form-inline {
.el-form-item {
width: 420px;
}
:deep(.el-form-item__content) {
display: flex;
flex-wrap: nowrap;
align-items: flex-start;
.tips {
margin-left: 10px;
font-size: 12px;
color: #999999;
}
}
}
.btn-group {
text-align: center;
}
</style>

View File

@@ -1,457 +0,0 @@
<template>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" size="720" :title="`${drawerProps.title}`">
<el-form
ref="ruleFormRef"
label-width="90px"
label-suffix=" :"
:rules="rules"
:disabled="drawerProps.isView"
:model="drawerProps.row"
:hide-required-asterisk="drawerProps.isView"
>
<el-form-item v-for="e in formColumns" :key="e.label" :label="e.label" :prop="e.prop" :required="e.required">
<template v-if="e.type === 'switch'">
<el-switch
v-model="e.value"
:active-value="`${1}`"
:inactive-value="`${0}`"
active-text="上架"
inactive-text="下架"
/>
</template>
<template v-if="e.type === 'switch-index'">
<el-switch
v-model="e.value"
:active-value="`${1}`"
:inactive-value="`${0}`"
active-text=""
inactive-text=""
/>
</template>
<template v-if="e.type === 'radio'">
<el-radio-group v-model="e.value" class="ml-4">
<el-radio v-for="item in e.options" :key="item.value" :value="item.value" size="large">{{
item.label
}}</el-radio>
</el-radio-group>
</template>
<template v-if="e.type === 'select'">
<el-select filterable v-model="e.value" :placeholder="`请选择${e.label}`" clearable>
<el-option
v-for="(item, index) in e.options"
:key="index"
:label="getField(item, e.fieldNames?.label)"
:value="getField(item, e.fieldNames?.value)"
/>
</el-select>
</template>
<template v-if="e.type === 'text'">
<el-input v-model="e.value" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'text-i'">
<el-input v-model="e.value" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'text-num'">
<el-input v-model="e.value" type="number" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'textarea'">
<el-input
type="textarea"
v-model="e.value"
maxlength="3650"
:placeholder="`请输入${e.label}`"
show-word-limit
/>
</template>
<template v-if="e.type === 'tag'">
<div class="tag-more-s">
<el-input
v-model="drawerProps.row.searchText"
placeholder="请输入搜索内容"
clearable
@input="handleSearch"
/>
<div>
<span>选中标签:</span>
<div class="tag-more-h">
<el-checkbox-group v-model="TagList" @change="placeableCheckedCitiesChange">
<el-checkbox-button
class="vip-power"
v-for="item in matchingObjects"
:value="item.t_id"
:key="item.t_id"
>
{{ item.t_name }}
</el-checkbox-button>
</el-checkbox-group>
</div>
</div>
<div>
<span>未选中标签:</span>
<div class="tag-more-h">
<el-checkbox-group v-model="TagList" @change="placeableCheckedCitiesChange">
<el-checkbox-button
class="vip-power"
v-for="item in nonMatchingObjects"
:value="item.t_id"
:key="item.t_id"
>
{{ item.t_name }}
</el-checkbox-button>
</el-checkbox-group>
</div>
</div>
</div>
</template>
<template v-if="e.type === 'text-i'">
<UploadImg disabled v-model:image-url="drawerProps.row!.n_cover" width="135px" height="135px" :file-size="3">
<template #empty>
<el-icon><Avatar /></el-icon>
<span>请上传封面</span>
</template>
</UploadImg>
</template>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="drawerVisible = false">取消</el-button>
<el-button v-show="!drawerProps.isView" type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-drawer>
</template>
<script setup lang="ts" name="UserDrawer">
import { ref, computed, reactive } from "vue";
import { ElMessage, FormInstance } from "element-plus";
import { NovelList } from "@/api/interface/novel/list";
import { arrNovelType } from "@/utils/serviceDict";
import { getTypeList } from "@/api/modules/tag/list";
import UploadImg from "@/components/Upload/Img.vue";
import { extractURI } from "@/utils/eleValidate";
type Options = {
label: string;
value: string | number;
};
interface Item {
t_id: any;
uri: any;
}
const getField = (item: any, fieldName: string | undefined) => {
return fieldName ? item[fieldName] : "";
};
type Fields = {
label: string;
prop: string;
type: string;
value: any;
required: boolean;
options?: Options[];
fieldNames?: { label: string; value: string };
};
const generateRules = (fields: Fields[]) => {
return fields.reduce(
(acc, field) => {
const { type, required, label, prop } = field;
let message = "";
if (type === "select") {
message = `请选择${label}`;
}
if (type === "tag") {
message = `请选择${label}`;
}
if (type === "radio") {
message = `请选择${label}`;
}
if (type === "text") {
message = `请输入${label}`;
}
if (type === "text-i") {
message = `请输入${label}`;
}
if (type === "text-num") {
message = `请输入${label}`;
}
if (type === "image") {
message = `请上传${label}`;
}
if (type === "image-more") {
message = `请上传${label}`;
}
acc[prop] = [{ required, message }];
return acc;
},
{} as Record<string, any>
);
};
const TagList = ref([] as any);
const arrTagList = ref([] as any);
const arrNewTagList = ref([] as any);
const matchingObjects = ref([] as any); //选中相同
const nonMatchingObjects = ref([] as any); //没有选中的
const matchingObjectsA = ref([] as any); //选中相同
const nonMatchingObjectsB = ref([] as any); //没有选中的
const getTagList = async () => {
try {
const initParam = reactive({
t_type: 3,
limit: 3650,
page: 1,
t_hidden: 1
});
const siteRes = await getTypeList(initParam);
arrTagList.value = siteRes.data.item;
arrNewTagList.value = siteRes.data.item;
matchingObjects.value = [];
matchingObjectsA.value = [];
// 遍历第一个数组的每个元素
TagList.value.forEach(index => {
// 遍历第二个数组的每个元素
arrNewTagList.value.forEach(obj => {
// 如果第二个数组中的元素的t_id与第一个数组中的元素相同则将其添加到新数组中
if (obj.t_id === index) {
matchingObjects.value = [...matchingObjects.value, obj];
matchingObjectsA.value = [...matchingObjectsA.value, obj];
const matchingIds = matchingObjectsA.value.map(obj => obj.t_id);
TagList.value = matchingIds;
}
});
});
// 创建一个新数组包含objectArray中id不存在于arrayA的元素
nonMatchingObjects.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
nonMatchingObjectsB.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
} catch (error) {
console.log(error);
}
};
// 处理搜索事件
function handleSearch() {
// 在输入事件中实时更新过滤后的标签列表
filteredTagList();
}
function filteredTagList() {
if (drawerProps.value.row!.searchText.length > 0) {
let arrData = arrTagList.value.filter(tag =>
tag.t_name.toLowerCase().includes(drawerProps.value.row!.searchText.toLowerCase())
);
if (arrData.length > 0) {
matchingObjects.value = matchingObjects.value.filter(objA => arrData.some(objB => objB.t_id === objA.t_id));
nonMatchingObjects.value = nonMatchingObjects.value.filter(objA => arrData.some(objB => objB.t_id === objA.t_id));
} else {
matchingObjects.value = [];
nonMatchingObjects.value = [];
}
} else {
matchingObjects.value = matchingObjectsA.value;
nonMatchingObjects.value = nonMatchingObjectsB.value;
}
}
//标签数组
async function placeableCheckedCitiesChange(value) {
matchingObjects.value = [];
matchingObjectsA.value = [];
drawerProps.value.row.n_tags = value;
// 遍历第一个数组的每个元素
TagList.value.forEach(index => {
// 遍历第二个数组的每个元素
arrNewTagList.value.forEach(obj => {
// 如果第二个数组中的元素的t_id与第一个数组中的元素相同则将其添加到新数组中
if (obj.t_id === index) {
matchingObjects.value = [...matchingObjects.value, obj];
matchingObjectsA.value = [...matchingObjectsA.value, obj];
const matchingIds = matchingObjectsA.value.map(obj => obj.t_id);
TagList.value = matchingIds;
}
});
});
nonMatchingObjects.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
nonMatchingObjectsB.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
}
const formColumns = computed((): Fields[] => {
return [
{
label: "小说名称",
prop: "n_name",
type: "text",
get value() {
return drawerProps.value.row!.n_name;
},
set value(val) {
drawerProps.value.row!.n_name = val;
},
required: true
},
{
label: "小说作者",
prop: "n_author",
type: "text",
get value() {
return drawerProps.value.row!.n_author;
},
set value(val) {
drawerProps.value.row!.n_author = val;
},
required: true
},
{
label: "来源 ID",
prop: "n_source_id",
type: "text-num",
get value() {
return drawerProps.value.row!.n_source_id;
},
set value(val) {
drawerProps.value.row!.n_source_id = val;
},
required: true
},
{
label: "小说状态",
prop: "n_at_status",
type: "switch",
get value() {
return String(drawerProps.value.row!.n_at_status);
},
set value(val) {
drawerProps.value.row!.n_at_status = val;
},
required: true
},
{
label: "小说类型",
prop: "n_type",
type: "radio",
get value() {
return drawerProps.value.row!.n_type;
},
set value(val) {
drawerProps.value.row!.n_type = val;
},
options: arrNovelType,
fieldNames: { label: "label", value: "value" },
required: true
},
// {
// label: "首页轮播状态",
// prop: "n_slide_status",
// type: "switch-index",
// get value() {
// return String(drawerProps.value.row!.n_slide_status);
// },
// set value(val) {
// drawerProps.value.row!.n_slide_status = val;
// },
// required: true
// },
// {
// label: "封面",
// prop: "n_cover",
// type: "text-i",
// get value() {
// return drawerProps.value.row!.n_cover;
// },
// set value(val) {
// drawerProps.value.row!.n_cover = val;
// },
// required: true
// },
{
label: "小说标签",
prop: "n_tags",
type: "tag",
get value() {
return drawerProps.value.row!.n_tags;
},
set value(val) {
drawerProps.value.row!.n_tags = val;
},
options: arrTagList.value,
fieldNames: { label: "label", value: "value" },
required: true
}
];
});
const rules = ref();
interface DrawerProps {
title: string;
isView: boolean;
row: Partial<NovelList.ResList>;
api?: (params: any) => Promise<any>;
getTableList?: () => void;
}
const drawerVisible = ref(false);
const drawerProps = ref<DrawerProps>({
isView: false,
title: "",
row: {}
});
// 接收父组件传过来的参数
const acceptParams = (params: DrawerProps) => {
drawerProps.value = params;
rules.value = generateRules(formColumns.value);
drawerVisible.value = true;
TagList.value = [];
if (drawerProps.value.title === "新增小说") {
drawerProps.value.row!.n_type = 0;
}
if (drawerProps.value.row.n_cover) {
drawerProps.value.row.n_cover_code = drawerProps.value.row.n_cover.code;
drawerProps.value.row.n_cover = drawerProps.value.row.n_cover_detail;
}
if (drawerProps.value.row.n_tags) {
TagList.value = drawerProps.value.row.n_tags.map((obj: Item) => obj.t_id).map(Number);
}
getTagList();
};
// 提交数据(新增/编辑)
const ruleFormRef = ref<FormInstance>();
const handleSubmit = () => {
ruleFormRef.value!.validate(async valid => {
if (!valid) return;
try {
const params = { ...drawerProps.value.row };
let arrData = {
uri: extractURI(params.n_cover),
code: params.n_cover_code
};
params.n_cover = arrData;
params.n_tags = TagList.value;
await drawerProps.value.api!(params);
ElMessage.success({ message: `${drawerProps.value.title}成功!` });
drawerProps.value.getTableList!();
drawerVisible.value = false;
} catch (error) {
console.log(error);
}
});
};
defineExpose({
acceptParams
});
</script>
<style lang="scss">
.el-checkbox-button__inner {
margin-right: 10px;
margin-bottom: 10px;
border-left-color: #dcdfe6 !important;
}
.tag-more-h {
margin-bottom: 10px;
}
.tag-more-s {
width: 100%;
padding: 10px;
border: 1px solid black;
box-shadow: 0 0 4px 2px rgb(0 0 0 / 90%);
}
</style>

View File

@@ -7,6 +7,9 @@
:request-api="getTableList"
:data-callback="dataCallback"
>
<template #empty>
<el-empty description="暂无小说分类" />
</template>
</ProTable>
</div>
</template>
@@ -18,17 +21,14 @@ import ProTable from "@/components/ProTable/index.vue";
import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import { getList } from "@/api/modules/novel/fenlei";
// ProTable 实例
const proTable = ref<ProTableInstance>();
const initParam = reactive({
limit: 15,
limit: 25,
page: 1
});
const dataCallback = (data: any) => {
console.log(data);
// 转换为数组
const arr = Object.entries(data.all).map(([xsfl_id, xsfl_name]) => ({ xsfl_id: Number(xsfl_id), xsfl_name }));
return {
items: arr,
@@ -46,15 +46,17 @@ const getTableList = (params: any) => {
return getList(newParams);
};
// 表格配置项
const columns = reactive<ColumnProps<fenLeiList.ResList>[]>([
const columns = ref<ColumnProps[]>([
{ type: "selection", fixed: "left", width: 50 },
{
prop: "key",
label: "搜索内容",
label: "分类名称 / ID",
isShow: false,
search: {
el: "input"
el: "input",
props: {
placeholder: "请输入分类名称或 ID"
}
}
},
{
@@ -64,12 +66,8 @@ const columns = reactive<ColumnProps<fenLeiList.ResList>[]>([
},
{
prop: "xsfl_name",
label: "分类名称"
},
// {
// prop: "xsfl_pinyin",
// label: "拼音"
// }
label: "分类名称",
showOverflowTooltip: true
}
]);
</script>

View File

@@ -7,86 +7,34 @@
:request-api="getTableList"
:data-callback="dataCallback"
>
<!-- 表格 header 按钮 -->
<template #tableHeader="scope">
<el-button type="primary" :icon="CirclePlus" @click="openSetDrawerRef(true)">批量设置</el-button>
<el-button type="primary" style="display: none" :icon="CirclePlus" @click="openDrawer(true)">新增</el-button>
<el-button
style="display: none"
type="danger"
:icon="Delete"
plain
:disabled="!scope.isSelected"
@click="batchDelete(scope.selectedList)"
>
批量删除小说
</el-button>
<template #empty>
<el-empty description="暂无小说数据" />
</template>
<!-- 表格操作 -->
<template #operation="scope">
<el-button type="success" link :icon="ChatLineSquare" @click="openChapterDrawer(scope.row)">章节</el-button>
<el-button type="primary" style="display: none" link :icon="EditPen" @click="openDrawer(false, scope.row)">
编辑
</el-button>
<el-button type="danger" style="display: none" link :icon="Delete" @click="deleteAccount(scope.row)">
删除
</el-button>
<template #tableHeader>
<el-button type="primary" :icon="CirclePlus" @click="openSetDrawerRef">批量设置等级</el-button>
</template>
</ProTable>
<Drawer ref="drawerRef" />
<SetDrawer ref="setDrawerRef" />
<ChapterDrawer ref="chapterDrawerRef" />
</div>
</template>
<script setup lang="tsx" name="complexProTable">
import { defineComponent, onMounted, h } from "vue";
import { reactive, ref } from "vue";
import { NovelList } from "@/api/interface/novel/list";
import { useHandleData } from "@/hooks/useHandleData";
import ProTable from "@/components/ProTable/index.vue";
import { Delete, EditPen, CirclePlus, ChatLineSquare } from "@element-plus/icons-vue";
import { CirclePlus } from "@element-plus/icons-vue";
import { ProTableInstance, ColumnProps } from "@/components/ProTable/interface";
import { getList, saveData, deleteItem,levelSet } from "@/api/modules/novel/list";
import Drawer from "./drawer.vue";
import { getList, levelSet } from "@/api/modules/novel/list";
import SetDrawer from "./setDrawer.vue";
import ChapterDrawer from "./components/chapter.vue";
import { getAllList } from "@/api/modules/novel/fenlei";
// import { LOSE_STR_KEY, LOGIN_URL } from "@/config";
import router from "@/routers";
import { getEndpointAllApi } from "@/api/modules/system/endpoint";
//端点列表
const Endpoints = ref([] as any);
// ProTable 实例
const proTable = ref<ProTableInstance>();
//小说分类
// const getFenLeiList = ref([] as any);
// getAllList().then(res => {
// if (res.code === LOSE_STR_KEY) {
// router.replace(LOGIN_URL);
// return;
// }
// getFenLeiList.value = res.data.items;
// });
const initParam = reactive({
limit: 15,
limit: 25,
page: 1
});
const dataCallback = (data: any) => {
console.log(data)
// data.items.forEach((item: any) => {
// const DataString = JSON.parse(item.xsxq_feng_mian);
// const found = Endpoints.value.find(item2 => item2.zydd_code === DataString.code);
// if (found && found.zydd_domain) {
// item["https_cover_url"] = found.zydd_domain + DataString.uri;
// } else {
// item["https_cover_url"] = import.meta.env.VITE_API_URL + DataString.uri;
// }
// });
return {
items: data.data,
total: data.total,
@@ -98,48 +46,30 @@ const dataCallback = (data: any) => {
};
const getTableList = async (params: any) => {
// 获取 getEndpointAllApi 的返回值
// const Configs = await getEndpointAllApi();
// Endpoints.value = Configs.data.items;
let newParams = JSON.parse(JSON.stringify(params));
initParam.page = newParams.page;
initParam.limit = newParams.limit;
return getList(newParams);
};
/**
created_at
:
{$date: {…}}
*/
// 表格配置项
const columns = reactive<ColumnProps<NovelList.ResList>[]>([
const columns = ref<ColumnProps[]>([
{ type: "selection", fixed: "left", width: 50 },
{
prop: "key",
label: "搜索内容",
label: "小说名称 / ID",
isShow: false,
search: {
el: "input"
el: "input",
props: {
placeholder: "请输入小说名称或 ID"
}
}
},
// {
// prop: "n_id",
// label: "id",
// width: 100
// },
// {
// prop: "n_source_id",
// label: "源id",
// width: 100
// },
{
prop: "n_name",
label: "名称",
width: 120
label: "小说名称",
width: 180,
showOverflowTooltip: true
},
{
prop: "n_level",
@@ -149,8 +79,9 @@ const columns = reactive<ColumnProps<NovelList.ResList>[]>([
{
prop: "n_name_pinyin",
label: "拼音",
width: 120
label: "拼音标识",
width: 150,
showOverflowTooltip: true
},
{
@@ -169,151 +100,42 @@ const columns = reactive<ColumnProps<NovelList.ResList>[]>([
},
{
prop: "n_author",
label: "作者"
label: "作者",
showOverflowTooltip: true
},
{
prop: "n_category",
label: "分类",
width: 120,
//fieldNames: { label: "xsfl_name", value: "xsfl_id" },
//enum: getFenLeiList
label: "小说分类",
width: 140,
showOverflowTooltip: true
},
{
prop: "n_description",
label: "描述"
label: "小说简介",
showOverflowTooltip: true
},
// {
// prop: "n_site_name",
// label: "源站"
// },
{
prop: "n_latest_chapter_name",
label: "最新章节"
label: "最新章节",
showOverflowTooltip: true
},
// { prop: "xsxq_source_code", label: "源码", width: 200 },
// { prop: "xsxq_zi_shu", label: "字数", width: 150 },
// { prop: "xsxq_jie_shao", label: "描述", width: 200 },
// { prop: "xsxq_geng_xin_shi_jian", label: "最后更新时间", width: 170 },
// { prop: "created_at", label: "添加时间", width: 170 },
{ prop: "n_update_time", label: "更新时间", width: 170 },
// { prop: "operation", label: "操作", fixed: "right", width: 100 }
{ prop: "n_update_time", label: "更新时间", width: 170 }
]);
// 删除小说信息
const deleteAccount = async (params: NovelList.ResList) => {
await useHandleData(deleteItem, { xsxq_id: params.xsxq_id }, "删除所选信息");
proTable.value?.getTableList();
};
// 批量删除小说信息
const batchDelete = async (id: any[]) => {
const ids = id.map((item: NovelList.ResList) => item.xsxq_id);
await useHandleData(deleteItem, { xsxq_id: ids.join(",") }, "删除所选信息");
proTable.value?.clearSelection();
proTable.value?.getTableList();
};
// 打开 drawer(新增、编辑)
const drawerRef = ref<InstanceType<typeof Drawer> | null>(null);
const openDrawer = (isEdit: boolean, row: Partial<NovelList.ResList> = {}) => {
const params = {
title: isEdit ? "新增小说" : "编辑小说",
row: { ...row },
api: saveData,
getTableList: proTable.value?.getTableList
};
drawerRef.value?.acceptParams(params as any);
};
// 批量设置推荐等级
const setDrawerRef = ref<InstanceType<typeof SetDrawer> | null>(null);
const openSetDrawerRef = (isEdit: boolean, row: Partial<NovelList.ResList> = {}) => {
const openSetDrawerRef = () => {
const params = {
title: "批量设置",
row: { ...row },
title: "批量设置小说等级",
row: {},
api: levelSet,
getTableList: proTable.value?.getTableList
getTableList: proTable.value?.getTableList,
successMessage: "小说等级批量设置完成,已刷新列表"
};
setDrawerRef.value?.acceptParams(params as any);
};
// 章节
const chapterDrawerRef = ref<InstanceType<typeof ChapterDrawer> | null>(null);
const openChapterDrawer = (row: Partial<NovelList.ResList> = {}) => {
chapterDrawerRef.value?.acceptParams(row.xsxq_id!);
};
// 异步加载图片组件
const AsyncImageComponent = defineComponent({
props: {
url: {
type: String,
required: true
}
},
setup(props) {
const imageUrl = ref("");
const isLoading = ref(true);
const error = ref(null);
onMounted(async () => {
try {
const data = await fetchImageData(props.url);
const decodedImage = decode(data);
imageUrl.value = decodedImage;
isLoading.value = false;
} catch (err) {
console.error("Error converting image to base64:", err);
error.value = err;
isLoading.value = false;
}
});
return () => {
if (isLoading.value) {
return h("el-image", { src: "Loading..." });
}
if (error.value) {
return h("el-image", { src: "无法加载" });
}
return (
<el-image
style="width: 98%; height: 30px;display: flex;align-items: center;"
src={imageUrl.value}
preview-src-list={[imageUrl.value]}
hide-on-click-modal={true}
z-index={999999999}
preview-teleported={true}
// fit="contain"
fit="cover"
/>
);
};
}
});
// 获取图片数据
async function fetchImageData(url) {
const response = await fetch(url);
const data = await response.arrayBuffer();
return data;
}
// 解码图片函数
function decode(data, key = 0x88) {
let binary = "";
let bytes = new Uint8Array(data);
let len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i] ^ key);
}
let src = window.btoa(binary);
let image = "data:image/jpeg;base64," + src;
return image;
}
</script>
<style lang="scss">

View File

@@ -1,5 +1,5 @@
<template>
<el-drawer v-model="drawerVisible" :destroy-on-close="true" size="720" :title="`${drawerProps.title}`">
<el-drawer v-model="drawerVisible" :destroy-on-close="true" size="720" :title="drawerProps.title">
<el-form
ref="ruleFormRef"
label-width="150px"
@@ -9,109 +9,18 @@
:model="drawerProps.row"
:hide-required-asterisk="drawerProps.isView"
>
<el-form-item v-for="e in formColumns" :key="e.label" :label="e.label" :prop="e.prop" :required="e.required">
<!-- <template v-if="e.type === 'switch'">
<el-switch
v-model="e.value"
:active-value="`${1}`"
:inactive-value="`${0}`"
active-text="上架"
inactive-text="下架"
/>
</template>
<template v-if="e.type === 'switch-index'">
<el-switch
v-model="e.value"
:active-value="`${1}`"
:inactive-value="`${0}`"
active-text=""
inactive-text=""
/>
</template> -->
<!-- <template v-if="e.type === 'radio'">
<el-radio-group v-model="e.value" class="ml-4">
<el-radio v-for="item in e.options" :key="item.value" :value="item.value" size="large">{{
item.label
}}</el-radio>
</el-radio-group>
</template>
<template v-if="e.type === 'select'">
<el-select filterable v-model="e.value" :placeholder="`请选择${e.label}`" clearable>
<el-option
v-for="(item, index) in e.options"
:key="index"
:label="getField(item, e.fieldNames?.label)"
:value="getField(item, e.fieldNames?.value)"
/>
</el-select>
</template> -->
<template v-if="e.type === 'text'">
<el-input v-model="e.value" :placeholder="`请输入${e.label},多个使用, 分割`" clearable></el-input>
</template>
<template v-if="e.type === 'text-i'">
<el-input v-model="e.value" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'text-num'">
<el-input v-model="e.value" type="number" :placeholder="`请输入${e.label}`" clearable></el-input>
</template>
<template v-if="e.type === 'textarea'">
<el-input
type="textarea"
:rows="20"
v-model="e.value"
maxlength="365000"
:placeholder="`请输入${e.label},多个使用, 分割`"
show-word-limit
/>
</template>
<!-- <template v-if="e.type === 'tag'">
<div class="tag-more-s">
<el-input
v-model="drawerProps.row.searchText"
placeholder="请输入搜索内容"
clearable
@input="handleSearch"
/>
<div>
<span>选中标签:</span>
<div class="tag-more-h">
<el-checkbox-group v-model="TagList" @change="placeableCheckedCitiesChange">
<el-checkbox-button
class="vip-power"
v-for="item in matchingObjects"
:value="item.t_id"
:key="item.t_id"
>
{{ item.t_name }}
</el-checkbox-button>
</el-checkbox-group>
</div>
</div>
<div>
<span>未选中标签:</span>
<div class="tag-more-h">
<el-checkbox-group v-model="TagList" @change="placeableCheckedCitiesChange">
<el-checkbox-button
class="vip-power"
v-for="item in nonMatchingObjects"
:value="item.t_id"
:key="item.t_id"
>
{{ item.t_name }}
</el-checkbox-button>
</el-checkbox-group>
</div>
</div>
</div>
</template> -->
<!-- <template v-if="e.type === 'text-i'">
<UploadImg disabled v-model:image-url="drawerProps.row!.n_cover" width="135px" height="135px" :file-size="3">
<template #empty>
<el-icon><Avatar /></el-icon>
<span>请上传封面</span>
</template>
</UploadImg>
</template> -->
<el-form-item label="推荐等级" prop="n_level" required>
<el-input v-model="drawerProps.row.n_level" type="number" placeholder="请输入推荐等级" clearable />
</el-form-item>
<el-form-item label="小说名称" prop="keywords" required>
<el-input
v-model="drawerProps.row.keywords"
type="textarea"
:rows="16"
maxlength="365000"
placeholder="请输入小说名称,多个使用英文逗号分隔"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
@@ -121,169 +30,15 @@
</el-drawer>
</template>
<script setup lang="ts" name="UserDrawer">
import { ref, computed, reactive } from "vue";
<script setup lang="ts" name="NovelLevelBatchDrawer">
import { reactive, ref } from "vue";
import { ElMessage, FormInstance } from "element-plus";
// import { NovelList } from "@/api/interface/novel/list";
import { arrNovelType } from "@/utils/serviceDict";
// import { getTypeList } from "@/api/modules/tag/list";
// import UploadImg from "@/components/Upload/Img.vue";
import { extractURI } from "@/utils/eleValidate";
type Options = {
label: string;
value: string | number;
};
import { NovelList } from "@/api/interface/novel/list";
interface Item {
t_id: any;
uri: any;
}
// const getField = (item: any, fieldName: string | undefined) => {
// return fieldName ? item[fieldName] : "";
// };
type Fields = {
label: string;
prop: string;
type: string;
value: any;
required: boolean;
options?: Options[];
fieldNames?: { label: string; value: string };
};
const generateRules = (fields: Fields[]) => {
return fields.reduce((acc, field) => {
const { type, required, label, prop } = field;
let message = "";
if (type === "text") {
message = `请输入${label}`;
}
if (type === "text-num") {
message = `请输入${label}`;
}
acc[prop] = [{ required, message }];
return acc;
}, {} as Record<string, any>);
};
const TagList = ref([] as any);
const arrTagList = ref([] as any);
const arrNewTagList = ref([] as any);
const matchingObjects = ref([] as any); //选中相同
const nonMatchingObjects = ref([] as any); //没有选中的
const matchingObjectsA = ref([] as any); //选中相同
const nonMatchingObjectsB = ref([] as any); //没有选中的
// const getTagList = async () => {
// try {
// const initParam = reactive({
// t_type: 3,
// limit: 3650,
// page: 1,
// t_hidden: 1
// });
// const siteRes = await getTypeList(initParam);
// arrTagList.value = siteRes.data.item;
// arrNewTagList.value = siteRes.data.item;
// matchingObjects.value = [];
// matchingObjectsA.value = [];
// // 遍历第一个数组的每个元素
// TagList.value.forEach(index => {
// // 遍历第二个数组的每个元素
// arrNewTagList.value.forEach(obj => {
// // 如果第二个数组中的元素的t_id与第一个数组中的元素相同则将其添加到新数组中
// if (obj.t_id === index) {
// matchingObjects.value = [...matchingObjects.value, obj];
// matchingObjectsA.value = [...matchingObjectsA.value, obj];
// const matchingIds = matchingObjectsA.value.map(obj => obj.t_id);
// TagList.value = matchingIds;
// }
// });
// });
// // 创建一个新数组包含objectArray中id不存在于arrayA的元素
// nonMatchingObjects.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
// nonMatchingObjectsB.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
// } catch (error) {
// console.log(error);
// }
// };
// 处理搜索事件
function handleSearch() {
// 在输入事件中实时更新过滤后的标签列表
filteredTagList();
}
function filteredTagList() {
if (drawerProps.value.row!.searchText.length > 0) {
let arrData = arrTagList.value.filter(tag =>
tag.t_name.toLowerCase().includes(drawerProps.value.row!.searchText.toLowerCase())
);
if (arrData.length > 0) {
matchingObjects.value = matchingObjects.value.filter(objA => arrData.some(objB => objB.t_id === objA.t_id));
nonMatchingObjects.value = nonMatchingObjects.value.filter(objA => arrData.some(objB => objB.t_id === objA.t_id));
} else {
matchingObjects.value = [];
nonMatchingObjects.value = [];
}
} else {
matchingObjects.value = matchingObjectsA.value;
nonMatchingObjects.value = nonMatchingObjectsB.value;
}
}
//标签数组
async function placeableCheckedCitiesChange(value) {
matchingObjects.value = [];
matchingObjectsA.value = [];
drawerProps.value.row.n_tags = value;
// 遍历第一个数组的每个元素
TagList.value.forEach(index => {
// 遍历第二个数组的每个元素
arrNewTagList.value.forEach(obj => {
// 如果第二个数组中的元素的t_id与第一个数组中的元素相同则将其添加到新数组中
if (obj.t_id === index) {
matchingObjects.value = [...matchingObjects.value, obj];
matchingObjectsA.value = [...matchingObjectsA.value, obj];
const matchingIds = matchingObjectsA.value.map(obj => obj.t_id);
TagList.value = matchingIds;
}
});
});
nonMatchingObjects.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
nonMatchingObjectsB.value = arrNewTagList.value.filter(obj => !TagList.value.includes(obj.t_id));
}
const formColumns = computed((): Fields[] => {
return [
{
label: "推荐等级",
prop: "n_level",
type: "text-num",
get value() {
return drawerProps.value.row!.n_level;
},
set value(val) {
drawerProps.value.row!.n_level = val;
},
required: true
},
{
label: "小说名字",
prop: "keywords",
type: "textarea",
get value() {
return drawerProps.value.row!.keywords;
},
set value(val) {
drawerProps.value.row!.keywords = val;
},
required: true
},
];
const rules = reactive({
n_level: [{ required: true, message: "请输入推荐等级" }],
keywords: [{ required: true, message: "请输入小说名称" }]
});
const rules = ref();
interface DrawerProps {
title: string;
@@ -291,6 +46,7 @@ interface DrawerProps {
row: Partial<NovelList.ResList>;
api?: (params: any) => Promise<any>;
getTableList?: () => void;
successMessage?: string;
}
const drawerVisible = ref(false);
@@ -300,45 +56,27 @@ const drawerProps = ref<DrawerProps>({
row: {}
});
// 接收父组件传过来的参数
const acceptParams = (params: DrawerProps) => {
drawerProps.value = params;
rules.value = generateRules(formColumns.value);
drawerProps.value = {
...params,
row: { ...params.row }
};
drawerVisible.value = true;
TagList.value = [];
if (drawerProps.value.title === "新增小说") {
drawerProps.value.row!.n_type = 0;
}
if (drawerProps.value.row.n_cover) {
drawerProps.value.row.n_cover_code = drawerProps.value.row.n_cover.code;
drawerProps.value.row.n_cover = drawerProps.value.row.n_cover_detail;
}
if (drawerProps.value.row.n_tags) {
TagList.value = drawerProps.value.row.n_tags.map((obj: Item) => obj.t_id).map(Number);
}
// getTagList();
};
// 提交数据(新增/编辑)
const ruleFormRef = ref<FormInstance>();
const handleSubmit = () => {
ruleFormRef.value!.validate(async valid => {
if (!valid) return;
try {
const params = { ...drawerProps.value.row };
// let arrData = {
// uri: extractURI(params.n_cover),
// code: params.n_cover_code
// };
// params.n_cover = arrData;
// params.n_tags = TagList.value;
await drawerProps.value.api!(params);
ElMessage.success({ message: `${drawerProps.value.title}成功!` });
await drawerProps.value.api!({ ...drawerProps.value.row });
ElMessage.success({
message: drawerProps.value.successMessage || `${drawerProps.value.title}完成,已刷新列表`
});
drawerProps.value.getTableList!();
drawerVisible.value = false;
} catch (error) {
console.log(error);
} catch (_error) {
return;
}
});
};
@@ -347,19 +85,3 @@ defineExpose({
acceptParams
});
</script>
<style lang="scss">
.el-checkbox-button__inner {
margin-right: 10px;
margin-bottom: 10px;
border-left-color: #dcdfe6 !important;
}
.tag-more-h {
margin-bottom: 10px;
}
.tag-more-s {
width: 100%;
padding: 10px;
border: 1px solid black;
box-shadow: 0 0 4px 2px rgb(0 0 0 / 90%);
}
</style>