feat: 🚀 订阅功能

This commit is contained in:
2025-09-16 16:38:30 +08:00
parent eb1b66a066
commit d3a3ef2911
456 changed files with 40544 additions and 124 deletions

View File

@@ -0,0 +1,31 @@
export namespace Table {
export interface Pageable {
page: number;
size: number;
total_size: number;
}
export interface StateProps {
tableData: any[];
pageable: Pageable;
searchParam: {
[key: string]: any;
};
// searchInitParam: {
// [key: string]: any;
// };
totalParam: {
[key: string]: any;
};
icon?: {
[key: string]: any;
};
}
}
export namespace HandleData {
export type MessageType = "" | "success" | "warning" | "info" | "error";
}
export namespace Theme {
export type GreyOrWeakType = "grey" | "weak";
}

14
src/hooks/useDateSort.ts Normal file
View File

@@ -0,0 +1,14 @@
export const useDateSort = (property: any, bol: any) => {
//property是你需要排序传入的key,bol为true时是升序false为降序
return function (a: any, b: any) {
let value1 = a[property];
let value2 = b[property];
if (bol) {
// 升序
return Date.parse(value1) - Date.parse(value2);
} else {
// 降序
return Date.parse(value2) - Date.parse(value1);
}
};
};

12
src/hooks/useDecimal.ts Normal file
View File

@@ -0,0 +1,12 @@
/**
* @description 精度处理
* @param data 数据源
* @param type Decimal方法
* @param typeData mul | div| add | sub 方法参数 的数据
* @return returnData {number}
* */
import { Decimal } from "decimal.js";
export const useDecimal = (data: number, type?: "mul" | "div" | "add" | "sub", typeData?: number) => {
let returnData = type && typeData ? new Decimal(data)[type](typeData).toNumber() : new Decimal(data).toNumber();
return returnData;
};

44
src/hooks/useDownload.ts Normal file
View File

@@ -0,0 +1,44 @@
import { ElNotification } from "element-plus";
/**
* @description 接收数据流生成 blob创建链接下载文件
* @param {Function} api 导出表格的api方法 (必传)
* @param {String} tempName 导出的文件名 (必传)
* @param {Object} params 导出的参数 (默认{})
* @param {Boolean} isNotify 是否有导出消息提示 (默认为 true)
* @param {String} fileType 导出的文件格式 (默认为.xlsx)
* */
export const useDownload = async (
api: (param: any) => Promise<any>,
tempName: string,
params: any = {},
isNotify: boolean = true,
fileType: string = ".xlsx"
) => {
if (isNotify) {
ElNotification({
title: "温馨提示",
message: "如果数据庞大会导致下载缓慢哦,请您耐心等待!",
type: "info",
duration: 3000
});
}
try {
const res = await api(params);
const blob = new Blob([res]);
// 兼容 edge 不支持 createObjectURL 方法
if ("msSaveOrOpenBlob" in navigator) return window.navigator.msSaveOrOpenBlob(blob, tempName + fileType);
const blobUrl = window.URL.createObjectURL(blob);
const exportFile = document.createElement("a");
exportFile.style.display = "none";
exportFile.download = `${tempName}${fileType}`;
exportFile.href = blobUrl;
document.body.appendChild(exportFile);
exportFile.click();
// 去除下载对 url 的影响
document.body.removeChild(exportFile);
window.URL.revokeObjectURL(blobUrl);
} catch (error) {
console.log(error);
}
};

9
src/hooks/useFilter.ts Normal file
View File

@@ -0,0 +1,9 @@
import { cloneDeep } from "lodash-es";
export const useFilter = (datas: any) => {
const target = cloneDeep(datas);
const returnTarget = target.filter((item: any) => {
return !item.disable;
});
return returnTarget;
};

View File

@@ -0,0 +1,34 @@
import { ElMessageBox, ElMessage } from "element-plus";
import { HandleData } from "./interface";
/**
* @description 操作单条数据信息 (二次确认【删除、禁用、启用、重置密码】)
* @param {Function} api 操作数据接口的api方法 (必传)
* @param {Object} params 携带的操作数据参数 {id,params} (必传)
* @param {String} message 提示信息 (必传)
* @param {String} confirmType icon类型 (不必传,默认为 warning)
* @returns {Promise}
*/
export const useHandleData = (
api: (params: any) => Promise<any>,
params: any = {},
message: string,
confirmType: HandleData.MessageType = "warning"
) => {
return new Promise((resolve, reject) => {
ElMessageBox.confirm(`是否${message}?`, "温馨提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: confirmType,
draggable: true
}).then(async () => {
const res = await api(params);
if (!res) return reject(false);
ElMessage({
type: "success",
message: `${message}成功!`
});
resolve(true);
});
});
};

