2.1 版本文件

This commit is contained in:
2026-08-14 10:55:06 +08:00
commit a3434ec620
67 changed files with 12559 additions and 0 deletions
+305
View File
@@ -0,0 +1,305 @@
/**
* 商城前台 AJAX 封装
*
* 提供统一的 XHR 请求方法、购物车操作、签到、工单提交等。
* 依赖:页面中需有 <meta name="csrf" content="..."> 或全局变量 FNW_CSRF
*/
var Fnw = (function () {
'use strict';
// ─── CSRF Token ───
function getCsrfToken() {
var m = document.querySelector('meta[name="csrf"]');
return m ? m.getAttribute('content') : (window.FNW_CSRF || '');
}
// ─── 通用 XHR ───
/**
* 发送请求
* @param {Object} opts
* @param {string} opts.action - API action 名称
* @param {string} [opts.method='GET'] - HTTP 方法
* @param {Object} [opts.data] - POST 数据
* @param {function} [opts.success] - 成功回调 (res)
* @param {function} [opts.error] - 失败回调 (res)
* @param {function} [opts.complete] - 完成回调
*/
function request(opts) {
var method = (opts.method || 'GET').toUpperCase();
var url = 'api.php?action=' + encodeURIComponent(opts.action);
var xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.setRequestHeader('Accept', 'application/json');
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
if (method === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
}
// CSRF header for POST
if (method === 'POST') {
xhr.setRequestHeader('X-CSRF-Token', getCsrfToken());
}
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
if (opts.complete) opts.complete();
if (xhr.status === 0) { if (opts.error) opts.error({ ok: false, msg: '网络错误,请检查连接' }); return; }
var res;
try { res = JSON.parse(xhr.responseText); } catch (e) { res = { ok: false, msg: '服务器响应异常' }; }
if (res.ok) {
if (opts.success) opts.success(res);
} else {
if (opts.error) opts.error(res);
}
};
if (method === 'POST' && opts.data) {
var params = [];
// Always include csrf_token in POST body as fallback
opts.data.csrf_token = getCsrfToken();
for (var k in opts.data) {
if (opts.data.hasOwnProperty(k)) {
params.push(encodeURIComponent(k) + '=' + encodeURIComponent(opts.data[k]));
}
}
xhr.send(params.join('&'));
} else {
// Append GET params
if (opts.data && method === 'GET') {
var qs = [];
for (var k in opts.data) {
if (opts.data.hasOwnProperty(k)) {
qs.push(encodeURIComponent(k) + '=' + encodeURIComponent(opts.data[k]));
}
}
if (qs.length) url += '&' + qs.join('&');
}
xhr.send();
}
}
// ─── Toast 提示 ───
var toastTimer = null;
function showToast(msg, type) {
type = type || 'ok'; // 'ok' | 'err' | 'info'
var existing = document.getElementById('fnwToast');
if (existing) existing.remove();
var el = document.createElement('div');
el.id = 'fnwToast';
el.className = 'fnw-toast fnw-toast-' + type;
el.innerHTML = '<span class="fnw-toast-icon">' +
(type === 'ok' ? '&#10003;' : type === 'err' ? '&#10007;' : '&#9432;') +
'</span><span class="fnw-toast-msg">' + msg + '</span>';
Object.assign(el.style, {
position: 'fixed',
top: '-60px',
left: '50%',
transform: 'translateX(-50%)',
zIndex: '9999',
padding: '12px 24px',
borderRadius: '10px',
fontSize: '.92rem',
fontWeight: '500',
display: 'flex',
alignItems: 'center',
gap: '8px',
boxShadow: '0 8px 24px rgba(0,0,0,.15)',
transition: 'top .3s ease',
maxWidth: '90vw'
});
var colors = {
ok: { bg: '#f0fdf4', border: '#86efac', color: '#166534', icon: '#22c55e' },
err: { bg: '#fef2f2', border: '#fca5a5', color: '#991b1b', icon: '#ef4444' },
info: { bg: '#eff6ff', border: '#93c5fd', color: '#1e40af', icon: '#3b82f6' }
};
var c = colors[type] || colors.info;
el.style.background = c.bg;
el.style.border = '1px solid ' + c.border;
el.style.color = c.color;
el.querySelector('.fnw-toast-icon').style.color = c.icon;
document.body.appendChild(el);
// Animate in
requestAnimationFrame(function () {
el.style.top = '20px';
});
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
el.style.top = '-60px';
setTimeout(function () { el.remove(); }, 320);
}, 2800);
return el;
}
// ─── 购物车 ───
var cart = {
add: function (productId, qty, callback) {
request({
action: 'cart_add',
method: 'POST',
data: { product_id: productId, qty: qty || 1 },
success: function (res) {
showToast(res.msg || '已加入购物车', 'ok');
cart.updateBadge(res.data.cart_count);
if (callback) callback(null, res.data);
},
error: function (res) {
showToast(res.msg || '加入购物车失败', 'err');
if (callback) callback(new Error(res.msg));
}
});
},
update: function (productId, qty, callback) {
request({
action: 'cart_update',
method: 'POST',
data: { product_id: productId, qty: qty },
success: function (res) {
cart.updateBadge(res.data.cart_count);
if (callback) callback(null, res.data);
},
error: function (res) {
showToast(res.msg || '更新失败', 'err');
if (callback) callback(new Error(res.msg));
}
});
},
remove: function (productId, callback) {
request({
action: 'cart_remove',
method: 'POST',
data: { product_id: productId },
success: function (res) {
showToast('已移除', 'ok');
cart.updateBadge(res.data.cart_count);
if (callback) callback(null, res.data);
},
error: function (res) {
showToast(res.msg || '移除失败', 'err');
if (callback) callback(new Error(res.msg));
}
});
},
clear: function (callback) {
request({
action: 'cart_clear',
method: 'POST',
success: function (res) {
showToast('购物车已清空', 'ok');
cart.updateBadge(0);
if (callback) callback(null);
},
error: function (res) {
showToast(res.msg || '清空失败', 'err');
if (callback) callback(new Error(res.msg));
}
});
},
updateBadge: function (count) {
var badges = document.querySelectorAll('.badge.cart-badge');
badges.forEach(function (b) {
b.textContent = count;
b.style.display = count > 0 ? 'inline' : 'none';
});
// Also try nav-link badge
var navBadge = document.querySelector('a[href="cart"] .badge');
if (navBadge) {
navBadge.textContent = count;
navBadge.style.display = count > 0 ? 'inline' : 'none';
}
}
};
// ─── 签到 ───
var sign = {
do: function (callback) {
request({
action: 'sign_in',
method: 'POST',
success: function (res) {
showToast(res.msg || '签到成功!', 'ok');
if (callback) callback(null, res.data);
},
error: function (res) {
showToast(res.msg || '签到失败', 'err');
if (callback) callback(new Error(res.msg));
}
});
}
};
// ─── 工单 ───
var ticket = {
submit: function (data, callback) {
request({
action: 'ticket_submit',
method: 'POST',
data: data,
success: function (res) {
showToast(res.msg || '工单提交成功!', 'ok');
if (callback) callback(null, res.data);
},
error: function (res) {
showToast(res.msg || '提交失败', 'err');
if (callback) callback(new Error(res.msg));
}
});
}
};
// ─── 搜索(防抖)─────────────────────────────────────────────
var searchTimer = null;
var search = {
/**
* 带防抖的搜索
* @param {string} q 关键词
* @param {number} [delay=400] 防抖毫秒数
* @param {function} callback 回调 (err, results)
*/
query: function (q, delay, callback) {
if (typeof delay === 'function') { callback = delay; delay = 400; }
clearTimeout(searchTimer);
if (!q || q.length < 1) { if (callback) callback(null, []); return; }
searchTimer = setTimeout(function () {
request({
action: 'search',
data: { q: q },
success: function (res) { if (callback) callback(null, res.data); },
error: function (res) { if (callback) callback(new Error(res.msg)); }
});
}, delay);
}
};
// ─── 公开 API ───
return {
request: request,
toast: showToast,
cart: cart,
sign: sign,
ticket: ticket,
search: search,
csrf: getCsrfToken
};
})();
// ─── 页面加载后初始化:注入 CSRF meta ───
(function () {
// 如果页面还没有 csrf meta tag,尝试从表单中获取并注入
if (!document.querySelector('meta[name="csrf"]')) {
var csrfInput = document.querySelector('input[name="csrf"]');
if (csrfInput) {
var meta = document.createElement('meta');
meta.name = 'csrf';
meta.content = csrfInput.value;
document.head.appendChild(meta);
}
}
})();
+441
View File
@@ -0,0 +1,441 @@
{
"_meta": {
"name": "English",
"code": "en_US",
"direction": "ltr"
},
"common": {
"site_name": "Free Cloud Mall",
"home": "Home",
"all_products": "All Products",
"search": "Search",
"search_placeholder": "Search products…",
"cart": "Cart",
"my_orders": "My Orders",
"my_tickets": "My Tickets",
"submit_ticket": "Submit Ticket",
"announcements": "Announcements",
"settings": "Settings",
"sign_in": "Daily Sign-in",
"invite_friends": "Invite Friends",
"logout": "Logout",
"login": "Login",
"register": "Register",
"loading": "Loading…",
"no_data": "No data available",
"confirm": "Confirm",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"close": "Close",
"more": "More",
"back": "Back",
"next": "Next",
"prev": "Previous",
"submit": "Submit",
"reset": "Reset",
"total": "Total",
"price": "Price",
"stock": "Stock",
"category": "Category",
"status": "Status",
"action": "Action",
"name": "Name",
"description": "Description",
"date": "Date",
"time": "Time",
"quantity": "Quantity",
"amount": "Amount",
"points": "Points",
"remaining": "Remaining",
"items": "items",
"unit_yuan": "CNY",
"email_verified": "Email Verified",
"email_unverified": "Email Not Verified"
},
"hero": {
"title": "Free Cloud Mall",
"subtitle": "Public Welfare · Affordable · Carefully Selected for You"
},
"product": {
"buy_now": "Buy Now",
"add_to_cart": "Add to Cart",
"out_of_stock": "Out of Stock",
"in_stock": "In Stock",
"points_price": "{points} pts",
"cash_price": "Order Directly",
"period": "Period",
"detail": "Product Details",
"purchased": "Purchase History",
"about_mall": "About Mall"
},
"cart": {
"title": "Shopping Cart",
"empty": "Your cart is empty, go shopping!",
"clear": "Clear Cart",
"checkout": "Checkout",
"continue_shop": "Continue Shopping",
"total": "Total",
"pay_method": "Payment Method",
"points_pay": "Points Payment",
"cash_pay": "Cash Payment (Requires Admin Approval)",
"confirm_order": "Confirm Order",
"remove": "Remove",
"update_qty": "Update Quantity"
},
"order": {
"order_list": "My Orders",
"order_no": "Order No.",
"order_time": "Order Time",
"order_detail": "View Details",
"pending": "Pending",
"paid": "Paid",
"shipped": "Shipped",
"completed": "Completed",
"cancelled": "Cancelled",
"address": "Shipping Address",
"phone": "Phone Number",
"remark": "Remark",
"expire_at": "Expires At",
"renew": "Renew",
"delivery_info": "Delivery Info"
},
"auth": {
"login_title": "Welcome Back",
"register_title": "Create Account",
"username": "Username",
"password": "Password",
"confirm_password": "Confirm Password",
"email": "Email",
"captcha": "CAPTCHA",
"refresh_captcha": "Refresh",
"forgot_password": "Forgot password?",
"no_account": "No account?",
"has_account": "Already have an account?",
"go_register": "Register",
"go_login": "Login",
"login_btn": "Sign In",
"register_btn": "Sign Up"
},
"ticket": {
"title": "Submit Ticket",
"dept": "Department",
"select_dept": "Select Department",
"subject": "Subject",
"content": "Description",
"priority": "Priority",
"low": "Low",
"normal": "Normal",
"high": "High",
"related_order": "Related Order (Optional)",
"submit": "Submit Ticket",
"my_tickets": "My Tickets",
"ticket_no": "Ticket No.",
"open": "Open",
"processing": "Processing",
"resolved": "Resolved",
"closed": "Closed",
"reply": "Reply",
"admin_reply": "Admin Reply",
"assignee": "Assignee"
},
"sign": {
"title": "Daily Sign-in",
"signed_today": "Already signed in today",
"not_signed": "Not signed in today",
"sign_btn": "Sign In",
"points_gained": "Earned {points} points",
"consecutive": "{days} consecutive days",
"rule": "Rule: Earn points daily by signing in. Points can be used to redeem products."
},
"user": {
"profile": "Profile",
"nickname": "Nickname",
"avatar": "Avatar",
"email_verified": "Email Verified",
"email_unverified": "Email Not Verified",
"verify_email": "Verify Email",
"resend": "Resend",
"change_pwd": "Change Password",
"current_pwd": "Current Password",
"new_pwd": "New Password",
"confirm_new_pwd": "Confirm New Password",
"my_points": "My Points",
"invite_code": "My Invite Code",
"invite_link": "Invite Link",
"invite_reward": "Invite Reward",
"invite_earned": "Earned from invites",
"copy_link": "Copy Link",
"copied": "Copied"
},
"announcement": {
"title": "Announcements",
"pinned": "Pinned",
"read_more": "Read More",
"published": "Published on",
"no_announcements": "No announcements"
},
"theme": {
"light": "Light Mode",
"dark": "Dark Mode",
"switch_theme": "Switch Theme"
},
"footer": {
"brand_slogan": "Carefully Selected · Public Welfare · Affordable",
"copyright": "All Rights Reserved",
"nav_home": "Mall Home",
"nav_all": "All Products",
"nav_cart": "Cart",
"nav_ticket": "Submit Ticket",
"nav_announce": "Announcements",
"nav_mytickets": "My Tickets",
"nav_myorders": "My Orders"
},
"admin": {
"dashboard": "Dashboard",
"products": "Products",
"groups": "Groups",
"orders": "Orders",
"tickets": "Tickets",
"departments": "Departments",
"users": "Users",
"admins": "Admins / CS",
"settings": "Settings",
"total_users": "Total Users",
"total_orders": "Total Orders",
"total_products": "Products",
"open_tickets": "Open Tickets",
"today_signins": "Today Sign-ins",
"recent_orders": "Recent Orders",
"my_pending": "My Pending",
"my_assigned": "Assigned to Me",
"add_product": "Add Product",
"edit_product": "Edit Product",
"delete_confirm": "Are you sure? This cannot be undone.",
"site_name_setting": "Site Name",
"change_password": "Change Password",
"mall_intro": "Mall Introduction",
"points_settings": "Points Settings",
"smtp_config": "SMTP Configuration",
"test_mail": "Send Test Email",
"save_success": "Saved Successfully",
"role_admin": "Admin",
"role_cs": "Customer Service"
},
"index": {
"announcement": "Announcement",
"more": "More",
"all": "All",
"no_result": "No matching products found, try another keyword.",
"points_unit": "pts"
},
"login": {
"title": "Login / Register",
"err_login_failed": "Incorrect username or password",
"err_captcha_wrong": "CAPTCHA is incorrect, please try again",
"err_username_short": "Username must be at least 3 characters",
"err_password_short": "Password must be at least 6 characters",
"err_password_mismatch": "Passwords do not match",
"err_email_invalid": "Please enter a valid email address",
"err_invite_invalid": "Invalid invite code, please check or leave blank",
"err_ip_limit": "Daily registration limit reached (2/day) from this network, please try again tomorrow",
"err_username_exists": "This username is already taken",
"tab_login": "Login",
"tab_register": "Register",
"label_username": "Username",
"label_password": "Password",
"label_email": "Email (for order & ticket notifications and email verification)",
"label_email_required": "Email *",
"label_captcha": "CAPTCHA",
"captcha_placeholder": "Enter the 4 digits above",
"captcha_hint": "Can't see clearly? Click to refresh",
"label_confirm_pwd": "Confirm Password",
"label_invite": "Invite Code (optional)",
"invite_placeholder": "Enter invite code if you have one for rewards",
"username_placeholder": "At least 3 characters",
"email_placeholder": "Required, e.g. you@example.com",
"pwd_placeholder": "At least 6 characters",
"btn_login": "Login",
"btn_register": "Register & Login",
"register_tip": "By registering you agree to our terms of service; max 2 registrations per network per day."
},
"cart_page": {
"title": "My Shopping Cart",
"empty": "Your cart is empty, go {link}~",
"empty_link": "find something good",
"points_each": "pts/item",
"update_qty": "Update Quantity",
"clear_confirm": "Clear the cart?",
"clear_btn": "Clear",
"checkout_btn": "Checkout",
"continue_btn": "Continue Shopping",
"remove_title": "Remove",
"subtotal_free": "—"
},
"ticket_page": {
"title": "Submit Ticket",
"desc_hint": "Having issues? Submit a ticket and staff will respond in the admin panel. You'll also receive email notifications.",
"type_general": "General Ticket",
"type_renewal": "Renewal Request",
"select_dept": "Select Department",
"related_order_label": "Related Order (required for renewal)",
"order_no_link": "No association",
"subject_placeholder": "Briefly describe your issue",
"subject_renewal_prefix": "Renewal: Order #",
"message_placeholder": "Please describe your issue in detail, including reproduction steps",
"contact_email": "Contact Email",
"submit_btn": "Submit Ticket",
"view_my_tickets": "View My Tickets",
"err_subject_empty": "Please enter a subject",
"err_subject_long": "Subject too long (max 160 chars)",
"err_message_empty": "Please enter a description",
"err_priority_bad": "Invalid priority level",
"err_order_invalid": "Please select a valid renewal order",
"renewal_period": "Period",
"renewal_expire": "Expires"
},
"mytickets": {
"title": "My Tickets",
"ok_msg": "Operation successful. Staff will review and respond as soon as possible (you'll receive email notifications if SMTP is configured).",
"ticket_detail": "Ticket #{id}",
"meta_type": "Type: ",
"meta_dept": "Department: ",
"meta_priority": "Priority: ",
"meta_time": "Submitted: ",
"meta_unspecified": "Unspecified",
"renewal_note": "Related Order: {link} (staff will renew this order after approval)",
"bubble_me": "Me · ",
"bubble_staff": "Staff · ",
"reply_label": "Follow-up Reply",
"reply_placeholder": "Add more details or feedback about your issue",
"send_reply": "Send Reply",
"back_list": "Back to List",
"closed_lock": "This ticket is closed. You can reopen it to continue, or submit a new ticket.",
"reopen_btn": "Reopen Ticket",
"list_title": "My Tickets",
"new_ticket": "New Ticket",
"no_tickets": "You haven't submitted any tickets yet.",
"view_btn": "View",
"col_id": "#",
"col_subject": "Subject",
"col_type": "Type",
"col_dept": "Department",
"col_priority": "Priority",
"col_status": "Status",
"col_updated": "Updated",
"err_no_perm": "Ticket not found or no permission",
"err_not_closed": "This ticket is not closed, no need to reopen",
"err_closed_reply": "This ticket is closed. Cannot reply; please submit a new ticket for further feedback",
"err_reply_empty": "Please enter reply content",
"err_reply_long": "Reply too long (max 5000 chars)"
},
"myorders": {
"title": "My Orders",
"paid_success": "Order {no} submitted successfully, marked as \"Paid\".",
"no_orders": "You don't have any orders yet, go {link}~",
"shop_link": "browse the mall",
"order_no_label": "Order No.: ",
"detail_btn": "View Details",
"total_label": "Total: ",
"pay_points": "· {pts} pts",
"shipping_label": "Shipping: ",
"period_label": "Period: ",
"expire_label": "Expires: ",
"renew_btn": "Request Renewal",
"items_fmt": "{name} × {qty}"
},
"settings_page": {
"title": "User Settings",
"saved": "Profile saved.",
"saved_email_change": "Profile saved. Email verification required due to email change.",
"err_username_short": "Username must be at least 3 characters",
"err_email_invalid": "Please enter a valid email address",
"err_nickname_long": "Nickname must be at most 30 characters",
"err_username_taken": "This username is already taken",
"avatar_section": "Avatar",
"select_image": "Select Image",
"avatar_hint": "JPG / PNG / WebP / GIF accepted, max 2MB, min 64×64",
"uploading": "Uploading…",
"upload_ok": "Avatar updated successfully",
"upload_fail": "Upload failed",
"img_too_big": "Image must not exceed 2MB",
"network_err": "Network error ({code})",
"network_retry": "Network error, please retry",
"response_err": "Unexpected response",
"save_btn": "Save Changes",
"back_orders": "Back to My Orders",
"verify_section": "Email Verification",
"verify_done": "Your email has been verified.",
"verify_not_done": "Email not verified yet. {link}.",
"verify_send_link": "Click to send verification email",
"nickname_placeholder": "Optional, max 30 chars"
},
"product_page": {
"back_list": "Back to Products",
"category_label": "",
"points_price_label": "Points Price: ",
"points_suffix": " pts",
"cash_price_label": "Order Directly",
"stock_label": "Stock: ",
"stock_unit": " items",
"period_label": "Period: ",
"qty_label": "Quantity",
"add_cart_btn": "Add to Cart",
"sold_out": "Out of Stock",
"err_out_of_stock": "This product is temporarily out of stock",
"err_low_stock": "Insufficient stock, maximum {max} can be purchased",
"purchased_title": "You Have Purchased This Product",
"purchased_toggle": "View Product Details",
"purchased_toggle_close": "Collapse",
"purchased_activated": "Activated",
"purchased_not_activated": "Not Activated",
"purchased_product_info": "Product Info: ",
"server_ip": "Server IP: ",
"conn_addr": "Connection Address: ",
"login_user": "Login Account: ",
"login_pass": "Login Password: ",
"remark_label": "Remark: ",
"pending_delivery": "Order placed, waiting for admin activation and product info.",
"reveal_btn": "Reveal",
"none_value": "—"
},
"admin_extra": {
"logs": "System Logs",
"log_viewer_title": "System Logs",
"log_date_select": "Select Date: ",
"log_view_btn": "View",
"log_available_dates": "Available dates: ",
"log_total_entries": "({n} entries)",
"log_no_records": "No log records for this day.",
"log_mode_hint": "Log levels: DEBUG / INFO / WARN / ERROR. Current mode: <strong>{mode}</strong>",
"log_debug_config": "You can enable DEBUG level by setting define('DEBUG', true) in config.php.",
"log_col_time": "Time",
"log_col_level": "Level",
"log_col_source": "Source",
"log_col_content": "Content",
"log_prev": "« Previous",
"log_next": "Next »",
"log_page_info": "Page {p} of {t}",
"cs_readonly": "Customer Service accounts are read-only. Please contact an administrator for changes.",
"admins_title": "Administrators & CS",
"admins_add_title": "Add Account (Admin / CS)",
"admins_add_desc": "Administrators have full access; CS staff can view all pages but only handle tickets.",
"admins_cs_cannot_add": "CS accounts cannot add administrators. Please contact an admin.",
"admins_list_title": "Account List",
"admins_super_tag": "Super",
"admins_protected": "(Protected)",
"admins_current": "Current Account",
"admins_super_protected": "Protected",
"admins_role_confirm": "Change {user}'s role to \"{role}\"?",
"admins_super_warn": "The Super Admin (ID=1) is protected from deletion or demotion. Other admins can be deleted or demoted by fellow admins.",
"admins_err_self_role": "Cannot change the role of the currently logged-in account.",
"admins_err_super_role": "Cannot change the role of the Super Admin (ID=1).",
"admins_err_self_del": "Cannot delete the currently logged-in account.",
"admins_err_super_del": "Cannot delete the Super Admin (ID=1). This account is system-protected.",
"admins_err_last_admin": "At least one admin must remain. Cannot delete.",
"admins_role_changed": "Account #{id} role changed to \"{role}\".",
"users_title": "User Management ({n} total)",
"cs_readonly_users": "CS accounts can only view the user list."
}
}
+441
View File
@@ -0,0 +1,441 @@
{
"_meta": {
"name": "简体中文",
"code": "zh_CN",
"direction": "ltr"
},
"common": {
"site_name": "自由云商城",
"home": "首页",
"all_products": "全部商品",
"search": "搜索",
"search_placeholder": "搜索商品名称…",
"cart": "购物车",
"my_orders": "我的订单",
"my_tickets": "我的工单",
"submit_ticket": "提交工单",
"announcements": "公告中心",
"settings": "用户设置",
"sign_in": "每日签到",
"invite_friends": "邀请好友",
"logout": "退出登录",
"login": "登录",
"register": "注册",
"loading": "加载中…",
"no_data": "暂无数据",
"confirm": "确认",
"cancel": "取消",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"close": "关闭",
"more": "更多",
"back": "返回",
"next": "下一步",
"prev": "上一步",
"submit": "提交",
"reset": "重置",
"total": "合计",
"price": "价格",
"stock": "库存",
"category": "分类",
"status": "状态",
"action": "操作",
"name": "名称",
"description": "描述",
"date": "日期",
"time": "时间",
"quantity": "数量",
"amount": "金额",
"points": "积分",
"remaining": "剩余",
"items": "件",
"unit_yuan": "元",
"email_verified": "邮箱已验证",
"email_unverified": "邮箱未验证"
},
"hero": {
"title": "自由云商城",
"subtitle": "公益 · 实惠 · 用心挑选的每一件好物"
},
"product": {
"buy_now": "立即购买",
"add_to_cart": "加入购物车",
"out_of_stock": "缺货",
"in_stock": "有货",
"points_price": "{points} 分",
"cash_price": "直接下单",
"period": "周期",
"detail": "商品详情",
"purchased": "已购买记录",
"about_mall": "关于商城"
},
"cart": {
"title": "购物车",
"empty": "购物车是空的,去逛逛吧",
"clear": "清空购物车",
"checkout": "去结算",
"continue_shop": "继续购物",
"total": "总计",
"pay_method": "支付方式",
"points_pay": "积分支付",
"cash_pay": "现金支付(需管理员审核)",
"confirm_order": "确认下单",
"remove": "移除",
"update_qty": "更新数量"
},
"order": {
"order_list": "我的订单",
"order_no": "订单号",
"order_time": "下单时间",
"order_detail": "查看详情",
"pending": "待处理",
"paid": "已付款",
"shipped": "已发货",
"completed": "已完成",
"cancelled": "已取消",
"address": "收货地址",
"phone": "联系电话",
"remark": "备注",
"expire_at": "到期时间",
"renew": "续期",
"delivery_info": "发货信息"
},
"auth": {
"login_title": "欢迎回来",
"register_title": "创建账户",
"username": "用户名",
"password": "密码",
"confirm_password": "确认密码",
"email": "邮箱",
"captcha": "验证码",
"refresh_captcha": "换一张",
"forgot_password": "忘记密码?",
"no_account": "没有账号?",
"has_account": "已有账号?",
"go_register": "去注册",
"go_login": "去登录",
"login_btn": "登录",
"register_btn": "注册"
},
"ticket": {
"title": "提交工单",
"dept": "部门",
"select_dept": "选择部门",
"subject": "标题",
"content": "描述内容",
"priority": "优先级",
"low": "低",
"normal": "普通",
"high": "高",
"related_order": "关联订单(可选)",
"submit": "提交工单",
"my_tickets": "我的工单",
"ticket_no": "工单号",
"open": "待处理",
"processing": "处理中",
"resolved": "已解决",
"closed": "已关闭",
"reply": "回复",
"admin_reply": "处理回复",
"assignee": "负责人"
},
"sign": {
"title": "每日签到",
"signed_today": "今天已签到",
"not_signed": "今日尚未签到",
"sign_btn": "签到",
"points_gained": "获得 {points} 积分",
"consecutive": "连续签到 {days} 天",
"rule": "签到规则:每天签到可获得积分,积分可用于兑换商品。"
},
"user": {
"profile": "个人资料",
"nickname": "昵称",
"avatar": "头像",
"email_verified": "邮箱已验证",
"email_unverified": "邮箱未验证",
"verify_email": "验证邮箱",
"resend": "重新发送",
"change_pwd": "修改密码",
"current_pwd": "当前密码",
"new_pwd": "新密码",
"confirm_new_pwd": "确认新密码",
"my_points": "我的积分",
"invite_code": "我的邀请码",
"invite_link": "邀请链接",
"invite_reward": "邀请奖励",
"invite_earned": "已获得邀请积分",
"copy_link": "复制链接",
"copied": "已复制"
},
"announcement": {
"title": "公告中心",
"pinned": "置顶",
"read_more": "阅读全文",
"published": "发布于",
"no_announcements": "暂无公告"
},
"theme": {
"light": "浅色模式",
"dark": "深色模式",
"switch_theme": "切换主题"
},
"footer": {
"brand_slogan": "用心挑选的每一件好物 · 公益 · 实惠",
"copyright": "版权所有",
"nav_home": "商城首页",
"nav_all": "全部商品",
"nav_cart": "购物车",
"nav_ticket": "提交工单",
"nav_announce": "公告中心",
"nav_mytickets": "我的工单",
"nav_myorders": "我的订单"
},
"admin": {
"dashboard": "仪表盘",
"products": "商品管理",
"groups": "商品分组",
"orders": "订单管理",
"tickets": "工单管理",
"departments": "工单部门",
"users": "用户管理",
"admins": "管理员/客服",
"settings": "设置",
"total_users": "总用户数",
"total_orders": "总订单数",
"total_products": "商品数",
"open_tickets": "待处理工单",
"today_signins": "今日签到",
"recent_orders": "最近订单",
"my_pending": "我待处理",
"my_assigned": "我负责的工单",
"add_product": "新增商品",
"edit_product": "编辑商品",
"delete_confirm": "确定删除吗?此操作不可撤销。",
"site_name_setting": "站点名称",
"change_password": "修改密码",
"mall_intro": "商城介绍",
"points_settings": "积分系统设置",
"smtp_config": "SMTP 邮件配置",
"test_mail": "发送测试邮件",
"save_success": "保存成功",
"role_admin": "管理员",
"role_cs": "客服"
},
"index": {
"announcement": "公告",
"more": "更多",
"all": "全部",
"no_result": "没有找到匹配的商品,换个关键词试试吧。",
"points_unit": "分"
},
"login": {
"title": "登录 / 注册",
"err_login_failed": "用户名或密码错误",
"err_captcha_wrong": "图形验证码不正确,请重新输入",
"err_username_short": "用户名至少 3 个字符",
"err_password_short": "密码至少 6 位",
"err_password_mismatch": "两次密码不一致",
"err_email_invalid": "请填写有效的邮箱地址",
"err_invite_invalid": "邀请码无效,请检查或留空",
"err_ip_limit": "当前网络今日注册次数已达上限(2 个/天),请明日再试",
"err_username_exists": "该用户名已被注册",
"tab_login": "登录",
"tab_register": "注册",
"label_username": "用户名",
"label_password": "密码",
"label_email": "邮箱(用于接收订单与工单通知,并完成邮箱验证)",
"label_email_required": "邮箱 *",
"label_captcha": "图形验证码",
"captcha_placeholder": "输入上图 4 位",
"captcha_hint": "看不清?点击刷新",
"label_confirm_pwd": "确认密码",
"label_invite": "邀请码(选填)",
"invite_placeholder": "如有邀请码可填写,享受邀请奖励",
"username_placeholder": "至少3位",
"email_placeholder": "必填,如 you@example.com",
"pwd_placeholder": "至少6位",
"btn_login": "登录",
"btn_register": "注册并登录",
"register_tip": "注册即表示同意公益服务条款;同一网络每日最多注册 2 个账号。"
},
"cart_page": {
"title": "我的购物车",
"empty": "购物车还是空的,去 {link} 吧~",
"empty_link": "挑点好物",
"points_each": "积分/件",
"update_qty": "更新数量",
"clear_confirm": "确定清空购物车?",
"clear_btn": "清空",
"checkout_btn": "去结算",
"continue_btn": "继续购物",
"remove_title": "移除",
"subtotal_free": "—"
},
"ticket_page": {
"title": "提交工单",
"desc_hint": "遇到问题?提交工单后,处理人员会在后台看到并回复,回复也会通过邮件通知你。",
"type_general": "普通工单",
"type_renewal": "续期申请",
"select_dept": "选择部门",
"related_order_label": "关联订单(续期申请时必选)",
"order_no_link": "不关联",
"subject_placeholder": "一句话概括你的问题",
"subject_renewal_prefix": "续期申请:订单 #",
"message_placeholder": "请详细描述你遇到的问题、复现步骤等",
"contact_email": "联系邮箱",
"submit_btn": "提交工单",
"view_my_tickets": "查看我的工单",
"err_subject_empty": "请填写工单标题",
"err_subject_long": "标题过长(最多 160 字)",
"err_message_empty": "请填写问题描述",
"err_priority_bad": "优先级不合法",
"err_order_invalid": "请选择有效的续期订单",
"renewal_period": "周期",
"renewal_expire": "到期"
},
"mytickets": {
"title": "我的工单",
"ok_msg": "操作成功,处理人员会尽快查看并回复(若已配置 SMTP 将邮件通知)。",
"ticket_detail": "工单 #{id}",
"meta_type": "类型:",
"meta_dept": "部门:",
"meta_priority": "优先级:",
"meta_time": "提交时间:",
"meta_unspecified": "未指定",
"renewal_note": "关联订单:{link}(处理人员批准后将为该订单续期)",
"bubble_me": "我 · ",
"bubble_staff": "处理人员 · ",
"reply_label": "追加回复",
"reply_placeholder": "补充说明你的问题或反馈处理结果",
"send_reply": "发送回复",
"back_list": "返回列表",
"closed_lock": "该工单已关闭,你可以重新开启后继续反馈,也可以提交新工单。",
"reopen_btn": "重新开启工单",
"list_title": "我的工单",
"new_ticket": "提交新工单",
"no_tickets": "你还没有提交过工单。",
"view_btn": "查看",
"col_id": "编号",
"col_subject": "标题",
"col_type": "类型",
"col_dept": "部门",
"col_priority": "优先级",
"col_status": "状态",
"col_updated": "更新时间",
"err_no_perm": "工单不存在或无权限",
"err_not_closed": "该工单未关闭,无需重新开启",
"err_closed_reply": "该工单已关闭,无法继续回复;如需反馈请提交新工单",
"err_reply_empty": "请填写回复内容",
"err_reply_long": "回复内容过长(最多 5000 字)"
},
"myorders": {
"title": "我的订单",
"paid_success": "订单 {no} 提交成功,已视为「已付款」。",
"no_orders": "你还没有订单,去 {link} 吧~",
"shop_link": "逛逛商城",
"order_no_label": "订单号:",
"detail_btn": "查看详情",
"total_label": "合计:",
"pay_points": "· {pts} 分",
"shipping_label": "收货:",
"period_label": "周期:",
"expire_label": "到期:",
"renew_btn": "申请续期",
"items_fmt": "{name} × {qty}"
},
"settings_page": {
"title": "用户设置",
"saved": "资料已保存。",
"saved_email_change": "资料已保存,因更换邮箱需重新完成邮箱验证。",
"err_username_short": "用户名至少 3 个字符",
"err_email_invalid": "请填写有效的邮箱地址",
"err_nickname_long": "昵称最多 30 个字符",
"err_username_taken": "该用户名已被占用",
"avatar_section": "头像",
"select_image": "选择图片",
"avatar_hint": "支持 JPG / PNG / WebP / GIF,最大 2MB,最小 64×64",
"uploading": "上传中…",
"upload_ok": "头像更新成功",
"upload_fail": "上传失败",
"img_too_big": "图片不能超过 2MB",
"network_err": "网络错误 ({code})",
"network_retry": "网络错误,请重试",
"response_err": "响应异常",
"save_btn": "保存修改",
"back_orders": "返回我的订单",
"verify_section": "邮箱验证",
"verify_done": "你的邮箱已完成验证。",
"verify_not_done": "邮箱尚未验证,{link}。",
"verify_send_link": "点击发送验证邮件",
"nickname_placeholder": "选填,最多 30 字"
},
"product_page": {
"back_list": "返回商品列表",
"category_label": "",
"points_price_label": "积分价:",
"points_suffix": " 积分",
"cash_price_label": "直接下单",
"stock_label": "库存:",
"stock_unit": " 件",
"period_label": "周期:",
"qty_label": "数量",
"add_cart_btn": "加入购物车",
"sold_out": "暂时缺货",
"err_out_of_stock": "该商品暂时缺货",
"err_low_stock": "库存不足,最多可购买 {max} 件",
"purchased_title": "你已购买该商品",
"purchased_toggle": "查看产品详情",
"purchased_toggle_close": "收起",
"purchased_activated": "已激活",
"purchased_not_activated": "未激活",
"purchased_product_info": "商品信息:",
"server_ip": "服务器 IP",
"conn_addr": "连接地址:",
"login_user": "登录账户:",
"login_pass": "登录密码:",
"remark_label": "备注:",
"pending_delivery": "已下单,等待管理员开通并填写商品信息。",
"reveal_btn": "显示",
"none_value": "—"
},
"admin_extra": {
"logs": "系统日志",
"log_viewer_title": "系统日志",
"log_date_select": "选择日期:",
"log_view_btn": "查看",
"log_available_dates": "可用日期:",
"log_total_entries": "(共 {n} 条)",
"log_no_records": "当天暂无日志记录。",
"log_mode_hint": "日志级别:DEBUG / INFO / WARN / ERROR。当前模式:<strong>{mode}</strong>",
"log_debug_config": "可在 config.php 中修改 define('DEBUG', true) 以开启 DEBUG 级别记录。",
"log_col_time": "时间",
"log_col_level": "级别",
"log_col_source": "来源",
"log_col_content": "内容",
"log_prev": "« 上一页",
"log_next": "下一页 »",
"log_page_info": "第 {p} / {t} 页",
"cs_readonly": "客服账号仅可查看,如需操作请联系管理员。",
"admins_title": "管理员与客服",
"admins_add_title": "添加账号(管理员 / 客服)",
"admins_add_desc": "管理员拥有全部后台权限;客服可查看所有页面但仅能处理工单相关操作。",
"admins_cs_cannot_add": "客服账号无法添加新管理员,如需操作请联系管理员。",
"admins_list_title": "账号列表",
"admins_super_tag": "超级",
"admins_protected": "(受保护)",
"admins_current": "当前账号",
"admins_super_protected": "受保护",
"admins_role_confirm": "确定将 {user} 的角色改为「{role}」?",
"admins_super_warn": "终极管理员受系统保护,不可被删除或降级。其他管理员可由同级管理员删除或改为客服。",
"admins_err_self_role": "不能修改当前登录账号的角色。",
"admins_err_super_role": "不能修改终极管理员(ID=1)的角色。",
"admins_err_self_del": "不能删除当前登录的账号。",
"admins_err_super_del": "不能删除终极管理员(ID=1),该账号受系统保护。",
"admins_err_last_admin": "至少要保留一名管理员,不能继续删除。",
"admins_role_changed": "已将账号 #{id} 角色改为「{role}」。",
"users_title": "用户管理(共 {n} 人)",
"cs_readonly_users": "客服账号仅可查看用户列表,无法执行操作。"
}
}
+263
View File
@@ -0,0 +1,263 @@
/*
* marked.js —— 轻量 Markdown 解析器(本地自带,不依赖任何 CDN)
* 用法:marked.parse(markdownString) -> htmlString
* 支持:标题、粗体/斜体/删除线、行内代码、代码块、引用、有序/无序列表(含嵌套)、
* 链接、图片、分隔线、GFM 表格、段落与软换行。
* 安全:先转义原文 HTML,链接 URL 做白名单过滤,避免 XSS。
*/
(function (global) {
'use strict';
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
// 仅转义属性里的引号(文本已在 escapeHtml 中转义过 < > &
function attrSafe(s) {
return String(s).replace(/"/g, '&quot;');
}
// URL 白名单:仅允许 http/https/mailto/tel/锚点/相对路径;拦截 javascript:/vbscript:/危险 data:
function safeUrl(url) {
var u = String(url || '').trim();
if (/^\s*(javascript|vbscript):/i.test(u)) return '#';
if (/^\s*data:/i.test(u) && !/^data:image\//i.test(u)) return '#';
return u;
}
// 行内解析:输入为「未转义」的 markdown 文本
function parseInline(text) {
var codes = [];
// 1. 先抽出行内代码,避免内部内容被后续规则破坏
text = String(text).replace(/`([^`]+)`/g, function (_m, c) {
codes.push(c);
return 'CODE' + (codes.length - 1) + '';
});
// 2. 转义 HTML(代码占位符只含字母数字与空字符,不受影响)
text = escapeHtml(text);
// 3. 图片 ![alt](url "title")
text = text.replace(/!\[([^\]]*)\]\(((?:[^()\s]|\([^()]*\))+)(?:\s+&quot;([^)]*?)&quot;)?\)/g, function (_m, alt, url, title) {
var t = title ? ' title="' + attrSafe(title) + '"' : '';
return '<img src="' + attrSafe(safeUrl(url)) + '" alt="' + attrSafe(alt) + '"' + t + '>';
});
// 4. 链接 [text](url "title")
text = text.replace(/\[([^\]]+)\]\(((?:[^()\s]|\([^()]*\))+)(?:\s+&quot;([^)]*?)&quot;)?\)/g, function (_m, txt, url, title) {
var t = title ? ' title="' + attrSafe(title) + '"' : '';
var ext = /^(https?:)?\/\//i.test(url);
var extAttr = ext ? ' target="_blank" rel="noopener noreferrer"' : '';
return '<a href="' + attrSafe(safeUrl(url)) + '"' + t + extAttr + '>' + txt + '</a>';
});
// 5. 粗体 **x** / __x__
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
text = text.replace(/__([^_]+)__/g, '<strong>$1</strong>');
// 6. 斜体 *x* / _x_
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
text = text.replace(/(^|[^\w])_([^_]+)_(?=[^\w]|$)/g, '$1<em>$2</em>');
// 7. 删除线 ~~x~~
text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
// 8. 还原行内代码
text = text.replace(/CODE(\d+)/g, function (_m, n) {
return '<code>' + escapeHtml(codes[+n]) + '</code>';
});
return text;
}
// 列表解析:返回 { html, next }
function parseList(lines, start) {
var first = lines[start];
var m = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(first);
var baseIndent = m[1].length;
var ordered = /\d+\./.test(m[2]);
var items = [];
var i = start;
while (i < lines.length) {
var line = lines[i];
if (/^\s*$/.test(line)) break; // 空行结束列表
var mm = /^(\s*)([-*+]|\d+\.)\s+(.*)$/.exec(line);
if (!mm) {
// 同级的后续非标记行视为当前条目的延续(懒继续)
if (items.length && /^\s*\S/.test(line) && !/^\s*[-*+]\s/.test(line) && !/^\s*\d+\.\s/.test(line)) {
items[items.length - 1].text.push(line.trim());
i++;
continue;
}
break;
}
var ind = mm[1].length;
if (ind < baseIndent) break; // 减缩进,结束
if (ind > baseIndent) { // 嵌套列表
var sub = parseList(lines, i);
items[items.length - 1].nested += sub.html;
i = sub.next;
continue;
}
// 同级新条目
items.push({ text: [mm[3]], nested: '' });
i++;
}
var tag = ordered ? 'ol' : 'ul';
var out = '<' + tag + '>';
for (var k = 0; k < items.length; k++) {
out += '<li>' + parseInline(items[k].text.join(' '));
if (items[k].nested) out += items[k].nested;
out += '</li>';
}
out += '</' + tag + '>';
return { html: out, next: i };
}
function splitRow(line) {
var t = line.trim();
if (t.charAt(0) === '|') t = t.slice(1);
if (t.charAt(t.length - 1) === '|') t = t.slice(0, -1);
return t.split('|').map(function (c) { return c.trim(); });
}
// 表格解析:返回 { html, next }
function parseTable(lines, start) {
var header = splitRow(lines[start]);
var sep = lines[start + 1];
var aligns = splitRow(sep).map(function (c) {
var left = c.charAt(0) === ':';
var right = c.charAt(c.length - 1) === ':';
if (left && right) return 'center';
if (right) return 'right';
if (left) return 'left';
return '';
});
var rows = [];
var j = start + 2;
while (j < lines.length && /\|/.test(lines[j]) && lines[j].trim() !== '') {
rows.push(splitRow(lines[j]));
j++;
}
function cell(tag, c, idx) {
var a = aligns[idx] ? ' style="text-align:' + aligns[idx] + '"' : '';
return '<' + tag + a + '>' + parseInline(c) + '</' + tag + '>';
}
var html = '<table><thead><tr>';
for (var h = 0; h < header.length; h++) html += cell('th', header[h], h);
html += '</tr></thead><tbody>';
for (var r = 0; r < rows.length; r++) {
html += '<tr>';
for (var c = 0; c < rows[r].length; c++) html += cell('td', rows[r][c], c);
html += '</tr>';
}
html += '</tbody></table>';
return { html: html, next: j };
}
// 块级解析
function parse(src) {
src = (src || '').replace(/\r\n?/g, '\n');
var lines = src.split('\n');
var html = [];
var para = [];
var i = 0;
function flushPara() {
if (para.length) {
var text = para.join('\n');
// 段内软换行 -> <br>
var inline = parseInline(text).replace(/\n/g, '<br>\n');
html.push('<p>' + inline + '</p>');
para = [];
}
}
while (i < lines.length) {
var line = lines[i];
// 代码围栏 ```
if (/^\s*```/.test(line)) {
flushPara();
var lang = line.replace(/^\s*```/, '').trim();
var buf = [];
i++;
while (i < lines.length && !/^\s*```/.test(lines[i])) {
buf.push(lines[i]);
i++;
}
i++; // 跳过结束围栏
html.push('<pre><code' +
(lang ? ' class="language-' + attrSafe(lang) + '"' : '') +
'>' + escapeHtml(buf.join('\n')) + '</code></pre>');
continue;
}
// 分隔线
if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
flushPara();
html.push('<hr>');
i++;
continue;
}
// 标题 # ~ ######
var hm = /^\s*(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line);
if (hm) {
flushPara();
var lvl = hm[1].length;
html.push('<h' + lvl + '>' + parseInline(hm[2].trim()) + '</h' + lvl + '>');
i++;
continue;
}
// 引用 >
if (/^\s*>\s?/.test(line)) {
flushPara();
var qbuf = [];
while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
qbuf.push(lines[i].replace(/^\s*>\s?/, ''));
i++;
}
html.push('<blockquote>' + parse(qbuf.join('\n')) + '</blockquote>');
continue;
}
// 表格(当前行含 |,且下一行是分隔行)
if (/\|/.test(line) && i + 1 < lines.length &&
/^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && /-/.test(lines[i + 1])) {
flushPara();
var tbl = parseTable(lines, i);
html.push(tbl.html);
i = tbl.next;
continue;
}
// 列表
if (/^\s*([-*+]|\d+\.)\s+/.test(line)) {
flushPara();
var lst = parseList(lines, i);
html.push(lst.html);
i = lst.next;
continue;
}
// 空行
if (/^\s*$/.test(line)) {
flushPara();
i++;
continue;
}
// 段落文本
para.push(line);
i++;
}
flushPara();
return html.join('\n');
}
var api = { parse: parse, escapeHtml: escapeHtml };
if (typeof module !== 'undefined' && module.exports) {
module.exports = api;
}
global.marked = api;
})(typeof window !== 'undefined' ? window : (typeof globalThis !== 'undefined' ? globalThis : this));
+3355
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.