feat: 会员中心 hgcloud 主题初始化 + drone 部署步骤
All checks were successful
continuous-integration/drone/push Build is passing

- 解压官方默认主题 default_yfMBA.tar.gz 到 clientarea/hgcloud/
- .gitignore 排除压缩包和临时解压目录
- drone 新增步骤: 同步 hgcloud 到 /clientarea/template/pc/
This commit is contained in:
yiqiu
2026-03-19 17:56:44 +08:00
parent 75756e5a64
commit 3b41cffbc9
381 changed files with 386825 additions and 1 deletions

View File

@@ -0,0 +1,109 @@
// 这是vue注册全局 指令的地方
// 自动聚焦的指令
Vue.directive("focus", {
inserted: function (el) {
el.focus();
},
});
// 判断是否安装了某插件的指令 用法 v-plugin="插件名" 例如 v-plugin="whmcs" v-plugin="'whmcs-bridge'" 用于显示/隐藏某些元素
Vue.directive("plugin", {
inserted(el, binding) {
const addonsDom = document.querySelector("#addons_js");
let addonsArr = [];
let arr = [];
if (addonsDom) {
addonsArr = JSON.parse(addonsDom.getAttribute("addons_js")) || []; // 插件列表
// 判断是否安装了某插件
arr = addonsArr.filter((item) => item.name === binding.value);
if (arr.length === 0) {
// 未安装 移除该元素 以及子元素所有的DOM
el.parentNode.removeChild(el);
}
} else {
el.parentNode.removeChild(el);
}
},
});
// 复制指令 用法 v-copy="复制的内容" 例如 v-copy="123456789" 用于复制内容到剪切板 不用写点击事件
Vue.directive("copy", {
bind(el, {value}) {
el.$value = value;
el.handler = () => {
el.style.position = "relative";
if (!el.$value) {
return;
}
try {
if (navigator.clipboard) {
navigator.clipboard.writeText(el.$value).then(
() => {
Vue.prototype.$message.success(lang.pay_text17);
},
() => {
console.error("复制失败");
}
);
} else {
// 动态创建 textarea 标签
const textarea = document.createElement("textarea");
textarea.readOnly = true; // 简化设置为 true
textarea.style.position = "absolute";
textarea.style.top = "0px";
textarea.style.left = "-9999px";
textarea.style.zIndex = "-9999";
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 el 中
document.body.appendChild(textarea); // 插入到 body 中,以避免对其他元素的影响
// 选择文本并复制
textarea.select();
const result = document.execCommand("copy");
if (result) {
Vue.prototype.$message.success(lang.pay_text17);
} else {
console.error("复制失败");
}
document.body.removeChild(textarea); // 从 body 中移除 textarea
}
} catch (err) {
console.error("复制失败", err);
}
};
el.addEventListener("click", el.handler); // 绑定点击事件
},
// 当传进来的值更新的时候触发
componentUpdated(el, {value}) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener("click", el.handler);
},
});
/* 转换时间 ==> 12 Oct, 2023 14:47 传入的是秒级 */
Vue.directive("time", (el, binding) => {
if (!binding.value) {
el.innerHTML = "--";
return;
}
const timestamp = binding.value;
const date = new Date(timestamp * 1000);
const curLang = localStorage.getItem("lang") || "en-us";
let formattedDate = "";
const year = date.getFullYear();
let month = "";
const day = String(date.getDate()).padStart(2, "0");
const hour = String(date.getHours()).padStart(2, "0");
const minute = String(date.getMinutes()).padStart(2, "0");
if (curLang === "zh-cn" || curLang === "zh-hk") {
month = String(date.getMonth() + 1).padStart(2, "0");
formattedDate = `${year}-${month}-${day} ${hour}:${minute}`;
} else {
month = new Intl.DateTimeFormat("en-US", {month: "short"}).format(date);
formattedDate = `${day} ${month}, ${year} <span style="color: #878A99;">${hour}:${minute}</span>`;
}
el.innerHTML = formattedDate;
});

View File

@@ -0,0 +1,159 @@
(function () {
const baseURL = `/console/v1`;
const Axios = axios.create({baseURL, timeout: 1000 * 60 * 10});
Axios.defaults.withCredentials = true;
// 这里是不需要jwt的页面 例如登录页
const noNeedJwtUrlArr = [
"login.htm",
"userStatus.htm",
"goodsList.htm",
"source.htm",
"news_detail.htm",
"helpTotal.htm",
"goods.htm",
"goods_iframe.htm",
"agreement.htm",
"regist.htm",
"forget.htm",
"oauth.htm",
"404.htm",
];
const nowUrl = location.href.split("/").pop();
const pageRouter = nowUrl.indexOf("?") !== -1 ? nowUrl.split("?")[0] : nowUrl;
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("queryParam")) {
const jwt = urlParams.get("queryParam");
localStorage.setItem("jwt", jwt);
// 替换当前url删除queryParam参数 保留其它参数
urlParams.delete("queryParam");
const newUrl =
window.location.pathname +
(urlParams.toString() ? "?" + urlParams.toString() : "");
window.history.replaceState({}, "", newUrl);
}
// 请求拦截器
Axios.interceptors.request.use(
(config) => {
if (localStorage.getItem("jwt")) {
config.headers.Authorization =
"Bearer" + " " + localStorage.getItem("jwt");
} else {
// 浏览器语言
const borwserLang = getBrowserLanguage();
config.headers.language = borwserLang;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// 响应拦截器
Axios.interceptors.response.use(
(response) => {
const code = response.data.status;
if (response.data.rule) {
// 返回有rule的时候, 才执行缓存操作
localStorage.setItem("menuList", JSON.stringify(response.data.rule)); // 权限菜单
}
if (code) {
switch (code) {
case 200:
break;
case 302:
location.href = `${baseURL}/install`;
break;
case 307:
break;
case 400:
if (response.data.data) {
const {client_operate_methods, operate_password} =
response.data.data;
if (client_operate_methods && operate_password) {
window.clientOperateVue?.$refs?.safeRef.openDialog(
client_operate_methods
);
}
}
return Promise.reject(response);
case 401: // 未授权:2个小时未操作自动退出登录
// 几个特定页面不跳转登录页面
localStorage.removeItem("jwt");
if (!noNeedJwtUrlArr.includes(pageRouter)) {
sessionStorage.redirectUrl = location.href;
location.href = `/login.htm`;
break;
}
return Promise.reject(response);
case 403:
break;
case 404:
// 判断当前页面是否为 noPermissions.htm, 如果是, 则不跳转, 否则跳转到 noPermissions.htm
// if (pageRouter !== "noPermissions.htm") {
// location.replace("/noPermissions.htm");
// }
return Promise.reject(response);
case 405:
location.href = "/login.htm";
break;
case 406:
break;
case 409: // 该管理没有该客户, 跳转首页
location.href = "";
break;
case 410:
break;
case 422:
break;
case 500:
this.$message.error("访问失败, 请重试!");
break;
case 501:
break;
case 502:
break;
case 503:
location.href = "/503.htm";
break;
case 504:
break;
case 505:
break;
}
}
return response;
},
(error) => {
console.log("error:", error);
if (error.code === "ERR_NETWORK") {
location.href = `/networkErro.htm`;
}
if (error.config) {
if (error.config.url.indexOf("system/autoupdate") !== -1) {
// 系统更新接口
if (error.message === "Network Error") {
setTimeout(() => {
location.reload();
}, 2000);
}
}
}
if (error.response) {
if (error.response.status === 302) {
location.href = `${baseURL}/install`;
return;
}
if (error.response.status === 500 && error.response.data.message) {
this.$message.error(error.response.data.message);
return;
}
}
return Promise.reject(error);
}
);
window.Axios = Axios;
})();

View File

@@ -0,0 +1,467 @@
//获取cookie、
function getCookie(name) {
let arr,
reg = new RegExp("(^| )" + name + "=([^;]*)(;|$)");
if ((arr = document.cookie.match(reg))) return arr[2];
else return null;
}
//设置cookie
function setCookie(c_name, value, expiredays = 1) {
let exdate = new Date();
exdate.setDate(exdate.getDate() + expiredays);
document.cookie =
c_name +
"=" +
escape(value) +
(expiredays == null ? "" : ";expires=" + exdate.toGMTString());
}
//删除cookie
function delCookie(name) {
let exp = new Date();
exp.setTime(exp.getTime() - 1);
let cval = getCookie(name);
if (cval != null)
document.cookie = name + "=" + cval + ";expires=" + exp.toGMTString();
}
// 时间戳转换
/**
* @param timestamp 时间戳
* @param format 格式 YYYY-MM-DD HH:mm
* @returns YY-MM-DD hh:mm
*/
function formateDate(time, format = "YYYY-MM-DD HH:mm") {
const date = new Date(time);
Y = date.getFullYear() + "-";
M =
(date.getMonth() + 1 < 10
? "0" + (date.getMonth() + 1)
: date.getMonth() + 1) + "-";
D = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + " ";
h = (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":";
m = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
s = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds();
if (format === "YYYY-MM-DD HH:mm") {
return Y + M + D + h + m;
} else if (format === "YYYY-MM-DD HH:mm:ss") {
return Y + M + D + h + m + ":" + s;
} else if (format === "YYYY-MM-DD") {
return Y + M + D;
}
}
/**
* @param timestamp 时间戳
* @returns YY.MM.DD
*/
function formateDate1(time) {
const date = new Date(time);
Y = date.getFullYear() + ".";
M =
(date.getMonth() + 1 < 10
? "0" + (date.getMonth() + 1)
: date.getMonth() + 1) + ".";
D = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + " ";
h = (date.getHours() < 10 ? "0" + date.getHours() : date.getHours()) + ":";
m = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes();
return Y + M + D;
}
/**
* @param timestamp 时间戳
* @returns YY年MM月DD日
*/
function formateDate2(time) {
const date = new Date(time);
Y = date.getFullYear() + "年";
M =
(date.getMonth() + 1 < 10
? "0" + (date.getMonth() + 1)
: date.getMonth() + 1) + "月";
D = (date.getDate() < 10 ? "0" + date.getDate() : date.getDate()) + "日";
return Y + M + D;
}
/**
* 生成密码字符串
* 33~47!~/
* 48~570~9
* 58~64:~@
* 65~90A~Z
* 91~96[~`
* 97~122a~z
* 123~127{~
* @param length 长度 生成的长度是length
* @param hasNum 是否包含数字 1-包含 0-不包含
* @param hasChar 是否包含字母 1-包含 0-不包含
* @param hasSymbol 是否包含其他符号 1-包含 0-不包含
* @param caseSense 是否大小写敏感 1-敏感 0-不敏感
* @param lowerCase 是否只需要小写只有当hasChar为0且caseSense为1时起作用 1-全部小写 0-全部大写
*/
function genEnCode(length, hasNum, hasChar, hasSymbol, caseSense, lowerCase) {
let m = "";
if (hasNum == 0 && hasChar == 0 && hasSymbol == 0) return m;
for (let i = length; i > 0; i--) {
let num = Math.floor(Math.random() * 94 + 33);
if (
(hasNum == 0 && num >= 48 && num <= 57) ||
(hasChar == 0 &&
((num >= 65 && num <= 90) || (num >= 97 && num <= 122))) ||
(hasSymbol == 0 &&
((num >= 33 && num <= 47) ||
(num >= 58 && num <= 64) ||
(num >= 91 && num <= 96) ||
(num >= 123 && num <= 127)))
) {
i++;
continue;
}
m += String.fromCharCode(num);
}
if (caseSense == "0") {
m = lowerCase == "0" ? m.toUpperCase() : m.toLowerCase();
}
return m;
}
/**
*
* @param {Number} n 返回n个随机字母字符串
* @returns
*/
function randomCoding(n) {
//创建26个字母数组
const arr = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
];
let idvalue = "";
for (let i = 0; i < n; i++) {
idvalue += arr[Math.floor(Math.random() * 26)];
}
return idvalue;
}
/**
* 防抖函数
* @param {Function} func 需要防抖的目标函数
* @param {number} wait 延迟时间(毫秒)
* @param {boolean} [immediate=false] 是否立即执行true 表示第一次触发立即执行)
* @returns {Function} 返回防抖处理后的函数
*/
function debounce(func, wait = 500, immediate = false) {
let timeoutId;
let result;
const debounced = function (...args) {
const context = this;
// 清除现有定时器
if (timeoutId) clearTimeout(timeoutId);
// 立即执行模式
if (immediate) {
const callNow = !timeoutId;
timeoutId = setTimeout(() => {
timeoutId = null;
}, wait);
if (callNow) result = func.apply(context, args);
}
// 非立即执行模式
else {
timeoutId = setTimeout(() => {
func.apply(context, args);
}, wait);
}
return result;
};
// 添加取消方法
debounced.cancel = function () {
clearTimeout(timeoutId);
timeoutId = null;
};
return debounced;
}
/**
*
* @param num 需要处理的三分数字
* @param fixed 保留小数位数
* @param separator 货币分隔符
* @returns 1,000.00
*/
function formatMoneyNumber(num, fixed = 2, separator = ",") {
// 判断数字是否为 null 或非数字类型,并默认设置为 0.00
if (num == null || isNaN(num)) {
num = 0.0;
}
// 将数字转换为字符串,并将负号单独提取出来
let str = String(Math.abs(Number(num)).toFixed(fixed));
let [integer, decimal] = str.split(".");
// 在整数部分添加千位分隔符
let result = "";
while (integer.length > 3) {
result = separator + integer.slice(-3) + result;
integer = integer.slice(0, -3);
}
result = integer + result;
// 如果原始数字是负数,则在最终结果中添加负号
if (num < 0) {
result = "-" + result;
}
// 如果有小数部分,则在最终结果中添加小数点和小数部分
if (decimal != null) {
result += `.${decimal}`;
}
return result;
}
function formatNuberFiexd(num, fixed = 2) {
// 判断数字是否为 null 或非数字类型,并默认设置为 0.00
if (num == null || isNaN(num)) {
num = 0.0;
}
return Number(num).toFixed(fixed);
}
/**
* 判断是否有某个插件
* @param pluginName 插件名称 string
* @returns {Boolean} 返回布尔值
*/
function havePlugin(pluginName) {
const addonsDom = document.querySelector("#addons_js");
let addonsArr = [];
let arr = [];
if (addonsDom) {
addonsArr = JSON.parse(addonsDom.getAttribute("addons_js")); // 插件列表
arr = addonsArr.map((item) => {
return item.name;
});
}
return arr.includes(pluginName);
}
/**
* 根据插件名称获取插件Id
* @param pluginName 插件名称 string
* @returns { String | Number } id
*/
function getPluginId(pluginName) {
const addonsDom = document.querySelector("#addons_js") || [];
if (addonsDom) {
const addonsArr = JSON.parse(addonsDom.getAttribute("addons_js")); // 插件列表
for (let index = 0; index < addonsArr.length; index++) {
const element = addonsArr[index];
if (pluginName === element.name) {
return element.id;
}
}
} else {
console.log("请检查页面是否有插件dom");
}
}
/**
* 获取当前会员中心主题
* @returns { String }
*/
function getClientareaTheme() {
// 放在html上的clientarea_theme
return document.documentElement.getAttribute("clientarea_theme") || "default";
}
/**
* 获取当前购物车主题
* @returns { String }
*/
function getCartTheme() {
// 放在html上的clientarea_theme
return document.documentElement.getAttribute("cart_theme") || "default";
}
/**
* 获取url参数
* @returns { Object } url地址参数
*/
function getUrlParams() {
const url = window.location.href;
// 判断是否有参数
if (url.indexOf("?") === -1) {
return {};
}
const params = url.split("?")[1];
const paramsArr = params.split("&");
const paramsObj = {};
paramsArr.forEach((item) => {
const key = item.split("=")[0];
const value = item.split("=")[1];
// 解析中文
paramsObj[key] = decodeURIComponent(value);
});
return paramsObj;
}
/**
* 导出EXCEL工具函数
* @params result axios请求返回的结果
* @returns { Promise }
*/
function exportExcelFun(res) {
return new Promise((resolve, reject) => {
try {
const fileName = res.headers["content-disposition"]
.split("filename=")[1]
.replace(new RegExp('"', "g"), "");
const blob = new Blob([res.data], {type: res.headers["content-type"]});
const downloadElement = document.createElement("a");
const href = window.URL.createObjectURL(blob); //创建下载的链接
downloadElement.href = href;
downloadElement.download = fileName; //下载后文件名
document.body.appendChild(downloadElement);
downloadElement.click(); //点击下载
document.body.removeChild(downloadElement); //下载完成移除元素
window.URL.revokeObjectURL(href); //释放掉blob对象
resolve();
} catch (err) {
console.log(err);
reject(err);
}
});
}
/**
* 用于检测插件多语言是否完整
* @param { Object } plugin_lang
* @returns // 控制台打印结果
*/
function checkLangFun(plugin_lang) {
// 判断 plugin_lang 对象中的各个语言包数量是否一致
const arr1 = Object.keys(plugin_lang["zh-cn"]);
const arr2 = Object.keys(plugin_lang["en-us"]);
const arr3 = Object.keys(plugin_lang["zh-hk"]);
if (
arr1.length !== arr2.length ||
arr1.length !== arr3.length ||
arr2.length !== arr3.length
) {
// 找出哪个语言包的哪个变量丢失了
const totalObj = Object.assign(
{},
plugin_lang["zh-cn"],
plugin_lang["en-us"],
plugin_lang["zh-hk"]
);
Object.keys(totalObj).forEach((item) => {
if (!arr1.includes(item)) {
console.log("zh-cn缺少", item);
}
if (!arr2.includes(item)) {
console.log("en-us缺少", item);
}
if (!arr3.includes(item)) {
console.log("zh-hk缺少", item);
}
});
}
}
/**
* 获取浏览器语言
* @returns {string}
*/
function getBrowserLanguage() {
if (!sessionStorage.getItem("brow_lang")) {
let langType = "zh-cn";
const lang =
navigator.language ||
navigator.browserLanguage ||
navigator.systemLanguage ||
navigator.userLanguage;
if (lang.indexOf("zh-cn") !== -1 || lang.indexOf("zh-CN") !== -1) {
langType = "zh-cn";
} else if (
lang.indexOf("zh-tw") !== -1 ||
lang.indexOf("zh-TW") !== -1 ||
lang.indexOf("zh-hk") !== -1 ||
lang.indexOf("zh-HK") !== -1
) {
langType = "zh-hk";
} else {
langType = "en-us";
}
sessionStorage.setItem("brow_lang", langType);
}
return sessionStorage.getItem("brow_lang");
}
/**
* 复制文本工具函数
* @param {string} text
* @returns {void}
*/
function copyText(text) {
// 检查浏览器是否支持 clipboard API
if (navigator.clipboard) {
navigator.clipboard
.writeText(text)
.then(() => {
Vue.prototype.$message.success(lang.pay_text17);
})
.catch((err) => {
console.error("文本复制失败:", err);
});
} else {
// 动态创建 textarea 标签
const textarea = document.createElement("textarea");
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = "readonly";
textarea.style.position = "absolute";
textarea.style.top = "0px";
textarea.style.left = "-9999px";
textarea.style.zIndex = "-9999";
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = text;
// 将 textarea 插入到 el 中
document.body.appendChild(textarea);
// 兼容IOS 没有 select() 方法
if (textarea.createTextRange) {
textarea.select(); // 选中值并复制
} else {
textarea.setSelectionRange(0, text.length);
textarea.focus();
}
const result = document.execCommand("Copy");
if (result) {
Vue.prototype.$message.success(lang.pay_text17);
}
document.body.removeChild(textarea);
}
}