10
src/hooks/useMsg.ts Normal file
View File

@@ -0,0 +1,10 @@
import { ElMessage } from "element-plus";
export const useMsg = (type: any = "success", msg: string, duration?: number, showClose?: boolean) => {
ElMessage({
showClose: showClose ? showClose : false,
message: msg,
type: type,
duration: duration ? duration : 3000
});
};

27
src/hooks/useOnline.ts Normal file
View File

@@ -0,0 +1,27 @@
import { ref, onMounted, onUnmounted } from "vue";
/**
* @description 网络是否可用
* */
export const useOnline = () => {
const online = ref(true);
const showStatus = (val: any) => {
online.value = typeof val == "boolean" ? val : val.target.online;
};
// 在页面加载后,设置正确的网络状态
navigator.onLine ? showStatus(true) : showStatus(false);
onMounted(() => {
// 开始监听网络状态的变化
window.addEventListener("online", showStatus);
window.addEventListener("offline", showStatus);
});
onUnmounted(() => {
// 移除监听网络状态的变化
window.removeEventListener("online", showStatus);
window.removeEventListener("offline", showStatus);
});
return { online };
};

6
src/hooks/usePathUrl.ts Normal file
View File

@@ -0,0 +1,6 @@
export const usePathUrl = () => {
const PATH_URL = `${import.meta.env.VITE_APP_SSO_LOGINURL}?client_id=${
import.meta.env.VITE_APP_SSO_APPID
}&redirect_uri=${encodeURIComponent(import.meta.env.VITE_REDIRECT_URL)}&response_type=code`;
return PATH_URL;
};

97
src/hooks/useSearch.ts Normal file
View File

@@ -0,0 +1,97 @@
import { cloneDeep } from "lodash-es";
export const useSearchInfoArray = (target: any[]): any => {
//深拷贝一份,不影响源数据
let targetClone: any[] = cloneDeep(target);
let returnTarget: any[] = [];
//如果是数组
if (Array.isArray(targetClone) && targetClone.length) {
targetClone.forEach(item => {
let obj = {
value: item.id,
label: item.name,
...item
};
returnTarget.push(obj);
});
}
return returnTarget;
};
export const useSearchCodeInfoArray = (target: any[]): any => {
//深拷贝一份,不影响源数据
let targetClone: any[] = cloneDeep(target);
let returnTarget: any[] = [];
//如果是数组
if (Array.isArray(targetClone) && targetClone.length) {
targetClone.forEach(item => {
let obj = {
value: item.code,
label: item.name,
...item
};
returnTarget.push(obj);
});
}
return returnTarget;
};
export const useSearchInfoObject = (target: Record<string, any>): any => {
//深拷贝一份,不影响源数据
let targetClone: Record<string, any> = cloneDeep(target);
let returnTarget: any[] = [];
if (Array.isArray(targetClone) && targetClone.length) {
targetClone.forEach(item => {
let obj = {
value: item.id,
label: item.name,
...item
};
returnTarget.push(obj);
});
return returnTarget;
}
//如果是对象
if (Object.prototype.toString.call(target) === "[object Object]") {
for (let key in targetClone) {
let obj: Record<string, any> = {
value: isNaN(parseInt(key)) ? key : parseInt(key), //是字符串数字就转为数组
label: targetClone[key]
};
returnTarget.push(obj);
}
}
return returnTarget;
};
//search配置项添加数据
export const useSetSearchData = (target: any[], data: Record<string, any>) => {
let targetClone = cloneDeep(target);
let length = targetClone.length;
//如果是数组
if (Array.isArray(targetClone) && length) {
for (let i = 0; i < length; i++) {
let item = targetClone[i];
if (data[item.prop] || data[item.label]) {
item.options = data[item.prop] || data[item.label];
}
}
}
return targetClone;
};
export const useSearchStockCodeInfoArray = (target: any[]): any => {
//深拷贝一份,不影响源数据
let targetClone: any[] = cloneDeep(target);
let returnTarget: any[] = [];
//如果是数组
if (Array.isArray(targetClone) && targetClone.length) {
targetClone.forEach(item => {
let obj = {
value: item.code,
label: item.name,
...item
};
returnTarget.push(obj);
});
}
return returnTarget;
};

34
src/hooks/useSelection.ts Normal file
View File

@@ -0,0 +1,34 @@
import { ref, computed } from "vue";
/**
* @description 表格多选数据操作
* @param {String} rowKey 当表格可以多选时,所指定的 id
* */
export const useSelection = (rowKey: string = "id") => {
const isSelected = ref<boolean>(false);
const selectedList = ref<{ [key: string]: any }[]>([]);
// 当前选中的所有 ids 数组
const selectedListIds = computed((): string[] => {
let ids: string[] = [];
selectedList.value.forEach(item => ids.push(item[rowKey]));
return ids;
});
/**
* @description 多选操作
* @param {Array} rowArr 当前选择的所有数据
* @return void
*/
const selectionChange = (rowArr: { [key: string]: any }[]) => {
rowArr.length ? (isSelected.value = true) : (isSelected.value = false);
selectedList.value = rowArr;
};
return {
isSelected,
selectedList,
selectedListIds,
selectionChange
};
};

142
src/hooks/useTable.ts Normal file
View File

@@ -0,0 +1,142 @@
import { Table } from "./interface";
import { reactive, computed, toRefs, watch, isRef } from "vue";
export const useTable = (
routeName: any,
api?: (params: any) => Promise<any>,
initParam: object | Ref<object> = {},
isPageable: boolean = true,
requestError?: (error: any) => void,
clearSelection?: () => void
) => {
const state = reactive<Table.StateProps>({
tableData: [],
pageable: {
page: 1,
size: 50,
total_size: 0
},
searchParam: {},
totalParam: {} // 保留作为参数快照
});
// 分页参数计算属性
const pageParam = computed({
get: () => ({
page: state.pageable.page,
size: state.pageable.size
}),
set: (newVal: any) => {
console.log("分页参数更新:", newVal);
}
});
//订阅列表数据处理
const initSubscribeData = () => {
if (routeName === "foundationSubscribeList") {
if (Array.isArray(state.totalParam?.org_number) && state.totalParam?.org_number?.length) {
state.totalParam.org_number = state.totalParam.org_number.join(",");
}
//品线
if (Array.isArray(state.totalParam?.product_line_name) && state.totalParam?.product_line_name?.length) {
state.totalParam.org_number = state.totalParam.product_line_name.join(",");
}
//客户名称
if (Array.isArray(state.totalParam?.customer_number) && state.totalParam?.customer_number?.length) {
state.totalParam.customer_number = state.totalParam.customer_number.join(",");
}
//客户编码
if (state.totalParam?.customer_number1) {
if (Array.isArray(state.totalParam.customer_number) && !state.totalParam.customer_number.length) {
state.totalParam.customer_number = state.totalParam.customer_number1;
} else {
state.totalParam.customer_number = state.totalParam.customer_number + "," + state.totalParam.customer_number1;
}
}
}
};
//删除临时参数和空值参数
const deleteParams = () => {
const KEY = ["Time", "customer_number1"];
for (let key in state.totalParam) {
if (KEY.includes(key) || !state.totalParam[key]) {
delete state.totalParam[key];
}
}
};
/**
* 获取表格数据
* 每次请求都基于最新的initParam和分页参数重新合并
*/
const getTableList = async () => {
if (!api) return;
try {
initSubscribeData();
deleteParams();
let params = {
...state.totalParam,
...pageParam.value
};
const { data } = await api(params);
state.tableData = data.data || [];
clearSelection && clearSelection();
if (isPageable && data.total_size !== undefined) {
updatePageable({
page: state.pageable.page,
size: state.pageable.size,
total_size: data.total_size
});
}
} catch (error) {
console.error("表格数据请求失败:", error);
requestError && requestError(error);
}
};
/**
* 更新分页信息
*/
const updatePageable = (pageable: Table.Pageable) => {
Object.assign(state.pageable, pageable);
};
/**
* 处理每页条数变化
*/
const handleSizeChange = (val: number) => {
state.pageable.page = 1; // 重置到第一页
state.pageable.size = val;
getTableList();
};
/**
* 处理当前页变化
*/
const handleCurrentChange = (val: number) => {
state.pageable.page = val;
getTableList();
};
/**
* 监听initParam变化同步到searchParam和totalParam
*/
watch(
() => (isRef(initParam) ? initParam.value : initParam),
(newVal: any) => {
state.totalParam = {};
// 同步到查询参数
state.searchParam = { ...newVal };
// 同步到参数快照
state.totalParam = { ...newVal, ...pageParam.value };
},
{ deep: true, immediate: true }
);
return {
...toRefs(state),
getTableList,
handleSizeChange,
handleCurrentChange,
updatePageable
};
};

87
src/hooks/useTheme.ts Normal file
View File

