Commit a3e29145 by DaiJiezhang

Refine wechat list layout and image actions

parent f7030db4
...@@ -347,6 +347,23 @@ async function saveWechatRecord(payload) { ...@@ -347,6 +347,23 @@ async function saveWechatRecord(payload) {
} }
/** /**
* 代码作用(白话):在列表里删除企微手机图片时,先取最新记录再清空图片字段并复用统一保存接口;关联文件:F:/Project/xyw_console/src/modules/wechat/wechat-list-runtime.js、F:/Project/xyw_console/src/modules/shared/wechat-api-client.js;关联逻辑(调用链/消息链/数据流):列表删除图片按钮 -> removeWechatRecordImage() -> getWechatRecordById() -> saveWechatRecord() -> refreshWechatList()。
*/
async function removeWechatRecordImage(recordId) {
const record = await getWechatRecordById(recordId);
if (!record) {
throw new Error('未找到对应企微记录');
}
const payload = {
...record,
phone_image: '',
phoneImage: '',
'手机图片': ''
};
return saveWechatRecord(payload);
}
/**
* 代码作用(白话):在企微保存成功后刷新当前页面,保证列表、详情和弹窗看到的是同一份最新数据;关联文件:F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):WechatAddDialogActions.afterSave() -> refreshWechatList() -> renderModule() -> 重新挂载 wechat 列表。 * 代码作用(白话):在企微保存成功后刷新当前页面,保证列表、详情和弹窗看到的是同一份最新数据;关联文件:F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):WechatAddDialogActions.afterSave() -> refreshWechatList() -> renderModule() -> 重新挂载 wechat 列表。
*/ */
async function refreshWechatList() { async function refreshWechatList() {
...@@ -477,24 +494,32 @@ function renderExternalRuntime(moduleKey) { ...@@ -477,24 +494,32 @@ function renderExternalRuntime(moduleKey) {
add: '新增资料', add: '新增资料',
reset: '重置', reset: '重置',
realName: '姓名', realName: '姓名',
account: '账号',
alias: '别名',
department: '部门', department: '部门',
gender: '性别',
phone: '手机号', phone: '手机号',
wxId: '微信 ID', owner: '实名人',
status: '状态', accountLocation: '账号位置',
owner: '负责人', ownerStatus: '实名状态',
updatedAt: '更新时间', phoneImage: '手机图片',
action: '操作', action: '操作',
view: '查看', view: '查看',
edit: '编辑', edit: '编辑',
replaceImage: '更换图片',
removeImage: '删除图片',
imageMissing: '暂无图片',
selected: '已选', selected: '已选',
items: '项', items: '项',
batchAction: '批量操作', batchAction: '批量操作',
total: '共', total: '共',
records: '条', records: '条',
pageSuffix: ' 条/页', pageSuffix: ' 条/页',
searchPlaceholder: '搜索姓名、微信 ID、手机号或部门', searchPlaceholder: '搜索姓名、账号、手机号、部门或实名人',
loadFailed: '加载企微列表失败,请确认本地后端已启动', loadFailed: '加载企微列表失败,请确认本地后端已启动',
openFailed: '打开企微详情失败,请稍后重试' openFailed: '打开企微详情失败,请稍后重试',
imageRemoved: '已删除手机图片',
imageRemoveFailed: '删除手机图片失败,请稍后重试'
}, },
wechatTablePageSizes: [10, 20, 50, 100], wechatTablePageSizes: [10, 20, 50, 100],
mountState: moduleMountState.wechat, mountState: moduleMountState.wechat,
...@@ -502,6 +527,7 @@ function renderExternalRuntime(moduleKey) { ...@@ -502,6 +527,7 @@ function renderExternalRuntime(moduleKey) {
loadWechatRows, loadWechatRows,
openAddDialog: openWechatAddDialog, openAddDialog: openWechatAddDialog,
openDrawer: openWechatDrawer, openDrawer: openWechatDrawer,
removeRecordImage: removeWechatRecordImage,
getVisiblePageNumbers, getVisiblePageNumbers,
getWechatStatusDotClass, getWechatStatusDotClass,
notifyError: showError, notifyError: showError,
...@@ -696,3 +722,7 @@ document.addEventListener('DOMContentLoaded', initPage); ...@@ -696,3 +722,7 @@ document.addEventListener('DOMContentLoaded', initPage);
...@@ -12,33 +12,45 @@ ...@@ -12,33 +12,45 @@
const ownerStatusRaw = source.ownerStatus ?? source.real_name_owner_status ?? source.realNameOwnerStatus; const ownerStatusRaw = source.ownerStatus ?? source.real_name_owner_status ?? source.realNameOwnerStatus;
const status = ownerStatusRaw === 'abnormal' || ownerStatusRaw === '异常' || ownerStatusRaw === 0 ? '异常' : '正常'; const status = ownerStatusRaw === 'abnormal' || ownerStatusRaw === '异常' || ownerStatusRaw === 0 ? '异常' : '正常';
const remark = source.remark || source.project || '暂无备注'; const remark = source.remark || source.project || '暂无备注';
const accountLocation = source.account_location || source.accountLocation || source['账号位置'] || '';
const phoneImage = source.phone_image || source.phoneImage || source['手机图片'] || '';
const departmentName = departmentMap[source.department] || source.department || source.department_name || source.departmentName || '';
const realName = source.realName || source.real_name || '';
const aliasName = source.alias_name || source.alias || source.aliasName || '';
const account = source.account || source.wxId || '';
const phoneNumber = source.phone_number || source.phone || source.phoneNumber || '';
const ownerName = source.real_name_owner || source.owner || source.realNameOwner || '';
return { return {
...source, ...source,
realName: source.realName || source.real_name || '', realName,
real_name: source.real_name || source.realName || '', real_name: realName,
alias: source.alias || source.alias_name || source.aliasName || '', alias: aliasName,
alias_name: source.alias_name || source.alias || source.aliasName || '', alias_name: aliasName,
account: source.account || source.wxId || '', account,
department: departmentMap[source.department] || source.department || source.department_name || source.departmentName || '未分配', department: departmentName || '未分配',
department_name: source.department_name || source.department || source.departmentName || '', department_name: departmentName,
phone: source.phone || source.phone_number || source.phoneNumber || '', phone: phoneNumber,
phone_number: source.phone_number || source.phone || source.phoneNumber || '', phone_number: phoneNumber,
owner: source.owner || source.real_name_owner || source.realNameOwner || '', owner: ownerName,
real_name_owner: source.real_name_owner || source.owner || source.realNameOwner || '', real_name_owner: ownerName,
ownerStatus: status, ownerStatus: status,
real_name_owner_status: status === '正常' ? 1 : 0, real_name_owner_status: status === '正常' ? 1 : 0,
status, status,
wxId: source.wxId || source.account || '', wxId: source.wxId || account,
createdAt: source.createdAt || source.created_at || '', createdAt: source.createdAt || source.created_at || '',
created_at: source.created_at || source.createdAt || '', created_at: source.created_at || source.createdAt || '',
updatedAt: source.updatedAt || source.updated_at || '', updatedAt: source.updatedAt || source.updated_at || '',
updated_at: source.updated_at || source.updatedAt || '', updated_at: source.updated_at || source.updatedAt || '',
remark, remark,
project: source.project || remark, project: source.project || remark,
gender: source.gender || '男', gender: source.gender || '',
account_location: source.account_location || source.accountLocation || source['账号位置'] || '', account_location: accountLocation,
phone_image: source.phone_image || source.phoneImage || source['手机图片'] || '' accountLocation,
'账号位置': accountLocation,
phone_image: phoneImage,
phoneImage,
'手机图片': phoneImage
}; };
} }
......
(function attachWechatApiClient() { (function attachWechatApiClient() {
// 当前企微资料接口对应测试数据库;后续切换到生产数据库(正式业务库)时,需要同步改成生产环境对应的后端配置。
const BASE_URL = 'http://localhost:8080/api/wechat-records'; const BASE_URL = 'http://localhost:8080/api/wechat-records';
/** /**
...@@ -55,3 +56,4 @@ ...@@ -55,3 +56,4 @@
saveWechatRecord saveWechatRecord
}; };
})(); })();
(function attachWechatListRuntime() { (function attachWechatListRuntime() {
const WECHAT_TABLE_COLUMN_WIDTHS = [ const WECHAT_TABLE_COLUMN_WIDTHS = [
'48px', '48px',
'calc((100% - 48px) * 0.14)', '140px',
'calc((100% - 48px) * 0.12)', '140px',
'calc((100% - 48px) * 0.14)', '140px',
'calc((100% - 48px) * 0.16)', '140px',
'calc((100% - 48px) * 0.10)', '92px',
'calc((100% - 48px) * 0.12)', '132px',
'calc((100% - 48px) * 0.12)', '132px',
'calc((100% - 48px) * 0.10)' '180px',
'120px',
'156px',
'216px'
]; ];
const WECHAT_TABLE_MIN_WIDTH = '1636px';
const WechatListTable = { const WechatListTable = {
name: 'WechatListTable', name: 'WechatListTable',
...@@ -21,6 +25,7 @@ ...@@ -21,6 +25,7 @@
loadWechatRows: { type: Function, required: true }, loadWechatRows: { type: Function, required: true },
openAddDialog: { type: Function, required: true }, openAddDialog: { type: Function, required: true },
openDrawer: { type: Function, required: true }, openDrawer: { type: Function, required: true },
removeRecordImage: { type: Function, required: true },
getVisiblePageNumbers: { type: Function, required: true }, getVisiblePageNumbers: { type: Function, required: true },
getWechatStatusDotClass: { type: Function, required: true }, getWechatStatusDotClass: { type: Function, required: true },
notifyError: { type: Function, required: true } notifyError: { type: Function, required: true }
...@@ -43,6 +48,23 @@ ...@@ -43,6 +48,23 @@
const loading = ref(false); const loading = ref(false);
/** /**
* 代码作用(白话):把列表里可能为空的字段统一转换成稳定显示文案,避免扩列后出现大片空白;关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js;关联逻辑(调用链/消息链/数据流):record 字段 -> displayValue() -> 列表单元格文本。
*/
function displayValue(value) {
if (value === null || value === undefined || value === '') {
return '-';
}
return String(value);
}
/**
* 代码作用(白话):从一条企微记录里读出手机图片链接,给缩略图、放大预览和删除动作共用;关联文件:F:/Project/xyw_console/src/modules/shared/record-adapters.js;关联逻辑(调用链/消息链/数据流):record -> getPhoneImageUrl() -> 图片单元格/删除图片动作。
*/
function getPhoneImageUrl(record) {
return record.phone_image || record.phoneImage || record['手机图片'] || '';
}
/**
* 代码作用(白话):把分页和勾选状态同步回外层挂载状态,保证路由切走再切回来时还能恢复现场;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):列表交互 -> syncMountState() -> mountState -> 下次重新挂载复用。 * 代码作用(白话):把分页和勾选状态同步回外层挂载状态,保证路由切走再切回来时还能恢复现场;关联文件:F:/Project/xyw_console/app-v2.js;关联逻辑(调用链/消息链/数据流):列表交互 -> syncMountState() -> mountState -> 下次重新挂载复用。
*/ */
function syncMountState() { function syncMountState() {
...@@ -100,9 +122,12 @@ ...@@ -100,9 +122,12 @@
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
result = result.filter((record) => result = result.filter((record) =>
(record.realName && record.realName.toLowerCase().includes(query)) || (record.realName && record.realName.toLowerCase().includes(query)) ||
(record.account && record.account.toLowerCase().includes(query)) ||
(record.alias_name && record.alias_name.toLowerCase().includes(query)) ||
(record.phone && record.phone.toLowerCase().includes(query)) || (record.phone && record.phone.toLowerCase().includes(query)) ||
(record.wxId && record.wxId.toLowerCase().includes(query)) || (record.wxId && record.wxId.toLowerCase().includes(query)) ||
(record.department && record.department.toLowerCase().includes(query)) (record.department && record.department.toLowerCase().includes(query)) ||
(record.real_name_owner && record.real_name_owner.toLowerCase().includes(query))
); );
} }
return result; return result;
...@@ -301,11 +326,36 @@ ...@@ -301,11 +326,36 @@
} }
} }
/**
* 代码作用(白话):从列表里直接打开当前记录的编辑弹窗,让用户在原有保存流程里更换图片链接;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/wechat/wechat-add-dialog.js;关联逻辑(调用链/消息链/数据流):更换图片按钮 -> handleReplaceImage() -> openDrawer('edit') -> 编辑弹窗保存。
*/
async function handleReplaceImage(recordId) {
await handleRowAction('edit', recordId);
}
/**
* 代码作用(白话):从列表里删除手机图片链接并刷新全量记录,保证列表、详情和编辑弹窗看到同一份最新状态;关联文件:F:/Project/xyw_console/app-v2.js、F:/Project/xyw_console/src/modules/shared/wechat-api-client.js;关联逻辑(调用链/消息链/数据流):删除图片按钮 -> handleRemoveImage() -> removeRecordImage() -> loadAllRecords。
*/
async function handleRemoveImage(recordId) {
focusRow(recordId);
loading.value = true;
try {
await props.removeRecordImage(recordId);
await loadAllRecords();
window.ElementPlus?.ElMessage?.success?.(props.wechatText.imageRemoved);
} catch (error) {
props.notifyError(error.message || props.wechatText.imageRemoveFailed);
} finally {
loading.value = false;
}
}
window.Vue.onMounted(loadAllRecords); window.Vue.onMounted(loadAllRecords);
syncMountState(); syncMountState();
return { return {
WECHAT_TABLE_COLUMN_WIDTHS, WECHAT_TABLE_COLUMN_WIDTHS,
WECHAT_TABLE_MIN_WIDTH,
currentPage, currentPage,
pageSize, pageSize,
visibleRows, visibleRows,
...@@ -326,6 +376,10 @@ ...@@ -326,6 +376,10 @@
resetTableState, resetTableState,
handleAdd, handleAdd,
handleRowAction, handleRowAction,
handleReplaceImage,
handleRemoveImage,
displayValue,
getPhoneImageUrl,
filterDate, filterDate,
searchQuery, searchQuery,
filterStatus, filterStatus,
...@@ -383,7 +437,7 @@ ...@@ -383,7 +437,7 @@
</div> </div>
</div> </div>
<div class="table-wrap table-wrap-refined" v-loading="loading"> <div class="table-wrap table-wrap-refined" v-loading="loading">
<table class="xtable domain-table domain-table-refined"> <table class="xtable domain-table domain-table-refined wechat-list-table" :style="{ minWidth: WECHAT_TABLE_MIN_WIDTH }">
<colgroup> <colgroup>
<col v-for="width in WECHAT_TABLE_COLUMN_WIDTHS" :key="width" :style="{ width }" /> <col v-for="width in WECHAT_TABLE_COLUMN_WIDTHS" :key="width" :style="{ width }" />
</colgroup> </colgroup>
...@@ -391,12 +445,15 @@ ...@@ -391,12 +445,15 @@
<tr> <tr>
<th class="th-selector"><input type="checkbox" :checked="allVisibleChecked" @change="toggleAllVisible($event.target.checked)" /></th> <th class="th-selector"><input type="checkbox" :checked="allVisibleChecked" @change="toggleAllVisible($event.target.checked)" /></th>
<th>{{ wechatText.realName }}</th> <th>{{ wechatText.realName }}</th>
<th>{{ wechatText.account }}</th>
<th>{{ wechatText.alias }}</th>
<th>{{ wechatText.department }}</th> <th>{{ wechatText.department }}</th>
<th>{{ wechatText.gender }}</th>
<th>{{ wechatText.phone }}</th> <th>{{ wechatText.phone }}</th>
<th>{{ wechatText.wxId }}</th>
<th>{{ wechatText.status }}</th>
<th>{{ wechatText.owner }}</th> <th>{{ wechatText.owner }}</th>
<th>{{ wechatText.updatedAt }}</th> <th>{{ wechatText.accountLocation }}</th>
<th>{{ wechatText.ownerStatus }}</th>
<th>{{ wechatText.phoneImage }}</th>
<th class="th-action">{{ wechatText.action }}</th> <th class="th-action">{{ wechatText.action }}</th>
</tr> </tr>
</thead> </thead>
...@@ -404,34 +461,50 @@ ...@@ -404,34 +461,50 @@
<tr v-for="row in visibleRows" :key="row.id" :class="{ selected: isRowSelected(row.id) }" @click="focusRow(row.id)"> <tr v-for="row in visibleRows" :key="row.id" :class="{ selected: isRowSelected(row.id) }" @click="focusRow(row.id)">
<td class="td-selector"><input type="checkbox" :checked="isRowChecked(row.id)" @click.stop @change="toggleRowChecked(row.id, $event.target.checked)" /></td> <td class="td-selector"><input type="checkbox" :checked="isRowChecked(row.id)" @click.stop @change="toggleRowChecked(row.id, $event.target.checked)" /></td>
<td class="td-domain"> <td class="td-domain">
<a href="#" class="domain-link" @click.prevent="focusRow(row.id)">{{ row.realName }}</a> <a href="#" class="domain-link" @click.prevent="focusRow(row.id)">{{ displayValue(row.realName || row.real_name) }}</a>
<div class="domain-inline-meta">{{ row.alias || row.alias_name || '-' }}</div>
</td> </td>
<td>{{ displayValue(row.account || row.wxId) }}</td>
<td>{{ displayValue(row.alias_name || row.alias) }}</td>
<td> <td>
<div class="date-stack"> <div class="date-stack">
<span>{{ row.department }}</span> <span>{{ displayValue(row.department || row.department_name) }}</span>
<div class="cell-sub">{{ row.project || row.remark || '暂无备注' }}</div> <div class="cell-sub">{{ displayValue(row.project || row.remark || '暂无备注') }}</div>
</div> </div>
</td> </td>
<td>{{ row.phone }}</td> <td>{{ displayValue(row.gender) }}</td>
<td>{{ row.wxId }}</td> <td>{{ displayValue(row.phone || row.phone_number) }}</td>
<td>{{ displayValue(row.owner || row.real_name_owner) }}</td>
<td class="wechat-location-cell">{{ displayValue(row.account_location || row.accountLocation || row['账号位置']) }}</td>
<td> <td>
<span class="status-chip-wrap"> <span class="status-chip-wrap">
<span class="status-dot" :class="getStatusClass(row.status)"></span> <span class="status-dot" :class="getStatusClass(row.ownerStatus || row.status)"></span>
<span>{{ row.status }}</span> <span>{{ displayValue(row.ownerStatus || row.status) }}</span>
</span> </span>
</td> </td>
<td>{{ row.owner }}</td> <td>
<td>{{ row.updatedAt || '-' }}</td> <div v-if="getPhoneImageUrl(row)" class="wechat-image-cell">
<el-image
class="wechat-image-thumb"
:src="getPhoneImageUrl(row)"
:preview-src-list="[getPhoneImageUrl(row)]"
fit="cover"
preview-teleported
@click.stop
></el-image>
</div>
<span v-else class="wechat-image-empty">{{ wechatText.imageMissing }}</span>
</td>
<td class="td-action"> <td class="td-action">
<div class="action-cluster"> <div class="action-cluster wechat-action-cluster">
<button class="action-pill action-pill-primary" type="button" @click.stop="handleRowAction('view', row.id)">{{ wechatText.view }}</button> <button class="action-pill action-pill-primary" type="button" @click.stop="handleRowAction('view', row.id)">{{ wechatText.view }}</button>
<button class="action-pill" type="button" @click.stop="handleRowAction('edit', row.id)">{{ wechatText.edit }}</button> <button class="action-pill" type="button" @click.stop="handleRowAction('edit', row.id)">{{ wechatText.edit }}</button>
<button class="action-pill" type="button" @click.stop="handleReplaceImage(row.id)">{{ wechatText.replaceImage }}</button>
<button class="action-pill action-pill-danger" type="button" :disabled="!getPhoneImageUrl(row)" @click.stop="handleRemoveImage(row.id)">{{ wechatText.removeImage }}</button>
</div> </div>
</td> </td>
</tr> </tr>
<tr v-if="!visibleRows.length"> <tr v-if="!visibleRows.length">
<td colspan="9" style="padding: 24px; text-align: center; color: #9ca3af;">暂无企微资料</td> <td colspan="12" style="padding: 24px; text-align: center; color: #9ca3af;">暂无企微资料</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
......
/* Import original styles first */ /* Import original styles first */
@import url('styles.css'); @import url('styles.css');
/* Overrides for right side replication */ /* Overrides for right side replication */
...@@ -306,16 +306,16 @@ ...@@ -306,16 +306,16 @@
} }
.fselect:after { .fselect:after {
content: "▾"; content: "鈻?;
color: #9ca3af; color: #9ca3af;
} }
/* Custom icon for el-select to match batch action button icon */ /* Custom icon for el-select to match batch action button icon 鈻?*/
.custom-fselect .el-input__suffix-inner > .el-icon > svg { .custom-fselect .el-input__suffix-inner > .el-icon > svg {
display: none !important; display: none !important;
} }
.custom-fselect .el-input__suffix-inner > .el-icon::before { .custom-fselect .el-input__suffix-inner > .el-icon::before {
content: "▾"; content: "鈻?;
color: #9ca3af; color: #9ca3af;
font-size: 14px; font-size: 14px;
display: flex; display: flex;
...@@ -1003,7 +1003,143 @@ ...@@ -1003,7 +1003,143 @@
fill: currentColor !important; fill: currentColor !important;
} }
/* 消除原来的旧外边距干扰 */ /* 娑堥櫎鍘熸潵鐨勬棫澶栬竟璺濆共鎵?*/
.domain-mode .action-dots { .domain-mode .action-dots {
margin: 0; margin: 0;
} }
\ No newline at end of file .wechat-list-table {
table-layout: fixed;
}
.table-wrap-refined {
overflow-x: auto;
overflow-y: hidden;
}
.wechat-location-cell {
white-space: normal;
word-break: break-word;
}
.wechat-image-cell {
display: flex;
align-items: center;
}
.wechat-image-thumb {
width: 84px;
height: 84px;
border-radius: 10px;
overflow: hidden;
border: 1px solid #dbe4f0;
background: #f8fafc;
}
.wechat-image-thumb .el-image__inner {
width: 100%;
height: 100%;
object-fit: cover;
}
.wechat-image-empty {
color: #94a3b8;
font-size: 12px;
}
.wechat-action-cluster {
flex-wrap: wrap;
justify-content: flex-start;
gap: 6px;
}
.action-pill-danger {
color: #dc2626;
}
.action-pill-danger:hover:not(:disabled) {
background: #fef2f2;
color: #b91c1c;
}
.action-pill:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.table-area-refined {
flex: 1 1 auto;
min-height: 0;
height: 100%;
}
.table-toolbar-refined {
flex: 0 0 auto;
}
.table-wrap-refined {
flex: 1 1 auto;
min-height: 0;
height: 0;
overflow-x: auto;
overflow-y: auto;
}
#wechatListRoot,
#phoneListRoot {
min-height: 0;
}
#vue-router-app,
.legacy-wrapper {
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
height: 100%;
}
#pageHeader {
flex: 0 0 auto;
}
#pageBody {
display: flex !important;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
#wechatListRoot,
#phoneListRoot,
#overviewListRoot {
flex: 1 1 auto;
min-height: 0;
height: 100%;
}
#wechatListRoot[style*='display: block'],
#phoneListRoot[style*='display: block'],
#overviewListRoot[style*='display: block'] {
display: flex !important;
flex-direction: column;
}
.table-area-refined {
flex: 1 1 auto !important;
min-height: 0 !important;
height: 100% !important;
}
.table-wrap-refined {
flex: 1 1 auto !important;
min-height: 0 !important;
height: 0 !important;
overflow-x: auto !important;
overflow-y: auto !important;
}
:root { :root {
--page-bg: #f5f7fb; --page-bg: #f5f7fb;
--surface: #ffffff; --surface: #ffffff;
--surface-soft: #fafcff; --surface-soft: #fafcff;
...@@ -2922,14 +2922,38 @@ button { ...@@ -2922,14 +2922,38 @@ button {
/* Professional admin dashboard visual override */ /* Professional admin dashboard visual override */
.shell-main { background: linear-gradient(180deg, #f7f9fc 0%, #eff4f9 100%); } .shell-main { background: linear-gradient(180deg, #f7f9fc 0%, #eff4f9 100%); }
.shell-header { padding: 28px 28px 20px; border-bottom: 1px solid rgba(15, 23, 42, 0.06); background: rgba(255, 255, 255, 0.92); } .legacy-wrapper {
.workspace-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 24px; } display: flex;
.heading-copy { display: grid; gap: 12px; max-width: 760px; } flex: 1 1 auto;
flex-direction: column;
min-height: 0;
height: 100%;
}
#pageHeader {
flex: 0 0 auto;
}
#pageBody {
flex: 1 1 auto;
min-height: 0;
}
#pageBody > #wechatListRoot,
#pageBody > #phoneListRoot,
#pageBody > #overviewListRoot {
height: 100%;
min-height: 0;
}
.shell-header { padding: 20px 28px 16px; border-bottom: 1px solid rgba(15, 23, 42, 0.06); background: rgba(255, 255, 255, 0.92); }
.workspace-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; flex-wrap: wrap; min-height: 88px; }
.heading-copy { display: grid; gap: 8px; flex: 1 1 420px; min-width: 0; max-width: 760px; align-content: start; }
.heading-caption-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } .heading-caption-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.heading-caption { margin: 0; font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #6b7a90; } .heading-caption { margin: 0; font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; color: #6b7a90; }
.heading-badge { display: inline-flex; align-items: center; height: 28px; padding: 0 12px; border-radius: 999px; background: #eef4ff; color: #2554cf; font-size: 12px; font-weight: 700; } .heading-badge { display: inline-flex; align-items: center; height: 28px; padding: 0 12px; border-radius: 999px; background: #eef4ff; color: #2554cf; font-size: 12px; font-weight: 700; }
.page-title { margin: 0; font-size: 32px; line-height: 1.1; font-weight: 800; color: #0f172a; } .page-title { margin: 0; font-size: 30px; line-height: 1.08; font-weight: 800; color: #0f172a; }
.page-summary { margin: 0; color: #667085; font-size: 14px; line-height: 1.7; } .page-summary { margin: 0; color: #667085; font-size: 14px; line-height: 1.5; }
.admin-summary-pills { display: flex; flex-wrap: wrap; gap: 10px; } .admin-summary-pills { display: flex; flex-wrap: wrap; gap: 10px; }
.status-pill { display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 12px; border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.18); background: #fff; box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04); font-size: 13px; color: #475467; } .status-pill { display: inline-flex; align-items: center; gap: 8px; height: 34px; padding: 0 12px; border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.18); background: #fff; box-shadow: 0 10px 24px rgba(15, 23, 42, 0.04); font-size: 13px; color: #475467; }
.status-pill strong { color: #0f172a; } .status-pill strong { color: #0f172a; }
...@@ -2937,7 +2961,7 @@ button { ...@@ -2937,7 +2961,7 @@ button {
.status-pill.green { background: #edfdf3; color: #177245; } .status-pill.green { background: #edfdf3; color: #177245; }
.status-pill.orange { background: #fff6ea; color: #c76a00; } .status-pill.orange { background: #fff6ea; color: #c76a00; }
.status-pill.red { background: #fff1f3; color: #d92d20; } .status-pill.red { background: #fff1f3; color: #d92d20; }
.header-tools { display: flex; flex: 0 0 460px; flex-direction: column; gap: 14px; } .header-tools { display: flex; flex: 1 1 360px; min-width: 280px; max-width: 460px; margin-left: auto; flex-direction: column; gap: 14px; }
.header-search { display: flex; align-items: center; gap: 10px; width: 100%; min-width: 0; height: 52px; padding: 0 16px; border: 1px solid rgba(148, 163, 184, 0.24); border-radius: 16px; background: rgba(255, 255, 255, 0.98); box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05); } .header-search { display: flex; align-items: center; gap: 10px; width: 100%; min-width: 0; height: 52px; padding: 0 16px; border: 1px solid rgba(148, 163, 184, 0.24); border-radius: 16px; background: rgba(255, 255, 255, 0.98); box-shadow: 0 12px 30px rgba(15, 23, 42, 0.05); }
.header-search::before { content: none; } .header-search::before { content: none; }
.header-search-icon { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: #eef4ff; color: #2554cf; font-size: 14px; } .header-search-icon { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border-radius: 50%; background: #eef4ff; color: #2554cf; font-size: 14px; }
...@@ -2946,7 +2970,7 @@ button { ...@@ -2946,7 +2970,7 @@ button {
.header-button, .header-button-primary, .toolbar-button, .footer-button, .footer-button-primary, .preview-link { height: 42px; padding: 0 16px; border-radius: 14px; font-weight: 700; } .header-button, .header-button-primary, .toolbar-button, .footer-button, .footer-button-primary, .preview-link { height: 42px; padding: 0 16px; border-radius: 14px; font-weight: 700; }
.header-button, .toolbar-button, .footer-button, .preview-link { border: 1px solid rgba(148, 163, 184, 0.2); background: #fff; box-shadow: 0 8px 22px rgba(15, 23, 42, 0.05); } .header-button, .toolbar-button, .footer-button, .preview-link { border: 1px solid rgba(148, 163, 184, 0.2); background: #fff; box-shadow: 0 8px 22px rgba(15, 23, 42, 0.05); }
.header-button-primary, .footer-button-primary { border: 1px solid #2563eb; background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); color: #fff; box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24); } .header-button-primary, .footer-button-primary { border: 1px solid #2563eb; background: linear-gradient(180deg, #3b82f6 0%, #2563eb 100%); color: #fff; box-shadow: 0 14px 28px rgba(37, 99, 235, 0.24); }
.shell-body { gap: 20px; padding: 20px 28px 28px; background: transparent; } .shell-body { display: flex; flex: 1 1 auto; flex-direction: column; gap: 16px; min-height: 0; overflow: hidden; padding: 16px 28px 20px; background: transparent; }
.stats-grid { gap: 16px; } .stats-grid { gap: 16px; }
.metric-card-premium { position: relative; display: flex; flex-direction: column; gap: 16px; min-height: 190px; padding: 20px 22px; border-radius: 22px; border: 1px solid rgba(226, 232, 240, 0.92); box-shadow: 0 18px 42px rgba(15, 23, 42, 0.06); overflow: hidden; } .metric-card-premium { position: relative; display: flex; flex-direction: column; gap: 16px; min-height: 190px; padding: 20px 22px; border-radius: 22px; border: 1px solid rgba(226, 232, 240, 0.92); box-shadow: 0 18px 42px rgba(15, 23, 42, 0.06); overflow: hidden; }
.metric-card-premium[data-tone="blue"] { background: linear-gradient(180deg, #eef5ff 0%, #ffffff 100%); } .metric-card-premium[data-tone="blue"] { background: linear-gradient(180deg, #eef5ff 0%, #ffffff 100%); }
...@@ -3034,7 +3058,7 @@ button { ...@@ -3034,7 +3058,7 @@ button {
.timeline-time { color: #98a2b3; font-size: 12px; } .timeline-time { color: #98a2b3; font-size: 12px; }
.detail-footer { display: flex; gap: 10px; } .detail-footer { display: flex; gap: 10px; }
.detail-footer > * { flex: 1; } .detail-footer > * { flex: 1; }
@media (max-width: 1360px) { .workspace-heading, .table-head-v3, .filter-toolbar, .preview-head-v3 { flex-direction: column; align-items: stretch; } .header-tools { flex: 1 1 auto; } .table-selection-chip { min-width: 0; } } @media (max-width: 1360px) { .workspace-heading, .table-head-v3, .filter-toolbar, .preview-head-v3 { flex-direction: column; align-items: stretch; } .header-tools { flex: 1 1 auto; min-width: 0; max-width: none; margin-left: 0; } .table-selection-chip { min-width: 0; } }
@media (max-width: 1180px) { .shell-header, .shell-body { padding-left: 18px; padding-right: 18px; } .page-title { font-size: 28px; } .preview-row-v3 { grid-template-columns: 32px minmax(0, 1fr); } .preview-link { grid-column: 2; justify-self: start; } } @media (max-width: 1180px) { .shell-header, .shell-body { padding-left: 18px; padding-right: 18px; } .page-title { font-size: 28px; } .preview-row-v3 { grid-template-columns: 32px minmax(0, 1fr); } .preview-link { grid-column: 2; justify-self: start; } }
@media (max-width: 840px) { .shell-header, .shell-body { padding-left: 14px; padding-right: 14px; } .page-title { font-size: 24px; } .detail-grid { grid-template-columns: 1fr; } } @media (max-width: 840px) { .shell-header, .shell-body { padding-left: 14px; padding-right: 14px; } .page-title { font-size: 24px; } .detail-grid { grid-template-columns: 1fr; } }
@media (min-width: 1280px) { @media (min-width: 1280px) {
...@@ -3047,3 +3071,5 @@ button { ...@@ -3047,3 +3071,5 @@ button {
.workspace:not(.sidebar-collapsed) .nav-link, .workspace:not(.sidebar-collapsed) .nav-link,
.workspace:not(.sidebar-collapsed) .nav-toggle-button { justify-content: flex-start; gap: 12px; padding: 0 14px; } .workspace:not(.sidebar-collapsed) .nav-toggle-button { justify-content: flex-start; gap: 12px; padding: 0 14px; }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment