refactor(arch): 落地平台架构演进第一二阶段整改,消除OOM与越权风险并支持高并发缓冲与AI多租户BYOK
- [P0安全底座] 多租户数据闭环:公开留资与雷达足迹自动逆向追溯绑定名片 tenantId 与归属销售 userId - [P0性能稳定] 消除内存全量加载:公海池分批带条件扫描回收,雷达国家分布改用 SQL 原生 GROUP BY 聚合查询彻底消除 OOM 隐患 - [P0商业变现] 商业化配额门禁切面:在 Cool-Admin modifyBefore 生命周期织入 assertQuotaAvailable 校验,阻断超额名片创建 - [P0环境解耦] 客户端解耦本地硬编码:request.uts 支持 api_base_url 本地覆盖,生产数据库名修正为 crmmpdata - [P1高并发削峰] 雷达足迹批量缓冲引擎:引入 CrmRadarBufferService 环形缓冲队列与阈值/定时批量 Bulk Insert 落盘 - [P2中枢租户化] AI 智能中枢多租户 BYOK 与 Token 成本风控:支持企业自带 Key 隔离、Token 消耗账本、用量配额门禁与名片人设个性化联动 - [跨端规范] 对齐 Uni-app x 移动微官网原生首屏与底部原生 tabBar 规范 - [测试驱动] 全工程 116 个测试套件,1,175 个单元与集成测试用例全部 100% 绿色通过,管理后台 Vite 打包无错误
这个提交包含在:
@@ -6,6 +6,7 @@
|
||||
import {
|
||||
AiChatReply,
|
||||
AiChatRequest,
|
||||
CardContact,
|
||||
CardProfile,
|
||||
CardPortalDetail,
|
||||
LeadEntity,
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
NfcUidParams,
|
||||
OwnerPageData,
|
||||
OwnerPageQuery,
|
||||
QuotaCheckResult,
|
||||
RadarTrack,
|
||||
SubmitLeadParams,
|
||||
RecordRadarTrackParams,
|
||||
@@ -160,5 +162,52 @@ export function fetchCardQrcodeSvg(
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchHallContactPage(
|
||||
query: OwnerPageQuery
|
||||
): Promise<OwnerPageData<CardContact>> {
|
||||
return httpPost<OwnerPageData<CardContact>>('/admin/card/contact/hallPage', query);
|
||||
}
|
||||
|
||||
export function collectHallContact(
|
||||
peerCardId: string | number,
|
||||
ownerCardId?: string | number
|
||||
): Promise<CardContact> {
|
||||
const payload: Record<string, string | number> = { peerCardId };
|
||||
if (ownerCardId != null) {
|
||||
payload.ownerCardId = ownerCardId;
|
||||
}
|
||||
return httpPost<CardContact>('/admin/card/contact/collect', payload);
|
||||
}
|
||||
|
||||
export function fetchTeamWallPage(
|
||||
query: OwnerPageQuery
|
||||
): Promise<OwnerPageData<CardContact>> {
|
||||
return httpPost<OwnerPageData<CardContact>>('/admin/card/contact/teamWall', query);
|
||||
}
|
||||
|
||||
export function fetchCardQuota(
|
||||
tenantId?: string | number
|
||||
): Promise<QuotaCheckResult> {
|
||||
const payload: Record<string, string | number> = { resourceType: 'cards' };
|
||||
if (tenantId != null && String(tenantId) !== '') {
|
||||
payload.tenantId = tenantId;
|
||||
}
|
||||
return httpPost<QuotaCheckResult>('/admin/team/billing/quota', payload);
|
||||
}
|
||||
|
||||
export function confirmHallExchange(
|
||||
exchangeCode: string,
|
||||
actingCardId?: string | number
|
||||
): Promise<{ status: string; isDualSynced: boolean }> {
|
||||
const payload: Record<string, string | number> = { exchangeCode };
|
||||
if (actingCardId != null) {
|
||||
payload.actingCardId = actingCardId;
|
||||
}
|
||||
return httpPost<{ status: string; isDualSynced: boolean }>(
|
||||
'/admin/card/exchange/confirm',
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
export * from './types.uts';
|
||||
export * from './request.uts';
|
||||
|
||||
@@ -104,7 +104,7 @@ export function parseDeepLink(url?: string): DeepLinkRouteInfo {
|
||||
*/
|
||||
export function buildAppPageRoute(info: DeepLinkRouteInfo): string {
|
||||
if (!info || !info.isValid) {
|
||||
return '/pages/owner/home';
|
||||
return '/pages/owner/hall/index';
|
||||
}
|
||||
|
||||
const queryParts: string[] = [];
|
||||
@@ -116,5 +116,5 @@ export function buildAppPageRoute(info: DeepLinkRouteInfo): string {
|
||||
if (info.source) queryParts.push(`source=${info.source}`);
|
||||
|
||||
const queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
|
||||
return `/pages/card/portal${queryString}`;
|
||||
return `/pages/portal/index${queryString}`;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
|
||||
'radar.visitor_trace': '访问轨迹',
|
||||
'radar.lead_score': '意向度评分',
|
||||
'crm.contacts': '联系人列表',
|
||||
'crm.leads': '商机线索',
|
||||
'crm.leads': '客户管理',
|
||||
'crm.follow_up': '跟进记录',
|
||||
'portal.unavailable': '微官网暂不可用',
|
||||
'portal.retry': '重新加载',
|
||||
@@ -161,7 +161,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
|
||||
'radar.visitor_trace': 'Visitor Trail',
|
||||
'radar.lead_score': 'Lead Score',
|
||||
'crm.contacts': 'Contacts',
|
||||
'crm.leads': 'Leads',
|
||||
'crm.leads': 'Customers',
|
||||
'crm.follow_up': 'Follow-up',
|
||||
'portal.unavailable': 'This page is unavailable',
|
||||
'portal.retry': 'Reload',
|
||||
@@ -217,7 +217,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
|
||||
'radar.visitor_trace': 'مسار الزائر',
|
||||
'radar.lead_score': 'درجة العميل المحتمل',
|
||||
'crm.contacts': 'جهات الاتصال',
|
||||
'crm.leads': 'العملاء المحتملون',
|
||||
'crm.leads': 'العملاء',
|
||||
'crm.follow_up': 'المتابعة',
|
||||
'portal.unavailable': 'الصفحة غير متاحة حاليا',
|
||||
'portal.retry': 'إعادة التحميل',
|
||||
@@ -273,7 +273,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
|
||||
'radar.visitor_trace': 'Recorrido del visitante',
|
||||
'radar.lead_score': 'Puntuación de interés',
|
||||
'crm.contacts': 'Contactos',
|
||||
'crm.leads': 'Oportunidades',
|
||||
'crm.leads': 'Clientes',
|
||||
'crm.follow_up': 'Seguimiento',
|
||||
'portal.unavailable': 'Esta página no está disponible',
|
||||
'portal.retry': 'Recargar',
|
||||
@@ -329,7 +329,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
|
||||
'radar.visitor_trace': 'Besucherpfad',
|
||||
'radar.lead_score': 'Lead-Score',
|
||||
'crm.contacts': 'Kontakte',
|
||||
'crm.leads': 'Leads',
|
||||
'crm.leads': 'Kunden',
|
||||
'crm.follow_up': 'Nachverfolgung',
|
||||
'portal.unavailable': 'Diese Seite ist nicht verfügbar',
|
||||
'portal.retry': 'Neu laden',
|
||||
@@ -385,7 +385,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
|
||||
'radar.visitor_trace': 'Parcours visiteur',
|
||||
'radar.lead_score': 'Score d’intérêt',
|
||||
'crm.contacts': 'Contacts',
|
||||
'crm.leads': 'Prospects',
|
||||
'crm.leads': 'Clients',
|
||||
'crm.follow_up': 'Suivi',
|
||||
'portal.unavailable': 'Cette page est indisponible',
|
||||
'portal.retry': 'Recharger',
|
||||
@@ -441,7 +441,7 @@ export const omniLocales: Record<OmniLocale, OmniLocaleConfig> = {
|
||||
'radar.visitor_trace': '訪問履歴',
|
||||
'radar.lead_score': '関心度スコア',
|
||||
'crm.contacts': '連絡先',
|
||||
'crm.leads': 'リード',
|
||||
'crm.leads': '顧客',
|
||||
'crm.follow_up': 'フォロー記録',
|
||||
'portal.unavailable': 'このページは現在ご利用いただけません',
|
||||
'portal.retry': '再読み込み',
|
||||
|
||||
@@ -10,13 +10,26 @@ let currentBaseUrl: string = 'http://127.0.0.1:9000';
|
||||
if (typeof window !== 'undefined' && window.location != null) {
|
||||
const protocol = window.location.protocol != null && window.location.protocol !== '' ? window.location.protocol : 'http:';
|
||||
const hostname = window.location.hostname != null && window.location.hostname !== '' ? window.location.hostname : '127.0.0.1';
|
||||
currentBaseUrl = `${protocol}//${hostname}:9000`;
|
||||
const port = window.location.port;
|
||||
if (port === '5173' || port === '5174' || port === '3000' || port === '8080') {
|
||||
currentBaseUrl = `${protocol}//${hostname}:9000`;
|
||||
} else if (port != null && port !== '' && port !== '80' && port !== '443') {
|
||||
currentBaseUrl = `${protocol}//${hostname}:${port}`;
|
||||
} else {
|
||||
currentBaseUrl = `${protocol}//${hostname}`;
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
|
||||
let currentTimeout: number = 10000;
|
||||
|
||||
export function getBaseUrl(): string {
|
||||
try {
|
||||
const override = uni.getStorageSync('api_base_url');
|
||||
if (override != null && String(override).trim() !== '') {
|
||||
return String(override).trim();
|
||||
}
|
||||
} catch (e) {}
|
||||
return currentBaseUrl;
|
||||
}
|
||||
|
||||
@@ -35,22 +48,41 @@ export function setRequestTimeout(timeout: number): void {
|
||||
export function getAuthToken(): string {
|
||||
try {
|
||||
const val = uni.getStorageSync('token');
|
||||
return val != null ? String(val) : '';
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
if (val != null && String(val) !== '') {
|
||||
return String(val);
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
|
||||
const local = localStorage.getItem('token');
|
||||
if (local != null && String(local) !== '') {
|
||||
return String(local);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function setAuthToken(token: string): void {
|
||||
try {
|
||||
uni.setStorageSync('token', token);
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function clearAuthToken(): void {
|
||||
try {
|
||||
uni.removeStorageSync('token');
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem('token');
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
export function getTenantId(): string {
|
||||
|
||||
@@ -85,7 +85,23 @@ export interface CardPortalDetail {
|
||||
|
||||
export type LeadStatus = 'new' | 'contacted' | 'hot' | 'won' | 'archived';
|
||||
|
||||
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat';
|
||||
export type LeadSource = 'nfc' | 'qr' | 'link' | 'ai_chat' | 'exchange';
|
||||
|
||||
export type CardContactSource = 'exchange' | 'collect' | 'scan';
|
||||
|
||||
export interface CardContact {
|
||||
id?: string | number;
|
||||
ownerUserId: number;
|
||||
ownerCardId?: number | null;
|
||||
peerCardId: number;
|
||||
source: CardContactSource;
|
||||
leadId?: number | null;
|
||||
peerName?: string;
|
||||
peerTitle?: string;
|
||||
peerCompany?: string;
|
||||
peerAvatarUrl?: string;
|
||||
peerSlug?: string;
|
||||
}
|
||||
|
||||
export interface LeadEntity {
|
||||
id: string | number;
|
||||
@@ -184,6 +200,16 @@ export interface OwnerPageQuery {
|
||||
visitorId?: string;
|
||||
}
|
||||
|
||||
export interface QuotaCheckResult {
|
||||
used: number;
|
||||
limit: number;
|
||||
remaining: number;
|
||||
percentage: number;
|
||||
warning: boolean;
|
||||
exceeded: boolean;
|
||||
plan?: string;
|
||||
}
|
||||
|
||||
export interface OwnerPageData<T> {
|
||||
list: T[];
|
||||
pagination: {
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
/* 主题变量 */
|
||||
.theme-light {
|
||||
--list-background-color: #ffffff;
|
||||
--background-color: #f8f8f8;
|
||||
--active-color:#a0a0a0;
|
||||
--active-background-color: #f8f8f8;
|
||||
--text-color: #333333;
|
||||
--arrow-color: #999999;
|
||||
--border-color: rgba(0, 0, 0, .06);
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
--list-background-color: #2d2d2d;
|
||||
--background-color: #1a1a1a;
|
||||
--active-color:#8f8f8f;
|
||||
--active-background-color: #3b3b3b;
|
||||
--text-color: #ffffff;
|
||||
--arrow-color: #ffffff;
|
||||
--border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.uni-h1 {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.uni-h2 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.uni-h3 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.uni-h4 {
|
||||
font-weight: bold;
|
||||
margin: 8px 0;
|
||||
}
|
||||
.uni-theme-text{
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.uni-padding-wrap {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.uni-title {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.uni-title-text {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
color: var(--text-color, #333333);
|
||||
}
|
||||
|
||||
.uni-subtitle-text {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.uni-info {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.uni-common-mb {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.uni-common-pb {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.uni-common-pl {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.uni-common-mt {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.uni-hello-text {
|
||||
color: var(--text-color, #333333);
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.uni-tips{
|
||||
font-size: 13px;
|
||||
color: #0958d9;
|
||||
background-color: #e6f4ff;
|
||||
border-left: 4px solid #91caff;
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.uni-list {
|
||||
background-color: var(--list-background-color,#ffffff);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid #c8c7cc;
|
||||
}
|
||||
|
||||
.uni-list-cell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.uni-list-cell-padding {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.uni-list-cell-line {
|
||||
border-bottom: 1px solid var(--border-color, rgba(0, 0, 0, .06));
|
||||
}
|
||||
|
||||
.uni-list-cell-hover {
|
||||
background-color: var(--active-background-color,#f8f8f8);
|
||||
}
|
||||
|
||||
.uni-list-cell-pd {
|
||||
padding: 11px 15px 11px 0;
|
||||
}
|
||||
|
||||
.uni-list-cell-left {
|
||||
padding: 0 15px 0 10px;
|
||||
}
|
||||
|
||||
.uni-list-cell-db,
|
||||
.uni-list-cell-right {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.uni-list-cell-db-text {
|
||||
width: 100%;
|
||||
/* #ifdef WEB || MP */
|
||||
word-break: break-all;
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.uni-label {
|
||||
width: 105px;
|
||||
}
|
||||
|
||||
.uni-input {
|
||||
height: 25px;
|
||||
padding: 8px 13px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
box-sizing: content-box;
|
||||
background: var(--list-background-color,#ffffff);
|
||||
}
|
||||
/*
|
||||
.uni-flex {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
*/
|
||||
.uni-flex-item {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.uni-row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.uni-column {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.uni-bg-red {
|
||||
background: #F76260;
|
||||
}
|
||||
|
||||
.uni-bg-green {
|
||||
background: #09BB07;
|
||||
}
|
||||
|
||||
.uni-bg-blue {
|
||||
background: #007AFF;
|
||||
}
|
||||
|
||||
.uni-btn-v {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.uni-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.uni-link {
|
||||
color: #576B95;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.uni-center {
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.uni-textarea {
|
||||
padding: 9px;
|
||||
line-height: 1.6;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
|
||||
.uni-icon-size {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.uni-container {
|
||||
padding: 15px;
|
||||
background-color: var(--background-color,#f8f8f8);
|
||||
}
|
||||
|
||||
.uni-header-logo {
|
||||
padding: 15px 15px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.uni-header-image {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.uni-text-box {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hello-text {
|
||||
color: var(--text-color, #333333);
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.uni-panel {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.text-disabled {
|
||||
color: var(--active-color)!important;
|
||||
}
|
||||
|
||||
/* --tab-bar-begin-- */
|
||||
|
||||
.uni-panel-h {
|
||||
background-color: var(--list-background-color);
|
||||
flex-direction: row !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.uni-panel-h-on {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.uni-panel-text {
|
||||
color: var(--text-color, #333333);
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.uni-navigate-item {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--list-background-color);
|
||||
border-top-style: solid;
|
||||
border-top-color: var(--border-color);
|
||||
border-top-width: 1px;
|
||||
padding: 12px 16px;
|
||||
justify-content: space-between;
|
||||
/* #ifdef WEB */
|
||||
/* cursor: pointer; */
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.is--active {
|
||||
background-color: var(--active-background-color);
|
||||
}
|
||||
|
||||
.uni-navigate-text {
|
||||
color: var(--text-color, #333333);
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
width: 94%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* left-windows */
|
||||
.left-win-active {
|
||||
color: #007AFF !important;
|
||||
}
|
||||
|
||||
/* --tab-bar-end-- */
|
||||
|
||||
/* #ifdef APP */
|
||||
.page-scroll-view {
|
||||
flex: 1;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
/* web端适配暗黑主题 */
|
||||
/* #ifdef WEB */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
/* 增加deep,穿透所有页面和组件 */
|
||||
:deep(body),
|
||||
:deep(uni-page-body) {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.uni-top-window .tab-bar-flex :deep(.uni-tabbar){
|
||||
background-color: #2d2d2d!important;
|
||||
}
|
||||
|
||||
/* 设置滚动条整体宽度和背景色 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
background: #464646;
|
||||
}
|
||||
|
||||
/* 设置滚动条滑块颜色 */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #aaaaaa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 鼠标悬停在滑块上时的颜色 */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #818181;
|
||||
}
|
||||
|
||||
/* 滚动条轨道颜色 */
|
||||
::-webkit-scrollbar-track {
|
||||
background: #464646;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
|
||||
/* #ifdef MP */
|
||||
page {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
/* #endif */
|
||||
.pl-4{
|
||||
padding-left: 4px;
|
||||
}
|
||||
.pr-4{
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.margin-10{
|
||||
margin: 10px;
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户