chore(config): 初始化项目配置文件

- 添加 .editorconfig 文件统一代码风格
- 配置 .env.development 环境变量文件
- 创建 .env.example 环境变量示例文件
- 设置 .eslintignore 忽略检查规则
- 配置 .eslintrc.js 代码检查规则
- 添加 .gitignore 文件忽略版本控制
- 设置 .prettierignore 忽略格式化规则
- 新增隐私政策HTML页面文件
- 创建API密钥编辑组件基础结构
This commit is contained in:
2025-12-15 13:29:17 +08:00
commit 1856a611ce
877 changed files with 176918 additions and 0 deletions

View File

@@ -0,0 +1,237 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
title="文章详情"
:body-style="{ paddingBottom: '18px' }"
@update:visible="updateVisible"
:confirm-loading="loading"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:label-col="styleResponsive ? { md: 2, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 20, sm: 20, xs: 20 } : { flex: '1' }
"
>
<a-spin :spinning="loading">
<div v-html="content"></div>
</a-spin>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import {
addCmsArticle,
getCmsArticle,
updateCmsArticle
} from '@/api/cms/cmsArticle';
import { CmsArticle } from '@/api/cms/cmsArticle/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { useWebsiteSettingStore } from '@/store/modules/setting';
// 是否是修改
const isUpdate = ref(false);
const setting = useWebsiteSettingStore();
const { locale } = useI18n();
const editor = ref<number>(1);
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: CmsArticle | null;
categoryId?: number;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const content = ref('');
const files = ref<ItemType[]>([]);
const category = ref<string[]>([]);
const password = ref();
// 用户信息
const form = reactive<CmsArticle>({
articleId: undefined,
// 文章模型
model: 'detail',
// 封面图
image: '',
// 文章标题
title: '',
type: 0,
// 展现方式
showType: 10,
// 文章来源
source: undefined,
// 产品概述
overview: undefined,
// 标签集
tags: undefined,
// 父级栏目ID
parentId: undefined,
// 栏目ID
categoryId: undefined,
// 栏目名称
categoryName: undefined,
// 文章内容
content: '',
// 虚拟阅读量
virtualViews: 0,
// 实际阅读量
actualViews: 0,
recommend: undefined,
translation: true,
permission: 0,
password: undefined,
password2: undefined,
// 用户ID
userId: undefined,
files: '',
lang: locale.value || undefined,
// 排序
sortNumber: 100,
// 备注
comments: undefined,
// 状态
status: 1,
// 创建时间
createTime: '',
// 更新时间
updateTime: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
if (password.value) {
form.password = password.value;
}
if (form.tags) {
form.tags = JSON.stringify(form.tags);
}
const formData = {
...form,
editor: editor.value || 1,
status: setting.setting?.articleReview ? 1 : 0,
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateCmsArticle : addCmsArticle;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
message.error(e.message);
})
.finally(() => {
loading.value = false;
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (props.categoryId) {
form.categoryId = props.categoryId;
}
if (localStorage.getItem('Editor')) {
editor.value = Number(localStorage.getItem('Editor'));
}
if (visible) {
images.value = [];
category.value = [];
files.value = [];
content.value = '';
if (props.data) {
loading.value = true;
// 文章详情
getCmsArticle(Number(props.data?.articleId)).then((data) => {
assignObject(form, data);
if (data.content) {
content.value = data.content;
}
if (data.image) {
images.value.push({
uid: uuid(),
url: data.image,
status: 'done'
});
}
if (!data.source) {
form.source = undefined;
}
if (data.tags) {
form.tags = JSON.parse(form.tags);
} else {
form.tags = undefined;
}
if (data.editor) {
editor.value = data.editor;
}
if (data.files) {
const arr = JSON.parse(data.files);
arr.map((url: string) => {
files.value.push({
uid: uuid(),
url: url,
status: 'done'
});
});
}
loading.value = false;
});
isUpdate.value = true;
} else {
isUpdate.value = false;
}
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,69 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="请输入关键词"
style="width: 240px"
v-model:value="where.keywords"
@search="reload"
/>
</a-space>
</template>
<script lang="ts" setup>
import {watch} from 'vue';
import { PlusOutlined } from '@ant-design/icons-vue';
import type {CmsArticle, CmsArticleParam} from '@/api/cms/cmsArticle/model';
import useSearch from '@/utils/use-search';
import {CmsNavigation} from '@/api/cms/cmsNavigation/model';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: CmsArticle[];
merchantId?: number;
navigationList?: CmsNavigation[];
categoryId?: number;
}>(),
{}
);
// 表单数据
const {where} = useSearch<CmsArticleParam>({
articleId: undefined,
model: undefined,
categoryId: undefined,
merchantId: undefined,
keywords: '',
sceneType: undefined,
userId: undefined
});
const emit = defineEmits<{
(e: 'search', where?: CmsArticleParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
const reload = () => {
emit('search', where);
};
// 新增
const add = () => {
emit('add');
};
watch(
() => props.merchantId,
() => {
if (props.categoryId) {
where.categoryId = props.categoryId;
}
reload();
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,381 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="articleId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 1200 }"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
:navigationList="navigationList"
:merchantId="merchantId"
:categoryId="categoryId"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'title'">
<span class="text-black">{{ record.title }}</span>
</template>
<template v-if="column.key === 'image'">
<a-image
:src="record.image"
:width="50"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
/>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ArticleEdit
v-model:visible="showEdit"
:navigationList="navigationList"
:categoryList="categoryList"
:categoryId="categoryId"
:data="current"
@done="reload"
/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, watch} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {
CheckOutlined,
CloseOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ArticleEdit from './components/articleEdit.vue';
import {
pageCmsArticle,
removeCmsArticle,
removeBatchCmsArticle,
updateCmsArticle
} from '@/api/cms/cmsArticle';
import type {CmsArticle, CmsArticleParam} from '@/api/cms/cmsArticle/model';
import {formatNumber} from 'ele-admin-pro/es';
import router from '@/router';
import {toTreeData} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import {
detail,
getPageTitle
} from '@/utils/common';
import {listCmsNavigation} from '@/api/cms/cmsNavigation';
import {CmsNavigation} from '@/api/cms/cmsNavigation/model';
import {CmsArticleCategory} from '@/api/cms/cmsArticleCategory/model';
import {listCmsArticleCategory} from '@/api/cms/cmsArticleCategory';
import {getCmsWebsiteSetting} from '@/api/cms/cmsWebsiteSetting';
import {useWebsiteSettingStore} from '@/store/modules/setting';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<CmsArticle[]>([]);
// 当前编辑数据
const current = ref<CmsArticle | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 店铺ID
const merchantId = ref<number>();
// 栏目ID
const categoryId = ref<number>();
// 栏目数据
const navigationList = ref<CmsNavigation[]>();
// 文章分类
const categoryList = ref<CmsArticleCategory[]>();
// 是否显示二维码
const showQrcode = ref(false);
// 网站设置信息
const setting = useWebsiteSettingStore();
// 表格数据源
const datasource: DatasourceFunction = ({page, limit, where, orders}) => {
if (categoryId.value) {
where.categoryId = categoryId.value;
}
where.model = 'help';
return pageCmsArticle({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
// {
// title: 'ID',
// dataIndex: 'articleId',
// key: 'articleId',
// align: 'center',
// width: 90
// },
// {
// title: '封面图',
// dataIndex: 'image',
// key: 'image',
// width: 120,
// align: 'center'
// },
{
title: '文章标题',
dataIndex: 'title',
key: 'title'
},
// {
// title: '栏目名称',
// dataIndex: 'categoryName',
// key: 'categoryName',
// width: 120,
// align: 'center'
// },
// {
// title: '所属栏目',
// dataIndex: 'categoryId',
// key: 'categoryId',
// align: 'center',
// hideInTable: true
// },
// {
// title: '实际阅读量',
// dataIndex: 'actualViews',
// key: 'actualViews',
// sorter: true,
// width: 120,
// align: 'center'
// },
// {
// title: '虚拟阅读量',
// dataIndex: 'virtualViews',
// key: 'virtualViews',
// width: 120,
// align: 'center',
// hideInTable: true
// },
// {
// title: '推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// sorter: true,
// width: 120,
// align: 'center'
// },
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// sorter: true,
// width: 120,
// align: 'center'
// },
// {
// title: '排序号',
// dataIndex: 'sortNumber',
// key: 'sortNumber',
// sorter: true,
// width: 120,
// align: 'center'
// },
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
width: 180,
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd'),
sorter: true
},
// {
// title: '操作',
// key: 'action',
// width: 180,
// fixed: 'right',
// align: 'center',
// hideInSetting: true
// }
]);
/* 搜索 */
const reload = (where?: CmsArticleParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: CmsArticle) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
// 分享二维码
const onShare = (row?: CmsArticle) => {
const data = {
isMobile: true,
...row
};
data.qrcode = `${detail(data)}`;
data.url = `${detail(row)}`;
current.value = data ?? null;
showQrcode.value = true;
};
const onUpdate = (row?: CmsArticle) => {
const status = row?.status == 0 ? 1 : 0;
updateCmsArticle({...row, status}).then((msg) => {
message.success(msg);
reload();
});
};
const onRecommend = (row: CmsArticle) => {
updateCmsArticle({
...row,
recommend: row.recommend == 1 ? 0 : 1
}).then((msg) => {
message.success(msg);
reload();
});
};
/* 删除单个 */
const remove = (row: CmsArticle) => {
const hide = message.loading('请求中..', 0);
removeCmsArticle(row.articleId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchCmsArticle(selection.value.map((d) => d.articleId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: CmsArticle) => {
return {
// 行点击事件
onClick: () => {
openEdit(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
// 加载栏目数据
if (!navigationList.value) {
listCmsNavigation({}).then((res) => {
navigationList.value = toTreeData({
data: res?.map((d) => {
d.value = d.navigationId;
d.label = d.title;
if (!d.component) {
d.disabled = true;
}
if (
d.model == 'index' ||
d.model == 'page' ||
d.model == 'order' ||
d.model == 'links'
) {
d.disabled = true;
}
return d;
}),
idField: 'navigationId',
parentIdField: 'parentId'
});
});
}
// 加载文章分类
if (!categoryList.value) {
listCmsArticleCategory({}).then((res) => {
categoryList.value = toTreeData({
data: res?.map((d) => {
d.value = d.categoryId;
d.label = d.title;
return d;
}),
idField: 'categoryId',
parentIdField: 'parentId'
});
});
}
watch(
() => router.currentRoute.value.query,
(query) => {
if (query) {
categoryId.value = Number(query.id);
reload();
}
},
{immediate: true}
);
</script>
<script lang="ts">
export default {
name: 'CmsArticleV2'
};
</script>