@@ -0,0 +1,87 @@
import { storeToRefs } from "pinia";
import { Theme } from "./interface";
import { ElMessage } from "element-plus";
import { DEFAULT_PRIMARY } from "@/config";
import { useGlobalStore } from "@/stores/modules/global";
import { getLightColor, getDarkColor } from "@/utils/color";
import { asideTheme, AsideThemeType } from "@/styles/theme/aside";
/**
* @description 全局主题 hooks
* */
export const useTheme = () => {
const globalStore = useGlobalStore();
const { primary, isDark, isGrey, isWeak, asideInverted, layout } = storeToRefs(globalStore);
// 切换暗黑模式 ==> 并带修改主题颜色、侧边栏颜色
const switchDark = () => {
const html = document.documentElement as HTMLElement;
if (isDark.value) html.setAttribute("class", "dark");
else html.setAttribute("class", "");
changePrimary(primary.value);
setAsideTheme();
};
// 修改主题颜色
const changePrimary = (val: string | null) => {
if (!val) {
val = DEFAULT_PRIMARY;
ElMessage({ type: "success", message: `主题颜色已重置为 ${DEFAULT_PRIMARY}` });
}
// 计算主题颜色变化
document.documentElement.style.setProperty("--el-color-primary", val);
document.documentElement.style.setProperty(
"--el-color-primary-dark-2",
isDark.value ? `${getLightColor(val, 0.2)}` : `${getDarkColor(val, 0.3)}`
);
for (let i = 1; i <= 9; i++) {
const primaryColor = isDark.value ? `${getDarkColor(val, i / 10)}` : `${getLightColor(val, i / 10)}`;
document.documentElement.style.setProperty(`--el-color-primary-light-${i}`, primaryColor);
}
globalStore.setGlobalState("primary", val);
};
// 灰色和弱色切换
const changeGreyOrWeak = (type: Theme.GreyOrWeakType, value: boolean) => {
const body = document.body as HTMLElement;
if (!value) return body.removeAttribute("style");
const styles: Record<Theme.GreyOrWeakType, string> = {
grey: "filter: grayscale(1)",
weak: "filter: invert(80%)"
};
body.setAttribute("style", styles[type]);
const propName = type === "grey" ? "isWeak" : "isGrey";
globalStore.setGlobalState(propName, false);
};
// 设置侧边栏样式 ==> light、inverted、dark
const setAsideTheme = () => {
// 默认所有侧边栏为 light 模式
let type: AsideThemeType = "light";
// 侧边栏反转色目前只支持在 vertical、classic 布局模式下生效 || transverse 布局下菜单栏默认为 inverted 模式
if ((["vertical", "classic"].includes(layout.value) && asideInverted.value) || layout.value == "transverse") {
type = "inverted";
}
// 侧边栏 dark 模式
if (isDark.value) type = "dark";
const theme = asideTheme[type!];
for (const [key, value] of Object.entries(theme)) {
document.documentElement.style.setProperty(key, value);
}
};
// init theme
const initTheme = () => {
switchDark();
if (isGrey.value) changeGreyOrWeak("grey", true);
if (isWeak.value) changeGreyOrWeak("weak", true);
};
return {
initTheme,
switchDark,
changePrimary,
changeGreyOrWeak,
setAsideTheme
};
};

41
src/hooks/useTime.ts Normal file
View File

@@ -0,0 +1,41 @@
import { ref } from "vue";
/**
* @description 获取本地时间
*/
export const useTime = () => {
const year = ref(0); // 年份
const month = ref(0); // 月份
const week = ref(""); // 星期几
const day = ref(0); // 天数
const hour = ref<number | string>(0); // 小时
const minute = ref<number | string>(0); // 分钟
const second = ref<number | string>(0); // 秒
const nowTime = ref<string>(""); // 当前时间
const specificDate = ref<string>(""); // 年月日
// 更新时间
const updateTime = () => {
const date = new Date();
year.value = date.getFullYear();
month.value = date.getMonth() + 1;
week.value = "日一二三四五六".charAt(date.getDay());
day.value = date.getDate();
hour.value =
(date.getHours() + "")?.padStart(2, "0") ||
new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getHours());
minute.value =
(date.getMinutes() + "")?.padStart(2, "0") ||
new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getMinutes());
second.value =
(date.getSeconds() + "")?.padStart(2, "0") ||
new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getSeconds());
nowTime.value = `${year.value}${month.value}${day.value} ${hour.value}:${minute.value}:${second.value}`;
specificDate.value = year.value + "-" + month.value + "-" + day.value;
};
updateTime();
console.log(year, month, day, hour, minute, second, week, nowTime, specificDate.value);
return { year, month, day, hour, minute, second, week, nowTime, specificDate };
};