This commit is contained in:
van
2026-06-03 12:06:53 +08:00
parent de32ec4a93
commit 75f9a61654
3 changed files with 193 additions and 36 deletions

View File

@@ -27,6 +27,14 @@ export function listQuickRecordShopOptions() {
}) })
} }
/** 一键迁移:拆分 model_number 末尾店铺前缀到 model_shop */
export function migrateModelShopSplit() {
return request({
url: '/system/jdorder/migrateModelShop',
method: 'post'
})
}
// JD订单详情 // JD订单详情
export function getJDOrder(id) { export function getJDOrder(id) {
return request({ return request({

View File

@@ -356,25 +356,33 @@ export default {
if (!base) return '' if (!base) return ''
return prefix ? base + prefix : base return prefix ? base + prefix : base
}, },
fullModelFromOption(o) {
if (!o) return ''
const base = String(o.modelNumber || '').trim()
const shop = String(o.modelShop || '').trim()
if (!base) return shop
if (!shop) return base
if (base.endsWith(shop)) return base
return base + shop
},
queryModels(queryString, cb) { queryModels(queryString, cb) {
const q = (queryString || '').trim().toLowerCase() const q = (queryString || '').trim().toLowerCase()
const rows = this.modelOptions const rows = this.modelOptions
.filter(o => { .filter(o => {
const m = String(o.modelNumber || '').trim() const full = this.fullModelFromOption(o)
if (!m) return false if (!full) return false
if (!q) return true if (!q) return true
const parsed = this.parseModelWithShop(m) const parsed = this.parseModelWithShop(full)
return m.toLowerCase().includes(q) || parsed.base.toLowerCase().includes(q) return full.toLowerCase().includes(q) || parsed.base.toLowerCase().includes(q)
}) })
.slice(0, 100) .slice(0, 100)
cb( cb(
rows.map(o => { rows.map(o => {
const full = String(o.modelNumber).trim() const full = this.fullModelFromOption(o)
const parsed = this.parseModelWithShop(full) const parsed = this.parseModelWithShop(full)
const displayBase = parsed.base || full
return { return {
value: full, value: full,
label: this.modelOptionLabel({ ...o, modelNumber: displayBase }) label: this.modelOptionLabel({ ...o, modelNumber: parsed.base || full })
} }
}) })
) )
@@ -383,8 +391,13 @@ export default {
onModelSuggestionSelect(item) { onModelSuggestionSelect(item) {
if (!item || !item.value) return if (!item || !item.value) return
const key = String(item.value).trim() const key = String(item.value).trim()
const hit = this.modelOptions.find(o => this.fullModelFromOption(o) === key)
if (hit && hit.modelShop) {
this.form.model = String(hit.modelNumber || '').trim()
this.form.shopPrefix = String(hit.modelShop || '').trim()
} else {
this.applyModelWithShop(key) this.applyModelWithShop(key)
const hit = this.modelOptions.find(o => o && String(o.modelNumber || '').trim() === key) }
if (!hit) return if (!hit) return
const payRaw = hit.lastPaymentAmount const payRaw = hit.lastPaymentAmount
const rebateRaw = hit.lastRebateAmount const rebateRaw = hit.lastRebateAmount

View File

@@ -64,7 +64,12 @@
<el-input v-model="queryParams.distributionMark" placeholder="分销标记" clearable size="small" @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.distributionMark" placeholder="分销标记" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="型号"> <el-form-item label="型号">
<el-input v-model="queryParams.modelNumber" placeholder="型号" clearable size="small" @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.modelNumber" placeholder="型号(含店铺拼接也可搜)" clearable size="small" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="店铺">
<el-select v-model="queryParams.modelShop" placeholder="全部" clearable filterable size="small" style="width: 140px">
<el-option v-for="opt in shopOptions" :key="opt.prefix" :label="opt.label" :value="opt.prefix" />
</el-select>
</el-form-item> </el-form-item>
<el-form-item label="型号不含"> <el-form-item label="型号不含">
<el-input v-model="queryParams.modelNumberExclude" placeholder="多个用逗号或空格,如 130,645,90" clearable size="small" @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.modelNumberExclude" placeholder="多个用逗号或空格,如 130,645,90" clearable size="small" @keyup.enter.native="handleQuery" />
@@ -191,6 +196,7 @@
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-setting" @click="showAutoWriteConfig = true" title="配置H-TF订单自动写入腾讯文档">腾峰文档配置</el-button> <el-button class="jd-tb-muted" plain size="small" icon="el-icon-setting" @click="showAutoWriteConfig = true" title="配置H-TF订单自动写入腾讯文档">腾峰文档配置</el-button>
<el-button type="primary" plain size="small" icon="el-icon-monitor" @click="showPushMonitor = true" title="查看推送监控和历史记录">推送监控</el-button> <el-button type="primary" plain size="small" icon="el-icon-monitor" @click="showPushMonitor = true" title="查看推送监控和历史记录">推送监控</el-button>
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-user" @click="showTouserConfig = true" title="配置分销标识对应的企业微信接收人">接收人配置</el-button> <el-button class="jd-tb-muted" plain size="small" icon="el-icon-user" @click="showTouserConfig = true" title="配置分销标识对应的企业微信接收人">接收人配置</el-button>
<el-button v-if="!isMobile" class="jd-tb-muted" plain size="small" icon="el-icon-scissors" :loading="modelShopMigrateLoading" @click="handleMigrateModelShop" title="按已配置店铺选项,将型号末尾前缀拆到「型号店铺」字段">拆分型号店铺</el-button>
<el-button class="jd-tb-muted" plain size="small" icon="el-icon-check" @click="handleBatchMarkRebateReceived" :loading="batchMarkLoading" title="批量将赔付金额大于0的订单标记为后返到账仅执行一次">批量标记后返到账</el-button> <el-button class="jd-tb-muted" plain size="small" icon="el-icon-check" @click="handleBatchMarkRebateReceived" :loading="batchMarkLoading" title="批量将赔付金额大于0的订单标记为后返到账仅执行一次">批量标记后返到账</el-button>
<el-button v-if="!isMobile" type="primary" plain size="small" icon="el-icon-upload2" @click="rebateImportDialogVisible = true" title="可一次选多个 Excel提交后由后台依次导入详情见后返上传记录">导入后返表</el-button> <el-button v-if="!isMobile" type="primary" plain size="small" icon="el-icon-upload2" @click="rebateImportDialogVisible = true" title="可一次选多个 Excel提交后由后台依次导入详情见后返上传记录">导入后返表</el-button>
<el-button v-if="!isMobile" class="jd-tb-muted" plain size="small" icon="el-icon-folder-opened" @click="openRebateUploadRecordDialog" title="查看历史上传的后返表原件并可重新下载">后返上传记录</el-button> <el-button v-if="!isMobile" class="jd-tb-muted" plain size="small" icon="el-icon-folder-opened" @click="openRebateUploadRecordDialog" title="查看历史上传的后返表原件并可重新下载">后返上传记录</el-button>
@@ -353,7 +359,7 @@
>{{ row.distributionMark }}</span> >{{ row.distributionMark }}</span>
<span class="card-summary-sep"> · </span> <span class="card-summary-sep"> · </span>
</template> </template>
<span class="card-summary-meta">{{ row.modelNumber || '—' }} · {{ toYuan(row.paymentAmount) }} · {{ toYuan(row.rebateAmount) }}</span> <span class="card-summary-meta">{{ fullModelNumber(row) || '—' }} · {{ toYuan(row.paymentAmount) }} · {{ toYuan(row.rebateAmount) }}</span>
</span> </span>
</div> </div>
@@ -396,15 +402,26 @@
</div> </div>
<div class="field-row field-row--model" @click.stop> <div class="field-row field-row--model" @click.stop>
<span class="field-label">型号</span> <span class="field-label">型号</span>
<div class="field-value field-value--third-party-edit"> <div class="field-value field-value--third-party-edit jd-mobile-model-edit">
<el-input <el-input
v-model="row.modelNumber" v-model="row.modelNumber"
class="jd-mobile-model-input" class="jd-mobile-model-input"
size="small" size="small"
clearable clearable
placeholder="型号单行约20字失焦或保存" placeholder="型号本体"
@blur="onModelNumberBlur(row)" @blur="onModelNumberBlur(row)"
/> />
<el-select
v-model="row.modelShop"
class="jd-mobile-model-shop"
size="small"
clearable
filterable
placeholder="店铺"
@change="onModelShopChange(row)"
>
<el-option v-for="opt in shopOptions" :key="opt.prefix" :label="opt.label" :value="opt.prefix" />
</el-select>
<el-button type="primary" size="mini" plain @click="onModelNumberSave(row)">保存</el-button> <el-button type="primary" size="mini" plain @click="onModelNumberSave(row)">保存</el-button>
</div> </div>
</div> </div>
@@ -650,18 +667,29 @@
</el-table-column> </el-table-column>
<!-- 业务信息列型号单行撑满列宽 20 字以内不挤更长在输入框内横向滚动不换行以免行高乱跳 --> <!-- 业务信息列型号单行撑满列宽 20 字以内不挤更长在输入框内横向滚动不换行以免行高乱跳 -->
<el-table-column label="型号" prop="modelNumber" min-width="232" class-name="jd-col-model"> <el-table-column label="型号" prop="modelNumber" min-width="280" class-name="jd-col-model">
<template slot-scope="scope"> <template slot-scope="scope">
<div class="jd-cell-stretch jd-cell-stretch--model"> <div class="jd-cell-stretch jd-cell-stretch--model jd-model-edit-row">
<el-input <el-input
v-model="scope.row.modelNumber" v-model="scope.row.modelNumber"
class="jd-order-input-model" class="jd-order-input-model"
size="mini" size="mini"
clearable clearable
placeholder="型号约20字" placeholder="型号本体"
@blur="onModelNumberBlur(scope.row)" @blur="onModelFieldBlur(scope.row)"
@keyup.enter.native="onModelNumberBlur(scope.row)" @keyup.enter.native="onModelFieldBlur(scope.row)"
/> />
<el-select
v-model="scope.row.modelShop"
class="jd-order-model-shop"
size="mini"
clearable
filterable
placeholder="店铺"
@change="onModelShopChange(scope.row)"
>
<el-option v-for="opt in shopOptions" :key="opt.prefix" :label="opt.label" :value="opt.prefix" />
</el-select>
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
@@ -1314,7 +1342,7 @@
</template> </template>
<script> <script>
import { listJDOrders, getJDOrder, updateJDOrder, normalizeJDOrderPutPayload, delJDOrder, fetchLogisticsManually, batchMarkRebateReceived, generateExcelText, importGroupRebateExcelBatch, listGroupRebateExcelUploads, deleteGroupRebateUpload, recalcProfitBatch, syncAutoProfitBatch } from '@/api/system/jdorder' import { listJDOrders, getJDOrder, updateJDOrder, normalizeJDOrderPutPayload, delJDOrder, fetchLogisticsManually, batchMarkRebateReceived, generateExcelText, importGroupRebateExcelBatch, listGroupRebateExcelUploads, deleteGroupRebateUpload, recalcProfitBatch, syncAutoProfitBatch, listQuickRecordShopOptions, migrateModelShopSplit } from '@/api/system/jdorder'
import { fillLogisticsByOrderNo, getTokenStatus, getTencentDocAuthUrl, testUserInfo, getAutoWriteConfig, reverseSyncThirdPartyOrderNo } from '@/api/jarvis/tendoc' import { fillLogisticsByOrderNo, getTokenStatus, getTencentDocAuthUrl, testUserInfo, getAutoWriteConfig, reverseSyncThirdPartyOrderNo } from '@/api/jarvis/tendoc'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
import ListLayout from '@/components/ListLayout' import ListLayout from '@/components/ListLayout'
@@ -1343,6 +1371,10 @@ export default {
_distMarkBaseline: {}, _distMarkBaseline: {},
/** 列表加载时的型号快照,失焦保存时与当前值比较避免重复请求 */ /** 列表加载时的型号快照,失焦保存时与当前值比较避免重复请求 */
_modelNumberBaseline: {}, _modelNumberBaseline: {},
/** 列表加载时的型号店铺快照 */
_modelShopBaseline: {},
shopOptions: [],
modelShopMigrateLoading: false,
loading: false, loading: false,
list: [], list: [],
total: 0, total: 0,
@@ -1354,6 +1386,7 @@ export default {
orderSearch: undefined, orderSearch: undefined,
distributionMark: undefined, distributionMark: undefined,
modelNumber: undefined, modelNumber: undefined,
modelShop: undefined,
modelNumberExclude: undefined, modelNumberExclude: undefined,
link: undefined, link: undefined,
buyer: undefined, buyer: undefined,
@@ -1554,8 +1587,7 @@ export default {
// 设置默认日期为今天 // 设置默认日期为今天
this.setDefaultDateRange() this.setDefaultDateRange()
this.getListWithFallback() this.getListWithFallback()
this.loadShopOptions()
// 监听腾讯文档授权回调消息
this.handleOAuthMessage = (event) => { this.handleOAuthMessage = (event) => {
if (event.data && event.data.type === 'tendoc_oauth_callback') { if (event.data && event.data.type === 'tendoc_oauth_callback') {
if (event.data.success) { if (event.data.success) {
@@ -1754,14 +1786,17 @@ export default {
normalizeOrderListItem(item) { normalizeOrderListItem(item) {
const dist = this.normalizeDistributionMarkInput(item.distributionMark) const dist = this.normalizeDistributionMarkInput(item.distributionMark)
const modelNum = this.normalizeModelNumberInput(item.modelNumber) const modelNum = this.normalizeModelNumberInput(item.modelNumber)
const modelShop = this.normalizeModelShopInput(item.modelShop)
if (item.id != null) { if (item.id != null) {
this.$set(this._distMarkBaseline, item.id, dist) this.$set(this._distMarkBaseline, item.id, dist)
this.$set(this._modelNumberBaseline, item.id, modelNum) this.$set(this._modelNumberBaseline, item.id, modelNum)
this.$set(this._modelShopBaseline, item.id, modelShop)
} }
return { return {
...item, ...item,
distributionMark: dist, distributionMark: dist,
modelNumber: modelNum, modelNumber: modelNum,
modelShop: modelShop,
isRefunded: item.isRefunded != null ? item.isRefunded : 0, isRefunded: item.isRefunded != null ? item.isRefunded : 0,
isRefundReceived: item.isRefundReceived != null ? item.isRefundReceived : 0, isRefundReceived: item.isRefundReceived != null ? item.isRefundReceived : 0,
isRebateReceived: item.isRebateReceived != null ? item.isRebateReceived : 0, isRebateReceived: item.isRebateReceived != null ? item.isRebateReceived : 0,
@@ -1795,6 +1830,7 @@ export default {
this._distMarkBaseline = {} this._distMarkBaseline = {}
this._modelNumberBaseline = {} this._modelNumberBaseline = {}
this._modelShopBaseline = {}
const list = (res.rows || res.data || []) const list = (res.rows || res.data || [])
this.list = list.map(item => this.normalizeOrderListItem(item)) this.list = list.map(item => this.normalizeOrderListItem(item))
this.total = res.total || 0 this.total = res.total || 0
@@ -2059,6 +2095,7 @@ export default {
orderSearch: undefined, orderSearch: undefined,
distributionMark: undefined, distributionMark: undefined,
modelNumber: undefined, modelNumber: undefined,
modelShop: undefined,
modelNumberExclude: undefined, modelNumberExclude: undefined,
link: undefined, link: undefined,
buyer: undefined, buyer: undefined,
@@ -2272,20 +2309,92 @@ export default {
if (v == null) return '' if (v == null) return ''
return String(v).trim() return String(v).trim()
}, },
/** 型号失焦或点保存时写入数据库(与基线一致则不调接口) */ normalizeModelShopInput(v) {
saveModelNumberIfChanged(row, successMsg) { if (v == null) return ''
return String(v).trim()
},
fullModelNumber(row) {
if (!row) return ''
const base = this.normalizeModelNumberInput(row.modelNumber)
const shop = this.normalizeModelShopInput(row.modelShop)
if (!base) return shop
if (!shop) return base
if (base.endsWith(shop)) return base
return base + shop
},
async loadShopOptions() {
try {
const res = await listQuickRecordShopOptions()
if (!(res && (res.code === 200 || res.msg === '操作成功'))) return
const rows = Array.isArray(res.data) ? res.data : []
this.shopOptions = rows
.filter(o => o && String(o.prefix || '').trim())
.map(o => ({
prefix: String(o.prefix).trim(),
label: o.label || o.prefix
}))
.sort((a, b) => b.prefix.length - a.prefix.length)
} catch (e) {
this.shopOptions = []
}
},
parseModelWithShopFromOptions(fullModel) {
const full = String(fullModel || '').trim()
if (!full || !this.shopOptions.length) return { base: full, shop: '' }
for (const opt of this.shopOptions) {
const p = opt.prefix
if (p && full.endsWith(p) && full.length > p.length) {
return { base: full.slice(0, -p.length), shop: p }
}
}
return { base: full, shop: '' }
},
/** 型号/店铺变更后写入数据库(与基线一致则不调接口) */
saveModelFieldsIfChanged(row, successMsg) {
if (row == null || row.id == null) return if (row == null || row.id == null) return
const next = this.normalizeModelNumberInput(row.modelNumber) const parsed = this.parseModelWithShopFromOptions(row.modelNumber)
row.modelNumber = next if (parsed.prefix) {
const base = this._modelNumberBaseline[row.id] != null ? this._modelNumberBaseline[row.id] : '' row.modelNumber = parsed.base
if (next === base) return row.modelShop = parsed.shop
}
const nextBase = this.normalizeModelNumberInput(row.modelNumber)
const nextShop = this.normalizeModelShopInput(row.modelShop)
row.modelNumber = nextBase
row.modelShop = nextShop
const baseNum = this._modelNumberBaseline[row.id] != null ? this._modelNumberBaseline[row.id] : ''
const baseShop = this._modelShopBaseline[row.id] != null ? this._modelShopBaseline[row.id] : ''
if (nextBase === baseNum && nextShop === baseShop) return
this.persistOrderRow(row, successMsg) this.persistOrderRow(row, successMsg)
}, },
onModelNumberBlur(row) { onModelNumberBlur(row) {
this.saveModelNumberIfChanged(row) this.onModelFieldBlur(row)
}, },
onModelNumberSave(row) { onModelNumberSave(row) {
this.saveModelNumberIfChanged(row, '型号已保存') this.saveModelFieldsIfChanged(row, '型号已保存')
},
onModelFieldBlur(row) {
this.saveModelFieldsIfChanged(row)
},
onModelShopChange(row) {
this.saveModelFieldsIfChanged(row, '型号店铺已保存')
},
handleMigrateModelShop() {
this.$confirm('将按「快捷录单店铺选项」配置,扫描全部订单:若型号末尾匹配店铺前缀,则写入「型号店铺」并从型号中截掉。是否继续?', '拆分型号店铺', {
type: 'warning'
}).then(() => {
this.modelShopMigrateLoading = true
return migrateModelShopSplit()
}).then(res => {
const data = (res && res.data) || {}
this.$message.success(data.message || '迁移完成')
this.getList()
}).catch(e => {
if (e !== 'cancel') {
this.$message.error((e && e.message) || '迁移失败')
}
}).finally(() => {
this.modelShopMigrateLoading = false
})
}, },
onOrderSellingPriceTypeChange(row) { onOrderSellingPriceTypeChange(row) {
row.sellingPriceManual = 0 row.sellingPriceManual = 0
@@ -2341,7 +2450,7 @@ export default {
/** 复制篮子群「发货摘要」:型号 + 整段地址(去空白) + 物流短链 */ /** 复制篮子群「发货摘要」:型号 + 整段地址(去空白) + 物流短链 */
buildBasketShipSummaryText(row) { buildBasketShipSummaryText(row) {
if (!row) return '' if (!row) return ''
const model = (row.modelNumber != null ? String(row.modelNumber).trim() : '') || '—' const model = this.fullModelNumber(row) || '—'
const addrRaw = row.address != null ? String(row.address).trim() : '' const addrRaw = row.address != null ? String(row.address).trim() : ''
const addr = (addrRaw.replace(/\s+/g, '')) || '—' const addr = (addrRaw.replace(/\s+/g, '')) || '—'
const url = this.normalizeBasketLogisticsUrl(row.logisticsLink) const url = this.normalizeBasketLogisticsUrl(row.logisticsLink)
@@ -3080,7 +3189,7 @@ export default {
// 既有顺序保持不变 // 既有顺序保持不变
parts.push(toLine(row.remark)) // 内部单号 parts.push(toLine(row.remark)) // 内部单号
parts.push(toLine(row.orderId)) // 订单号 parts.push(toLine(row.orderId)) // 订单号
parts.push(toLine(row.modelNumber)) // 型号 parts.push(toLine(this.fullModelNumber(row))) // 型号
parts.push(toLine(row.thirdPartyOrderNo)) // 第三方单号 parts.push(toLine(row.thirdPartyOrderNo)) // 第三方单号
parts.push(toLine(row.distributionMark)) // 分销标记 parts.push(toLine(row.distributionMark)) // 分销标记
parts.push(toLine(row.address)) // 地址 parts.push(toLine(row.address)) // 地址
@@ -3113,7 +3222,7 @@ export default {
? row.thirdPartyOrderNo : (row.remark || '') ? row.thirdPartyOrderNo : (row.remark || '')
// 型号 // 型号
const modelNumber = row.modelNumber || '' const modelNumber = this.fullModelNumber(row)
// 数量固定为1 // 数量固定为1
const quantity = '1' const quantity = '1'
@@ -3196,7 +3305,7 @@ export default {
const orderId = row.orderId || '' const orderId = row.orderId || ''
// 型号modelNumber // 型号modelNumber
const modelNumber = row.modelNumber || '' const modelNumber = this.fullModelNumber(row)
// 返现金额(团长):空 // 返现金额(团长):空
const leaderRebateAmount = '' const leaderRebateAmount = ''
@@ -3384,7 +3493,7 @@ export default {
? row.thirdPartyOrderNo : (row.remark || '') ? row.thirdPartyOrderNo : (row.remark || '')
// 型号 // 型号
const modelNumber = row.modelNumber || '' const modelNumber = this.fullModelNumber(row)
// 数量固定为1 // 数量固定为1
const quantity = '1' const quantity = '1'
@@ -3480,7 +3589,7 @@ export default {
const orderId = row.orderId || '' const orderId = row.orderId || ''
// 型号modelNumber // 型号modelNumber
const modelNumber = row.modelNumber || '' const modelNumber = this.fullModelNumber(row)
// 返现金额(团长):空 // 返现金额(团长):空
const leaderRebateAmount = '' const leaderRebateAmount = ''
@@ -3564,7 +3673,7 @@ export default {
dateStr = `${year}/${month}/${day}` dateStr = `${year}/${month}/${day}`
} }
const modelNumber = row.modelNumber || '' const modelNumber = this.fullModelNumber(row)
const quantity = (row.productCount != null && row.productCount !== '') ? String(row.productCount) : '1' const quantity = (row.productCount != null && row.productCount !== '') ? String(row.productCount) : '1'
const address = row.address || '' const address = row.address || ''
const priceStr = row.paymentAmount != null ? row.paymentAmount.toFixed(2) : '' const priceStr = row.paymentAmount != null ? row.paymentAmount.toFixed(2) : ''
@@ -4113,6 +4222,33 @@ export default {
text-overflow: clip; text-overflow: clip;
white-space: nowrap; white-space: nowrap;
} }
.order-table ::v-deep .jd-model-edit-row {
display: flex;
gap: 4px;
align-items: center;
width: 100%;
}
.order-table ::v-deep .jd-model-edit-row .jd-order-input-model {
flex: 1;
min-width: 0;
}
.order-table ::v-deep .jd-model-edit-row .jd-order-model-shop {
width: 108px;
flex-shrink: 0;
}
.jd-mobile-model-edit {
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.jd-mobile-model-edit .jd-mobile-model-input {
flex: 1;
min-width: 120px;
}
.jd-mobile-model-edit .jd-mobile-model-shop {
width: 100%;
}
.order-table ::v-deep td.jd-col-model .cell { .order-table ::v-deep td.jd-col-model .cell {
padding-left: 4px; padding-left: 4px;
padding-right: 4px; padding-right: 4px;