feat: 优化消息互动中心与首页Banner呈现,同步全量业务数据与高清图标
@@ -27,9 +27,9 @@ class FcInstall extends Command
|
||||
* image 留空时小程序用「亮绿底 + 墨绿字 + 白色插画」内置配图渲染。
|
||||
*/
|
||||
const BANNERS = [
|
||||
['同城好物节 · 二手直降', '/pages/post/list?categoryId=3', 'linear-gradient(105deg, #00e070 0%, #00c860 42%, #046b3b 100%)'],
|
||||
['招聘旺季 · 免费发布', '/pages/post/create', 'linear-gradient(105deg, #00d96c 0%, #00b85c 40%, #023a21 100%)'],
|
||||
['本地服务 · 上门更省心', '/pages/post/list?categoryId=5', 'linear-gradient(105deg, #00e878 0%, #049e52 45%, #045c34 100%)'],
|
||||
['同城好物节 · 二手直降', '/pages/post/list?categoryId=3', 'linear-gradient(120deg, #10b981 0%, #34d399 50%, #a7f3d0 100%)'],
|
||||
['招聘旺季 · 免费发布', '/pages/post/create', 'linear-gradient(120deg, #059669 0%, #10b981 50%, #6ee7b7 100%)'],
|
||||
['本地服务 · 上门更省心', '/pages/post/list?categoryId=5', 'linear-gradient(120deg, #00d26a 0%, #34d399 50%, #bbf7d0 100%)'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,14 +24,14 @@ class FcSeedPosts extends Command
|
||||
const USERS = [
|
||||
1001 => ['同城小助手', '13800001111'],
|
||||
1002 => ['小王同学', '15033443344'],
|
||||
1003 => ['洁净到家', '13156785678'],
|
||||
2001 => ['顺捷物流', '13988888888'],
|
||||
2002 => ['安家房产', '13566666666'],
|
||||
2003 => ['骑行爱好者', '13712341234'],
|
||||
2004 => ['车行天下', '13800000000'],
|
||||
2005 => ['洁净到家', '13156785678'],
|
||||
2006 => ['小王同学', '15033443344'],
|
||||
2007 => ['优选二手车', '13700000000'],
|
||||
1003 => ['洁净到家·陈姐', '13156785678'],
|
||||
2001 => ['顺捷物流·杨经理', '13988888888'],
|
||||
2002 => ['安家房产·周主管', '13566666666'],
|
||||
2003 => ['骑行爱好者·小周', '13712341234'],
|
||||
2004 => ['车行天下·张总', '13800000000'],
|
||||
2005 => ['洁净到家·王师傅', '13156785678'],
|
||||
2006 => ['数码达人·阿豪', '15033443344'],
|
||||
2007 => ['优选二手车·赵经理', '13700000000'],
|
||||
];
|
||||
|
||||
/** 区域中心坐标(gcj02 近似值,用于首页「附近」排序演示) */
|
||||
@@ -82,6 +82,7 @@ class FcSeedPosts extends Command
|
||||
'id' => $id,
|
||||
'user_id' => $uid,
|
||||
'category_id' => $categoryId,
|
||||
'identity_type' => in_array($uid, [1003, 2001, 2002, 2004, 2005, 2007, 2011], true) ? 2 : 1,
|
||||
'title' => $row[1],
|
||||
'content' => $row[2],
|
||||
'cover' => $media[0],
|
||||
@@ -460,5 +461,17 @@ class FcSeedPosts extends Command
|
||||
->count();
|
||||
Db::name('fc_category')->where('id', $categoryId)->update(['post_count' => $count]);
|
||||
}
|
||||
|
||||
// 同步商家有效发帖数
|
||||
$merchants = Db::name('fc_merchant')->field('id,user_id')->select();
|
||||
foreach ($merchants as $m) {
|
||||
$mCount = Db::name('fc_post')
|
||||
->where('user_id', $m['user_id'])
|
||||
->where('audit_status', 1)
|
||||
->where('status', 1)
|
||||
->whereNull('deletetime')
|
||||
->count();
|
||||
Db::name('fc_merchant')->where('id', $m['id'])->update(['post_count' => $mCount]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,44 +3,45 @@
|
||||
namespace app\admin\model;
|
||||
|
||||
use think\Model;
|
||||
use traits\model\SoftDelete;
|
||||
use app\common\library\FcRelation;
|
||||
|
||||
|
||||
/**
|
||||
* 消息模型 · 同城分类信息
|
||||
*/
|
||||
class FcMessage extends Model
|
||||
{
|
||||
|
||||
|
||||
|
||||
|
||||
use SoftDelete;
|
||||
|
||||
// 表名
|
||||
protected $name = 'fc_message';
|
||||
|
||||
|
||||
// 自动写入时间戳字段
|
||||
protected $autoWriteTimestamp = 'integer';
|
||||
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = 'createtime';
|
||||
protected $updateTime = false;
|
||||
protected $deleteTime = false;
|
||||
protected $updateTime = 'updatetime';
|
||||
protected $deleteTime = 'deletetime';
|
||||
|
||||
// 追加属性
|
||||
protected $append = [
|
||||
'user_name',
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取接收用户名属性
|
||||
*
|
||||
* @param mixed $value 原值
|
||||
* @param array $data 当前行数据
|
||||
* @return string 用户昵称或标识
|
||||
*/
|
||||
public function getUserNameAttr($value, $data)
|
||||
{
|
||||
return FcRelation::userName(isset($data['user_id']) ? $data['user_id'] : 0);
|
||||
$uid = isset($data['user_id']) ? (int)$data['user_id'] : 0;
|
||||
if ($uid === 0) {
|
||||
return '全体用户(广播)';
|
||||
}
|
||||
return FcRelation::userName($uid);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,15 +6,11 @@
|
||||
<div class="tab-pane fade active in" id="one">
|
||||
<div class="widget-body no-padding">
|
||||
<div id="toolbar" class="toolbar">
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" >刷新</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('fc/message/add')?'':'hide'}" title="{:__('Add')}" >添加</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('fc/message/edit')?'':'hide'}" title="{:__('Edit')}" >{:__('Edit')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('fc/message/del')?'':'hide'}" title="{:__('Delete')}" >{:__('Delete')}</a>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a href="javascript:;" class="btn btn-primary btn-refresh" title="{:__('Refresh')}" ><i class="fa fa-refresh"></i> 刷新</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-add {:$auth->check('fc/message/add')?'':'hide'}" title="{:__('Add')}" ><i class="fa fa-plus"></i> 添加</a>
|
||||
<a href="javascript:;" class="btn btn-success btn-edit btn-disabled disabled {:$auth->check('fc/message/edit')?'':'hide'}" title="{:__('Edit')}" ><i class="fa fa-pencil"></i> {:__('Edit')}</a>
|
||||
<a href="javascript:;" class="btn btn-danger btn-del btn-disabled disabled {:$auth->check('fc/message/del')?'':'hide'}" title="{:__('Delete')}" ><i class="fa fa-trash"></i> {:__('Delete')}</a>
|
||||
<a class="btn btn-info btn-recyclebin btn-dialog {:$auth->check('fc/message/recyclebin')?'':'hide'}" href="fc/message/recyclebin" title="{:__('Recycle bin')}"><i class="fa fa-recycle"></i> 回收站</a>
|
||||
</div>
|
||||
<table id="table" class="table table-striped table-bordered table-hover table-nowrap"
|
||||
data-operate-edit="{:$auth->check('fc/message/edit')}"
|
||||
|
||||
@@ -222,6 +222,14 @@ class Home extends V1
|
||||
};
|
||||
$applyArea($query);
|
||||
|
||||
// 付费发帖拦截:未付清款项的帖子(微信支付待付款)严禁向外展示
|
||||
$applyPaid = function ($q) {
|
||||
$q->where(function ($w) {
|
||||
$w->where('amount', 'null')->whereOr('amount', '<=', 0)->whereOr('is_paid', 1);
|
||||
});
|
||||
};
|
||||
$applyPaid($query);
|
||||
|
||||
$counter = Db::name('fc_post')
|
||||
->where('audit_status', 1)
|
||||
->where('status', 1)
|
||||
@@ -233,6 +241,7 @@ class Home extends V1
|
||||
$counter->where('title', 'like', '%' . $keyword . '%');
|
||||
}
|
||||
$applyArea($counter);
|
||||
$applyPaid($counter);
|
||||
$total = $counter->count();
|
||||
|
||||
// 「附近」:距离在 PHP 内用 Haversine 计算后排序(不用 SQL 距离函数,避免 MySQL 兼容问题);
|
||||
|
||||
@@ -68,11 +68,40 @@ class Member extends V1
|
||||
? array_values($cfg['benefits'])
|
||||
: $default['benefits'];
|
||||
|
||||
$freeViewQuota = isset($cfg['freeView']) ? max(0, (int)$cfg['freeView']) : $default['freeView'];
|
||||
$postDiscount = isset($cfg['postDiscount']) ? (string)$cfg['postDiscount'] : $default['postDiscount'];
|
||||
|
||||
// 真实查询当前用户的会员权益消耗与剩余额度
|
||||
$me = $this->user();
|
||||
$isMember = false;
|
||||
$freeViewUsed = 0;
|
||||
$freeViewLeft = 0;
|
||||
$postQuotaLeft = 0;
|
||||
$memberExpireAt = null;
|
||||
|
||||
if ($me) {
|
||||
$now = time();
|
||||
$ext = $this->ext($me['id']);
|
||||
$isMember = (int)$ext['is_member'] === 1
|
||||
&& (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now);
|
||||
$today = date('Y-m-d');
|
||||
$freeViewUsed = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
|
||||
$freeViewLeft = $isMember ? max(0, $freeViewQuota - $freeViewUsed) : 0;
|
||||
$postQuotaLeft = $this->quotaLeft($ext);
|
||||
$memberExpireAt = !empty($ext['member_expiretime']) ? $this->iso($ext['member_expiretime']) : null;
|
||||
}
|
||||
|
||||
$this->ok([
|
||||
'freeView' => isset($cfg['freeView']) ? (int)$cfg['freeView'] : $default['freeView'],
|
||||
'postDiscount' => isset($cfg['postDiscount']) ? (string)$cfg['postDiscount'] : $default['postDiscount'],
|
||||
'distRate' => isset($cfg['distRate']) ? (string)$cfg['distRate'] : $default['distRate'],
|
||||
'benefits' => $benefits,
|
||||
'freeView' => $freeViewQuota,
|
||||
'postDiscount' => $postDiscount,
|
||||
'distRate' => isset($cfg['distRate']) ? (string)$cfg['distRate'] : $default['distRate'],
|
||||
'benefits' => $benefits,
|
||||
'isMember' => $isMember,
|
||||
'freeViewQuota' => $freeViewQuota,
|
||||
'freeViewUsed' => $freeViewUsed,
|
||||
'freeViewLeft' => $freeViewLeft,
|
||||
'postQuotaLeft' => $postQuotaLeft,
|
||||
'memberExpireAt' => $memberExpireAt,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,16 @@ use think\Db;
|
||||
/**
|
||||
* 用户端 · 消息中心(Module-MESSAGE)
|
||||
* 路由:
|
||||
* GET /api/v1/message/list M1 消息列表
|
||||
* POST /api/v1/message/:id/read M2 标记已读
|
||||
* GET /api/v1/message/unread-count M3 未读消息数(首页角标)
|
||||
* GET /api/v1/message/list M1 消息列表(支持个人消息与系统广播)
|
||||
* POST /api/v1/message/:id/read M2 标记单条已读
|
||||
* GET /api/v1/message/unread-count M3 未读消息数(首页角标统计)
|
||||
* POST /api/v1/message/read-all M4 一键已读(批量标记)
|
||||
* POST /api/v1/message/clear M5 一键清理(批量软删除/屏蔽)
|
||||
* POST /api/v1/message/:id/delete M6 删除单条消息(软删除/屏蔽)
|
||||
*
|
||||
* 说明:user_id=0 表示全体广播消息,用户可见。
|
||||
* 说明:
|
||||
* - user_id > 0 为用户专属通知,支持物理或软删除(deletetime)。
|
||||
* - user_id = 0 为全站系统广播,利用 read_uids 和 deleted_uids 支持多用户独立已读与独立清理,互不影响。
|
||||
*/
|
||||
class Message extends V1
|
||||
{
|
||||
@@ -19,6 +24,8 @@ class Message extends V1
|
||||
* M1 消息列表
|
||||
* GET /api/v1/message/list 入参 {type?,page,size}
|
||||
* → 分页 {list:[{id,type,title,content,read,createdAt}],page,size,total,hasMore}
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function list()
|
||||
{
|
||||
@@ -28,8 +35,17 @@ class Message extends V1
|
||||
|
||||
// 闭包重建查询:TP5.0 的 count() 会清空 Query::options,不能复用同一对象
|
||||
list($rows, $total, $page, $size) = $this->pageQuery(function ($forCount) use ($uid, $type) {
|
||||
$q = Db::name('fc_message')->where('user_id', 'in', [$uid, 0]);
|
||||
if ($type !== '' && $type !== null) {
|
||||
$q = Db::name('fc_message')
|
||||
->whereNull('deletetime')
|
||||
->where(function ($sub) use ($uid) {
|
||||
$sub->where('user_id', $uid)
|
||||
->whereOr(function ($sub2) use ($uid) {
|
||||
$sub2->where('user_id', 0)
|
||||
->whereRaw("NOT FIND_IN_SET('{$uid}', IFNULL(deleted_uids, ''))");
|
||||
});
|
||||
});
|
||||
|
||||
if ($type !== '' && $type !== null && $type !== 'all') {
|
||||
$q->where('type', $type);
|
||||
}
|
||||
if ($forCount) {
|
||||
@@ -40,12 +56,21 @@ class Message extends V1
|
||||
|
||||
$list = [];
|
||||
foreach ($rows as $row) {
|
||||
// 计算当前用户下的已读状态(广播消息匹配 read_uids 集合)
|
||||
$isRead = false;
|
||||
if ((int)$row['user_id'] === $uid) {
|
||||
$isRead = (bool)(int)$row['isread'];
|
||||
} else {
|
||||
$readUids = array_filter(explode(',', (string)$row['read_uids']));
|
||||
$isRead = (bool)(int)$row['isread'] || in_array((string)$uid, $readUids);
|
||||
}
|
||||
|
||||
$list[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'type' => (string)$row['type'],
|
||||
'title' => (string)$row['title'],
|
||||
'content' => (string)$row['content'],
|
||||
'read' => (bool)(int)$row['isread'],
|
||||
'read' => $isRead,
|
||||
'createdAt' => $this->iso($row['createtime']),
|
||||
];
|
||||
}
|
||||
@@ -54,8 +79,10 @@ class Message extends V1
|
||||
}
|
||||
|
||||
/**
|
||||
* M2 标记已读
|
||||
* M2 标记单条已读
|
||||
* POST /api/v1/message/:id/read → {read:true}
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function read()
|
||||
{
|
||||
@@ -63,9 +90,31 @@ class Message extends V1
|
||||
$uid = (int)$user['id'];
|
||||
$id = (int)$this->request->param('id', 0);
|
||||
|
||||
Db::name('fc_message')->where('id', $id)
|
||||
->where('user_id', 'in', [$uid, 0])
|
||||
->update(['isread' => 1, 'readtime' => time()]);
|
||||
$msg = Db::name('fc_message')->where('id', $id)->whereNull('deletetime')->find();
|
||||
if (!$msg) {
|
||||
$this->fail(404, '消息不存在或已删除');
|
||||
}
|
||||
|
||||
$now = time();
|
||||
if ((int)$msg['user_id'] === $uid) {
|
||||
Db::name('fc_message')->where('id', $id)->update([
|
||||
'isread' => 1,
|
||||
'readtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
} elseif ((int)$msg['user_id'] === 0) {
|
||||
// 系统广播:维护用户级已读集合 read_uids
|
||||
$readUids = array_filter(explode(',', (string)$msg['read_uids']));
|
||||
if (!in_array((string)$uid, $readUids)) {
|
||||
$readUids[] = (string)$uid;
|
||||
Db::name('fc_message')->where('id', $id)->update([
|
||||
'read_uids' => implode(',', array_unique($readUids)),
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$this->fail(403, '无权操作此消息');
|
||||
}
|
||||
|
||||
$this->ok(['read' => true]);
|
||||
}
|
||||
@@ -73,19 +122,184 @@ class Message extends V1
|
||||
/**
|
||||
* M3 未读消息数(首页消息角标)
|
||||
* GET /api/v1/message/unread-count → {count}
|
||||
* 规则:统计当前用户可见(user_id = 本人 或 0 广播)且 isread=0 的消息条数,
|
||||
* 替代前端原先写死的角标数字。
|
||||
* 规则:统计个人专属未读数 + 全体广播中未被当前用户读取的有效条数
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function unreadCount()
|
||||
{
|
||||
$user = $this->auth();
|
||||
$uid = (int)$user['id'];
|
||||
|
||||
$count = (int)Db::name('fc_message')
|
||||
->where('user_id', 'in', [$uid, 0])
|
||||
// 1. 专属个人消息未读数
|
||||
$userCount = (int)Db::name('fc_message')
|
||||
->where('user_id', $uid)
|
||||
->where('isread', 0)
|
||||
->whereNull('deletetime')
|
||||
->count();
|
||||
|
||||
$this->ok(['count' => $count]);
|
||||
// 2. 系统广播未读数
|
||||
$broadcastMsgs = Db::name('fc_message')
|
||||
->where('user_id', 0)
|
||||
->whereNull('deletetime')
|
||||
->select();
|
||||
|
||||
$broadcastCount = 0;
|
||||
foreach ($broadcastMsgs as $b) {
|
||||
$deletedUids = array_filter(explode(',', (string)$b['deleted_uids']));
|
||||
if (in_array((string)$uid, $deletedUids)) {
|
||||
continue; // 用户已删除该广播
|
||||
}
|
||||
if ((int)$b['isread'] === 1) {
|
||||
continue; // 广播全局标记已读
|
||||
}
|
||||
$readUids = array_filter(explode(',', (string)$b['read_uids']));
|
||||
if (!in_array((string)$uid, $readUids)) {
|
||||
$broadcastCount++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->ok(['count' => $userCount + $broadcastCount]);
|
||||
}
|
||||
|
||||
/**
|
||||
* M4 一键已读
|
||||
* POST /api/v1/message/read-all 入参 {type?}
|
||||
* 规则:将当前分类下的专属未读消息及有效广播全部标记为已读
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function readAll()
|
||||
{
|
||||
$user = $this->auth();
|
||||
$uid = (int)$user['id'];
|
||||
$type = $this->input('type', '');
|
||||
$now = time();
|
||||
|
||||
// 1. 批量标记专属个人消息
|
||||
$q = Db::name('fc_message')
|
||||
->where('user_id', $uid)
|
||||
->where('isread', 0)
|
||||
->whereNull('deletetime');
|
||||
|
||||
if ($type !== '' && $type !== null && $type !== 'all') {
|
||||
$q->where('type', $type);
|
||||
}
|
||||
$affected = $q->update([
|
||||
'isread' => 1,
|
||||
'readtime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
|
||||
// 2. 批量将有效系统广播加入当前用户已读
|
||||
$broadQ = Db::name('fc_message')->where('user_id', 0)->whereNull('deletetime');
|
||||
if ($type !== '' && $type !== null && $type !== 'all') {
|
||||
$broadQ->where('type', $type);
|
||||
}
|
||||
$broadcasts = $broadQ->select();
|
||||
foreach ($broadcasts as $b) {
|
||||
$delUids = array_filter(explode(',', (string)$b['deleted_uids']));
|
||||
if (in_array((string)$uid, $delUids)) {
|
||||
continue;
|
||||
}
|
||||
$readUids = array_filter(explode(',', (string)$b['read_uids']));
|
||||
if (!in_array((string)$uid, $readUids)) {
|
||||
$readUids[] = (string)$uid;
|
||||
Db::name('fc_message')->where('id', $b['id'])->update([
|
||||
'read_uids' => implode(',', array_unique($readUids)),
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$affected++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->ok(['success' => true, 'affected' => (int)$affected]);
|
||||
}
|
||||
|
||||
/**
|
||||
* M5 一键清理
|
||||
* POST /api/v1/message/clear 入参 {type?}
|
||||
* 规则:对专属通知执行软删除(deletetime);对广播消息记录当前用户已删除集合(deleted_uids)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clear()
|
||||
{
|
||||
$user = $this->auth();
|
||||
$uid = (int)$user['id'];
|
||||
$type = $this->input('type', '');
|
||||
$now = time();
|
||||
|
||||
// 1. 专属消息软删除
|
||||
$q = Db::name('fc_message')->where('user_id', $uid)->whereNull('deletetime');
|
||||
if ($type !== '' && $type !== null && $type !== 'all') {
|
||||
$q->where('type', $type);
|
||||
}
|
||||
$affected = $q->update([
|
||||
'deletetime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
|
||||
// 2. 广播消息:将 uid 追加到 deleted_uids
|
||||
$broadQ = Db::name('fc_message')->where('user_id', 0)->whereNull('deletetime');
|
||||
if ($type !== '' && $type !== null && $type !== 'all') {
|
||||
$broadQ->where('type', $type);
|
||||
}
|
||||
$broadcasts = $broadQ->select();
|
||||
foreach ($broadcasts as $b) {
|
||||
$delUids = array_filter(explode(',', (string)$b['deleted_uids']));
|
||||
if (!in_array((string)$uid, $delUids)) {
|
||||
$delUids[] = (string)$uid;
|
||||
Db::name('fc_message')->where('id', $b['id'])->update([
|
||||
'deleted_uids' => implode(',', array_unique($delUids)),
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
$affected++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->ok(['success' => true, 'affected' => (int)$affected]);
|
||||
}
|
||||
|
||||
/**
|
||||
* M6 删除单条消息
|
||||
* POST /api/v1/message/:id/delete
|
||||
* 规则:对专属消息软删除(deletetime);对广播消息加入 deleted_uids
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
$user = $this->auth();
|
||||
$uid = (int)$user['id'];
|
||||
$id = (int)$this->request->param('id', 0);
|
||||
$now = time();
|
||||
|
||||
$msg = Db::name('fc_message')->where('id', $id)->whereNull('deletetime')->find();
|
||||
if (!$msg) {
|
||||
$this->fail(404, '消息不存在或已删除');
|
||||
}
|
||||
|
||||
if ((int)$msg['user_id'] === $uid) {
|
||||
// 专属个人消息执行软删除
|
||||
Db::name('fc_message')->where('id', $id)->update([
|
||||
'deletetime' => $now,
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
} elseif ((int)$msg['user_id'] === 0) {
|
||||
// 广播消息:记录已删除该消息的用户 UID
|
||||
$delUids = array_filter(explode(',', (string)$msg['deleted_uids']));
|
||||
if (!in_array((string)$uid, $delUids)) {
|
||||
$delUids[] = (string)$uid;
|
||||
Db::name('fc_message')->where('id', $id)->update([
|
||||
'deleted_uids' => implode(',', array_unique($delUids)),
|
||||
'updatetime' => $now,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$this->fail(403, '无权操作此消息');
|
||||
}
|
||||
|
||||
$this->ok(['success' => true]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +308,7 @@ class Post extends V1
|
||||
// LBS 坐标(gcj02;未定位/为 0 时存 NULL,首页「附近」不参与距离排序)
|
||||
'lat' => $this->coord('lat', 90),
|
||||
'lng' => $this->coord('lng', 180),
|
||||
'is_paid' => $paid ? 1 : 0,
|
||||
'is_paid' => ($paid && $paidNow) ? 1 : 0, // 微信支付发帖未付款前为 0,防止未支付信息外泄
|
||||
'amount' => $paid ? $amount : 0,
|
||||
'pay_type' => $paid ? $payType : '',
|
||||
'audit_status' => 0,
|
||||
@@ -823,38 +823,77 @@ class Post extends V1
|
||||
* @param array $post fc_post 行
|
||||
* @return array
|
||||
*/
|
||||
/**
|
||||
* 详情页联系方式与内容访问控制(严格执行会员权益与付费拦截)
|
||||
*
|
||||
* 可见规则:
|
||||
* ① 本人发布的信息或已付费解锁记录(fc_pay_view)→ unlocked=true,返回真实联系方式明文
|
||||
* ② 会员享有每日免费查看额度,未点击解锁时不泄露真实号码,下发真实剩余额度与会员标识
|
||||
* ③ 未解锁状态下,对标题与详细正文中的手机号执行掩码遮蔽,封堵正文直接贴电话的白嫖漏洞
|
||||
*
|
||||
* @param array $data buildPost() 结果
|
||||
* @param array $post fc_post 行
|
||||
* @return array 增强后的 Post 详情结构
|
||||
*/
|
||||
protected function contactAccess($data, $post)
|
||||
{
|
||||
$data['unlocked'] = false;
|
||||
$data['wechat'] = '';
|
||||
|
||||
$me = $this->user();
|
||||
if (!$me) {
|
||||
return $data;
|
||||
$now = time();
|
||||
$isMember = false;
|
||||
$quota = $this->memberFreeViewQuota();
|
||||
$used = 0;
|
||||
$freeViewLeft = 0;
|
||||
$payViewPrice = round((float)$this->cfg('fee.payView', 2), 2);
|
||||
|
||||
$isOwner = false;
|
||||
$paid = false;
|
||||
|
||||
if ($me) {
|
||||
$ext = $this->ext($me['id']);
|
||||
$isMember = (int)$ext['is_member'] === 1
|
||||
&& (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now);
|
||||
$isOwner = (int)$post['user_id'] === (int)$me['id'];
|
||||
$paid = (bool)Db::name('fc_pay_view')
|
||||
->where('user_id', $me['id'])
|
||||
->where('post_id', $post['id'])
|
||||
->find();
|
||||
|
||||
$today = date('Y-m-d');
|
||||
$used = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
|
||||
$freeViewLeft = $isMember ? max(0, $quota - $used) : 0;
|
||||
}
|
||||
|
||||
$now = time();
|
||||
$ext = $this->ext($me['id']);
|
||||
$isMember = (int)$ext['is_member'] === 1
|
||||
&& (empty($ext['member_expiretime']) || (int)$ext['member_expiretime'] > $now);
|
||||
$isOwner = (int)$post['user_id'] === (int)$me['id'];
|
||||
$paid = (bool)Db::name('fc_pay_view')
|
||||
->where('user_id', $me['id'])
|
||||
->where('post_id', $post['id'])
|
||||
->find();
|
||||
$data['isMember'] = $isMember;
|
||||
$data['freeViewQuota'] = $quota;
|
||||
$data['freeViewUsed'] = $used;
|
||||
$data['freeViewLeft'] = $freeViewLeft;
|
||||
$data['payViewPrice'] = $payViewPrice;
|
||||
|
||||
// 会员每日免费查看剩余次数(只做展示;解锁统一走 payView,按额度/付费计费)
|
||||
$quota = $this->memberFreeViewQuota();
|
||||
$today = date('Y-m-d');
|
||||
$used = ((string)$ext['free_view_date'] === $today) ? (int)$ext['free_view_used'] : 0;
|
||||
$data['freeViewLeft'] = $isMember ? max(0, $quota - $used) : 0;
|
||||
|
||||
if ($isOwner || $paid || $isMember) {
|
||||
$data['unlocked'] = true;
|
||||
$data['phone'] = (string)$post['phone'];
|
||||
$data['contact'] = (string)$post['phone'];
|
||||
// 真实解锁态判定:仅本人发布或已生成有效解锁记录
|
||||
if ($isOwner || $paid) {
|
||||
$data['unlocked'] = true;
|
||||
$data['phone'] = (string)$post['phone'];
|
||||
$data['contact'] = (string)$post['phone'];
|
||||
$data['contactName'] = (string)(isset($post['contact']) ? $post['contact'] : '');
|
||||
$data['wechat'] = $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $post['id']);
|
||||
$data['wechat'] = $post['wechat'] !== '' ? (string)$post['wechat'] : ('tongcheng_' . $post['id']);
|
||||
// 若发帖人有关联商家门店,在解锁态下一并展现真实门店电话
|
||||
if (!empty($data['merchant']) && is_array($data['merchant']) && !empty($data['merchant']['id'])) {
|
||||
$rawShopContact = Db::name('fc_merchant')->where('id', $data['merchant']['id'])->value('contact');
|
||||
if ($rawShopContact) {
|
||||
$data['merchant']['contact'] = (string)$rawShopContact;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 未解锁态:标题与详细描述中的任何手机号执行动态安全遮蔽,杜绝正文绕过
|
||||
if (!empty($data['content'])) {
|
||||
$data['content'] = $this->maskTextPhones($data['content']);
|
||||
}
|
||||
if (!empty($data['title'])) {
|
||||
$data['title'] = $this->maskTextPhones($data['title']);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
@@ -923,7 +962,10 @@ class Post extends V1
|
||||
$id = $this->routeId();
|
||||
|
||||
$payType = (string)$this->input('payType', '');
|
||||
if (!in_array($payType, ['wxpay', 'balance'], true)) {
|
||||
if ($payType === '') {
|
||||
$payType = 'free';
|
||||
}
|
||||
if (!in_array($payType, ['wxpay', 'balance', 'free'], true)) {
|
||||
$this->fail('支付方式不支持');
|
||||
}
|
||||
|
||||
@@ -965,6 +1007,9 @@ class Post extends V1
|
||||
if ($usedFree) {
|
||||
$amount = 0;
|
||||
$payType = 'free';
|
||||
} elseif ($payType === 'free') {
|
||||
// 用户希望使用会员免费特权,但额度已耗尽或非会员
|
||||
throw new \RuntimeException($isMember ? '今日会员免费查看额度已用完,请选择支付方式付费查看' : '该内容需付费查看,请选择支付方式');
|
||||
} elseif ($payType === 'balance') {
|
||||
// 条件扣款:并发下不会扣成负数
|
||||
$affected = Db::name('user')
|
||||
@@ -972,7 +1017,7 @@ class Post extends V1
|
||||
->where('money', '>=', $amount)
|
||||
->setDec('money', $amount);
|
||||
if (!$affected) {
|
||||
throw new \RuntimeException('余额不足');
|
||||
throw new \RuntimeException('余额不足,请充值或使用微信支付');
|
||||
}
|
||||
$balance = (float)Db::name('user')->where('id', (int)$user['id'])->value('money');
|
||||
$this->flow('expense', '付费查看', $amount, $balance, $id);
|
||||
@@ -1019,7 +1064,16 @@ class Post extends V1
|
||||
$this->ok(['order' => $pending]);
|
||||
}
|
||||
|
||||
$this->ok(['unlocked' => true, 'contact' => $contact]);
|
||||
$today = date('Y-m-d');
|
||||
$extNow = $this->ext($user['id']);
|
||||
$usedNow = ((string)$extNow['free_view_date'] === $today) ? (int)$extNow['free_view_used'] : 0;
|
||||
|
||||
$this->ok([
|
||||
'unlocked' => true,
|
||||
'contact' => $contact,
|
||||
'freeViewLeft' => $isMember ? max(0, $quota - $usedNow) : 0,
|
||||
'usedFree' => isset($usedFree) ? $usedFree : false,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1442,7 +1496,7 @@ class Post extends V1
|
||||
'address' => (string)(isset($m['address']) ? $m['address'] : ''),
|
||||
'lat' => (isset($m['lat']) && $m['lat'] !== null && $m['lat'] !== '') ? (float)$m['lat'] : null,
|
||||
'lng' => (isset($m['lng']) && $m['lng'] !== null && $m['lng'] !== '') ? (float)$m['lng'] : null,
|
||||
'contact' => (string)(isset($m['contact']) ? $m['contact'] : ''),
|
||||
'contact' => $this->maskPhone(isset($m['contact']) ? $m['contact'] : ''), // 默认脱敏,未解锁绝不泄露
|
||||
'category' => (string)(isset($m['category']) ? $m['category'] : ''),
|
||||
'notice' => (string)(isset($m['notice']) ? $m['notice'] : ''),
|
||||
'auditStatus' => (int)$m['audit_status'],
|
||||
|
||||
@@ -457,11 +457,68 @@ class V1 extends Api
|
||||
return date('c', (int)$timestamp);
|
||||
}
|
||||
|
||||
/** 手机号脱敏 */
|
||||
/**
|
||||
* 手机号与电话号码多格式安全脱敏
|
||||
* 兼容标准11位手机号、带+86/0086国际前缀、带空格/中划线及带区号固定电话,防止未付费信息绕过泄露
|
||||
*
|
||||
* @param string $phone 原始号码
|
||||
* @return string 脱敏后的号码字符串
|
||||
*/
|
||||
protected function maskPhone($phone)
|
||||
{
|
||||
$phone = (string)$phone;
|
||||
return strlen($phone) === 11 ? substr($phone, 0, 3) . '****' . substr($phone, 7) : $phone;
|
||||
$raw = trim((string)$phone);
|
||||
if ($raw === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 统一提取数字部分进行标准化识别
|
||||
$clean = preg_replace("/[^\d]/", "", $raw);
|
||||
|
||||
// 去除大陆前缀 86 或 0086
|
||||
if (strpos($clean, "86") === 0 && strlen($clean) === 13) {
|
||||
$clean = substr($clean, 2);
|
||||
}
|
||||
|
||||
// 11位大陆标准手机号
|
||||
if (preg_match("/^1[3-9]\d{9}$/", $clean)) {
|
||||
return substr($clean, 0, 3) . "****" . substr($clean, 7);
|
||||
}
|
||||
|
||||
// 10~12位座机固话(如 085188888888、01088888888)
|
||||
if (preg_match("/^0\d{9,11}$/", $clean)) {
|
||||
$prefixLen = (strpos($clean, "01") === 0 || strpos($clean, "02") === 0) ? 3 : 4;
|
||||
return substr($clean, 0, $prefixLen) . "****" . substr($clean, -3);
|
||||
}
|
||||
|
||||
// 其它>=7位的任意连续数字号码,统一对中段进行掩码
|
||||
$len = strlen($clean);
|
||||
if ($len >= 7) {
|
||||
$start = max(1, (int)floor(($len - 4) / 2));
|
||||
return substr($clean, 0, $start) . "****" . substr($clean, $start + 4);
|
||||
}
|
||||
|
||||
return $raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本内容中的手机号与电话动态遮蔽
|
||||
* 用于未付费解锁状态下对标题、详细描述中的手机号执行掩码保护,杜绝发帖人在正文直接贴电话白嫖
|
||||
*
|
||||
* @param string $content 原始富文本或正文内容
|
||||
* @return string 动态掩码后的文本
|
||||
*/
|
||||
protected function maskTextPhones($content)
|
||||
{
|
||||
$text = (string)$content;
|
||||
if ($text === "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
// 匹配各类11位大陆手机号码,含可能夹带的空格或横杠(如 138 0000 0000 或 138-0000-0000)
|
||||
$pattern = '/(?:(?:\+|00)86[\s-]?)?(1[3-9]\d)[\s-]?(\d{4})[\s-]?(\d{4})/u';
|
||||
return preg_replace_callback($pattern, function ($matches) {
|
||||
return $matches[1] . '****' . $matches[3];
|
||||
}, $text);
|
||||
}
|
||||
|
||||
/** 生成业务单号(前缀 + 时间 + 8 位随机十六进制,避免同秒并发碰撞) */
|
||||
|
||||
@@ -45,10 +45,34 @@ class OrderFulfill
|
||||
self::unlockPayView($order);
|
||||
}
|
||||
|
||||
// post 及其它类型:信息已落库,仅需结算分销
|
||||
if ($type === 'post') {
|
||||
self::fulfillPost($order);
|
||||
}
|
||||
|
||||
// 其它消费类型统一结算分销
|
||||
DistributionService::settleOrder($order);
|
||||
}
|
||||
|
||||
/**
|
||||
* 付费发帖支付成功后激活支付状态
|
||||
* 将 fc_post 的 is_paid 置为 1,确保未付款信息不会流出展示
|
||||
*
|
||||
* @param array $order
|
||||
* @return void
|
||||
*/
|
||||
protected static function fulfillPost(array $order)
|
||||
{
|
||||
$postId = (int)(isset($order['related_id']) ? $order['related_id'] : 0);
|
||||
if ($postId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Db::name('fc_post')->where('id', $postId)->update([
|
||||
'is_paid' => 1,
|
||||
'updatetime' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 付费查看解锁(user_id + post_id 唯一索引保证幂等)
|
||||
*
|
||||
|
||||
@@ -101,7 +101,10 @@ return [
|
||||
/* =============== 六、用户端 · 消息中心(Module-MESSAGE) =============== */
|
||||
'api/v1/message/list' => 'api/Message/list',
|
||||
'api/v1/message/unread-count' => 'api/Message/unreadCount',
|
||||
'api/v1/message/:id/read' => 'api/Message/read',
|
||||
'api/v1/message/read-all' => 'api/Message/readAll',
|
||||
'api/v1/message/clear' => 'api/Message/clear',
|
||||
'api/v1/message/:id/read' => 'api/Message/read',
|
||||
'api/v1/message/:id/delete' => 'api/Message/delete',
|
||||
|
||||
/* =============== 六·B、用户端 · 交易与支付(充值 / 微信支付 / 优惠券) ===============
|
||||
* · pay/notify 为微信支付异步通知:**公开路由,无需登录**,验签与订单置为 paid 由
|
||||
|
||||
@@ -11,6 +11,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu
|
||||
del_url: 'fc/message/del',
|
||||
multi_url: 'fc/message/multi',
|
||||
import_url: 'fc/message/import',
|
||||
recyclebin_url: 'fc/message/recyclebin',
|
||||
table: 'fc_message',
|
||||
}
|
||||
});
|
||||
|
||||
|
之前 宽度: | 高度: | 大小: 11 KiB 之后 宽度: | 高度: | 大小: 9.9 KiB |
|
之前 宽度: | 高度: | 大小: 11 KiB 之后 宽度: | 高度: | 大小: 10 KiB |
|
之前 宽度: | 高度: | 大小: 9.7 KiB 之后 宽度: | 高度: | 大小: 9.0 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 9.8 KiB 之后 宽度: | 高度: | 大小: 9.2 KiB |
|
之前 宽度: | 高度: | 大小: 9.6 KiB 之后 宽度: | 高度: | 大小: 9.2 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 1.3 KiB 之后 宽度: | 高度: | 大小: 1.2 KiB |
|
之前 宽度: | 高度: | 大小: 1.9 KiB 之后 宽度: | 高度: | 大小: 1.8 KiB |
|
之前 宽度: | 高度: | 大小: 822 B 之后 宽度: | 高度: | 大小: 3.0 KiB |
|
之前 宽度: | 高度: | 大小: 2.8 KiB 之后 宽度: | 高度: | 大小: 2.5 KiB |
|
之前 宽度: | 高度: | 大小: 9.1 KiB 之后 宽度: | 高度: | 大小: 8.6 KiB |
|
之前 宽度: | 高度: | 大小: 837 B 之后 宽度: | 高度: | 大小: 3.0 KiB |
|
之前 宽度: | 高度: | 大小: 9.2 KiB 之后 宽度: | 高度: | 大小: 8.7 KiB |
|
之前 宽度: | 高度: | 大小: 11 KiB 之后 宽度: | 高度: | 大小: 9.7 KiB |
|
之前 宽度: | 高度: | 大小: 8.6 KiB 之后 宽度: | 高度: | 大小: 8.2 KiB |
|
之前 宽度: | 高度: | 大小: 9.8 KiB 之后 宽度: | 高度: | 大小: 8.7 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.5 KiB |
|
之前 宽度: | 高度: | 大小: 9.5 KiB 之后 宽度: | 高度: | 大小: 9.2 KiB |
|
之前 宽度: | 高度: | 大小: 9.7 KiB 之后 宽度: | 高度: | 大小: 9.1 KiB |
|
之前 宽度: | 高度: | 大小: 3.3 KiB 之后 宽度: | 高度: | 大小: 2.8 KiB |
|
之前 宽度: | 高度: | 大小: 1.1 KiB 之后 宽度: | 高度: | 大小: 1018 B |
|
之前 宽度: | 高度: | 大小: 3.1 KiB 之后 宽度: | 高度: | 大小: 2.6 KiB |
|
之前 宽度: | 高度: | 大小: 1.3 KiB 之后 宽度: | 高度: | 大小: 1.3 KiB |
|
之前 宽度: | 高度: | 大小: 2.2 KiB 之后 宽度: | 高度: | 大小: 1.9 KiB |
|
之前 宽度: | 高度: | 大小: 1.4 KiB 之后 宽度: | 高度: | 大小: 1.3 KiB |
|
之前 宽度: | 高度: | 大小: 2.2 KiB 之后 宽度: | 高度: | 大小: 2.0 KiB |
|
之前 宽度: | 高度: | 大小: 2.7 KiB 之后 宽度: | 高度: | 大小: 2.4 KiB |
58
tools/analyze_integrity.php
普通文件
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* 深入分析用户、商家和帖子数据完整性
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function analyzeIntegrity() {
|
||||
$pdo = new PDO("mysql:host=127.0.0.1;port=3307;dbname=fenleixinxi;charset=utf8mb4", "fenleixinxi", "fenleixinxi");
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
echo "=== 1. 所有发布帖子的 user_id 在 fa_user 中是否存在 ===" . PHP_EOL;
|
||||
$postUsers = $pdo->query("SELECT DISTINCT user_id FROM fa_fc_post")->fetchAll(PDO::FETCH_COLUMN);
|
||||
foreach ($postUsers as $uid) {
|
||||
$user = $pdo->query("SELECT * FROM fa_user WHERE id = $uid")->fetch(PDO::FETCH_ASSOC);
|
||||
if (!$user) {
|
||||
echo "【警告】Post 的 user_id={$uid} 在 fa_user 中不存在!" . PHP_EOL;
|
||||
} else {
|
||||
$ext = $pdo->query("SELECT * FROM fa_fc_user_ext WHERE user_id = $uid")->fetch(PDO::FETCH_ASSOC);
|
||||
$merchant = $pdo->query("SELECT * FROM fa_fc_merchant WHERE user_id = $uid")->fetch(PDO::FETCH_ASSOC);
|
||||
echo "Post user_id: {$uid} | 用户名: {$user['username']} | 昵称: {$user['nickname']} | 头像: " . ($user['avatar'] ?: '【空】') . " | 手机: " . ($user['mobile'] ?: '【空】') . " | user_ext: " . ($ext ? "有(type={$ext['user_type']})" : '【无】') . " | merchant: " . ($merchant ? "有(name={$merchant['name']}, logo=" . ($merchant['logo'] ?: '【空】') . ")" : '无') . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
echo PHP_EOL . "=== 2. 查看 fa_fc_merchant 现有所有商家的详情 ===" . PHP_EOL;
|
||||
$merchants = $pdo->query("SELECT * FROM fa_fc_merchant")->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($merchants as $m) {
|
||||
echo "Merchant ID: {$m['id']}, user_id: {$m['user_id']}, name: {$m['name']}, contact: {$m['contact']}, logo: " . ($m['logo'] ?: '【空】') . ", banner: " . ($m['banner'] ?: '【空】') . ", address: " . ($m['address'] ?: '【空】') . ", lat: {$m['lat']}, lng: {$m['lng']}, category: {$m['category']}, shop_status: {$m['shop_status']}, audit_status: {$m['audit_status']}" . PHP_EOL;
|
||||
}
|
||||
|
||||
echo PHP_EOL . "=== 3. 检查 fa_fc_post 中各帖子的 identity_type 与 user/merchant 匹配情况 ===" . PHP_EOL;
|
||||
$posts = $pdo->query("SELECT p.id, p.user_id, p.category_id, p.identity_type, p.title, p.contact, p.phone, u.nickname, ue.user_type FROM fa_fc_post p LEFT JOIN fa_user u ON p.user_id = u.id LEFT JOIN fa_fc_user_ext ue ON p.user_id = ue.user_id WHERE p.deletetime IS NULL")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$inconsistent = [];
|
||||
foreach ($posts as $p) {
|
||||
$m = $pdo->query("SELECT * FROM fa_fc_merchant WHERE user_id = {$p['user_id']}")->fetch(PDO::FETCH_ASSOC);
|
||||
$isMerchantUser = ($p['user_type'] == 2 || $m);
|
||||
if ($isMerchantUser && $p['identity_type'] != 2) {
|
||||
$inconsistent[] = "Post #{$p['id']} '{$p['title']}' (user_id={$p['user_id']}, {$p['nickname']}) 是商家身份但 identity_type={$p['identity_type']}";
|
||||
}
|
||||
}
|
||||
echo "商家发布但 identity_type 为个人的帖子数量: " . count($inconsistent) . PHP_EOL;
|
||||
foreach (array_slice($inconsistent, 0, 10) as $msg) {
|
||||
echo " " . $msg . PHP_EOL;
|
||||
}
|
||||
|
||||
echo PHP_EOL . "=== 4. 检查用户表中的头像和手机号情况 ===" . PHP_EOL;
|
||||
$users = $pdo->query("SELECT * FROM fa_user")->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($users as $u) {
|
||||
$issues = [];
|
||||
if (empty($u['avatar'])) $issues[] = "无头像";
|
||||
if (empty($u['mobile'])) $issues[] = "无手机";
|
||||
if (empty($u['nickname']) || strpos($u['nickname'], '同城用户') === 0) $issues[] = "昵称弱({$u['nickname']})";
|
||||
if (!empty($issues)) {
|
||||
echo "User #{$u['id']} ({$u['username']}): " . implode(', ', $issues) . PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
analyzeIntegrity();
|
||||
249
tools/banner1.html
普通文件
@@ -0,0 +1,249 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
width: 1404px;
|
||||
height: 560px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
.banner-stage {
|
||||
width: 1404px;
|
||||
height: 560px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 80px;
|
||||
background: linear-gradient(135deg, #E6FBF0 0%, #D8F7E7 50%, #C4F3DC 100%);
|
||||
}
|
||||
.glow-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.glow-1 {
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
background: rgba(0, 224, 112, 0.35);
|
||||
right: -40px;
|
||||
top: -60px;
|
||||
}
|
||||
.glow-2 {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: rgba(245, 166, 35, 0.22);
|
||||
right: 320px;
|
||||
bottom: -80px;
|
||||
}
|
||||
.glow-subtle {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
left: 80px;
|
||||
top: -60px;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 780px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 26px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 2px solid rgba(0, 210, 106, 0.45);
|
||||
border-radius: 100px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #00A854;
|
||||
box-shadow: 0 6px 20px rgba(0, 168, 84, 0.08);
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.tag .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #00D26A;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.title {
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
line-height: 1.18;
|
||||
color: #1E293B;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.title span.highlight {
|
||||
background: linear-gradient(135deg, #00A854 0%, #00D26A 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 42px;
|
||||
background: linear-gradient(135deg, #00E070 0%, #00B85C 100%);
|
||||
border-radius: 100px;
|
||||
color: #FFFFFF;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 10px 24px rgba(0, 184, 92, 0.32);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.btn-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
}
|
||||
.badge-hot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: rgba(254, 243, 199, 0.95);
|
||||
border: 2px solid #FCD34D;
|
||||
border-radius: 100px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #B45309;
|
||||
}
|
||||
.badge-hot .fire {
|
||||
color: #F59E0B;
|
||||
font-size: 24px;
|
||||
}
|
||||
.visual {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 440px;
|
||||
height: 440px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.visual svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 20px 32px rgba(0, 168, 84, 0.16));
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="banner-stage">
|
||||
<div class="glow-circle glow-1"></div>
|
||||
<div class="glow-circle glow-2"></div>
|
||||
<div class="glow-circle glow-subtle"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tag"><span class="dot"></span>同城优选 · 真实验货</div>
|
||||
<div class="title">同城好物节 <span class="highlight">二手直降</span></div>
|
||||
<div class="subtitle">闲置数码家居 · 当面验货超省心 · 邻里自提</div>
|
||||
<div class="action-row">
|
||||
<div class="btn">立即捡漏 <span class="btn-arrow">→</span></div>
|
||||
<div class="badge-hot"><span class="fire">★</span> 爆款 3 折起</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual">
|
||||
<svg viewBox="0 0 440 440" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="220" cy="380" rx="160" ry="24" fill="rgba(0,168,84,0.12)"/>
|
||||
<path d="M140 210 L280 160 L360 210 L220 260 Z" fill="url(#boxTop)" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<path d="M140 210 L220 260 L220 370 L140 320 Z" fill="url(#boxLeft)"/>
|
||||
<path d="M220 260 L360 210 L360 320 L220 370 Z" fill="url(#boxRight)"/>
|
||||
<path d="M200 188 L300 224 L300 342 L200 306 Z" fill="url(#tapeGrad)" opacity="0.95"/>
|
||||
<path d="M246 172 L184 194 L184 312 L246 290 Z" fill="url(#tapeGrad2)" opacity="0.92"/>
|
||||
<g transform="translate(60, 110) scale(0.95)">
|
||||
<rect x="20" y="50" width="160" height="110" rx="24" fill="url(#camBody)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<rect x="50" y="30" width="50" height="24" rx="8" fill="#00D26A"/>
|
||||
<circle cx="100" cy="105" r="42" fill="url(#camLensBg)" stroke="#FFFFFF" stroke-width="6"/>
|
||||
<circle cx="100" cy="105" r="26" fill="url(#camLensGlass)"/>
|
||||
<circle cx="86" cy="92" r="8" fill="#FFFFFF" opacity="0.8"/>
|
||||
<circle cx="150" cy="65" r="14" fill="#F5A623"/>
|
||||
<circle cx="150" cy="65" r="6" fill="#FFF"/>
|
||||
</g>
|
||||
<g transform="translate(290, 80) rotate(15)">
|
||||
<ellipse cx="40" cy="40" rx="36" ry="36" fill="url(#goldGrad)" stroke="#FFE082" stroke-width="4"/>
|
||||
<text x="40" y="49" font-size="28" font-weight="900" fill="#B45309" text-anchor="middle">¥</text>
|
||||
</g>
|
||||
<g transform="translate(330, 240) rotate(-12)">
|
||||
<ellipse cx="26" cy="26" rx="24" ry="24" fill="url(#goldGrad)" stroke="#FFE082" stroke-width="3"/>
|
||||
<text x="26" y="33" font-size="20" font-weight="900" fill="#B45309" text-anchor="middle">%</text>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="boxTop" x1="140" y1="160" x2="360" y2="260" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset="1" stop-color="#E2F7EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="boxLeft" x1="140" y1="210" x2="220" y2="370" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D26A"/>
|
||||
<stop offset="1" stop-color="#009A4E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="boxRight" x1="220" y1="210" x2="360" y2="370" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00BA5E"/>
|
||||
<stop offset="1" stop-color="#007B3E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="tapeGrad" x1="200" y1="188" x2="300" y2="342" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCD34D"/>
|
||||
<stop offset="1" stop-color="#F59E0B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="tapeGrad2" x1="184" y1="172" x2="246" y2="312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDE68A"/>
|
||||
<stop offset="1" stop-color="#D97706"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="camBody" x1="20" y1="50" x2="180" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#334155"/>
|
||||
<stop offset="1" stop-color="#0F172A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="camLensBg" x1="58" y1="63" x2="142" y2="147" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D26A"/>
|
||||
<stop offset="1" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="camLensGlass" x1="74" y1="79" x2="126" y2="131" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#064E3B"/>
|
||||
<stop offset="1" stop-color="#022C22"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="goldGrad" x1="4" y1="4" x2="76" y2="76" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDE047"/>
|
||||
<stop offset="1" stop-color="#F59E0B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
243
tools/banner2.html
普通文件
@@ -0,0 +1,243 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
width: 1404px;
|
||||
height: 560px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
.banner-stage {
|
||||
width: 1404px;
|
||||
height: 560px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 80px;
|
||||
background: linear-gradient(135deg, #E6FAF4 0%, #D8F7EE 50%, #C2F0E5 100%);
|
||||
}
|
||||
.glow-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.glow-1 {
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
background: rgba(16, 185, 129, 0.32);
|
||||
right: -40px;
|
||||
top: -60px;
|
||||
}
|
||||
.glow-2 {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: rgba(59, 130, 246, 0.18);
|
||||
right: 320px;
|
||||
bottom: -80px;
|
||||
}
|
||||
.glow-subtle {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
left: 80px;
|
||||
top: -60px;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 780px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 26px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 2px solid rgba(16, 185, 129, 0.45);
|
||||
border-radius: 100px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
box-shadow: 0 6px 20px rgba(16, 185, 129, 0.08);
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.tag .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #10B981;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.title {
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
line-height: 1.18;
|
||||
color: #1E293B;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.title span.highlight {
|
||||
background: linear-gradient(135deg, #059669 0%, #10B981 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 42px;
|
||||
background: linear-gradient(135deg, #10B981 0%, #059669 100%);
|
||||
border-radius: 100px;
|
||||
color: #FFFFFF;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 10px 24px rgba(16, 185, 129, 0.32);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.btn-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
}
|
||||
.badge-hot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: rgba(220, 252, 231, 0.95);
|
||||
border: 2px solid #86EFAC;
|
||||
border-radius: 100px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #166534;
|
||||
}
|
||||
.badge-hot .fire {
|
||||
color: #10B981;
|
||||
font-size: 24px;
|
||||
}
|
||||
.visual {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 440px;
|
||||
height: 440px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.visual svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 20px 32px rgba(16, 185, 129, 0.16));
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="banner-stage">
|
||||
<div class="glow-circle glow-1"></div>
|
||||
<div class="glow-circle glow-2"></div>
|
||||
<div class="glow-circle glow-subtle"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tag"><span class="dot"></span>名企急招 · 官方认证</div>
|
||||
<div class="title">同城高薪直聘 <span class="highlight">急速入职</span></div>
|
||||
<div class="subtitle">海量好岗任选 · 直接聊老板 · 真实高薪看得见</div>
|
||||
<div class="action-row">
|
||||
<div class="btn">去投简历 <span class="btn-arrow">→</span></div>
|
||||
<div class="badge-hot"><span class="fire">⚡</span> 72h 急速反馈</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual">
|
||||
<svg viewBox="0 0 440 440" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="220" cy="380" rx="160" ry="24" fill="rgba(16,185,129,0.12)"/>
|
||||
<!-- 立体公文包 -->
|
||||
<rect x="90" y="160" width="260" height="190" rx="32" fill="url(#caseBody)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<!-- 公文包手柄 -->
|
||||
<path d="M160 160 C160 100, 280 100, 280 160" stroke="url(#caseHandle)" stroke-width="18" stroke-linecap="round" fill="none"/>
|
||||
<!-- 包带和金锁扣 -->
|
||||
<rect x="135" y="160" width="30" height="190" fill="#047857" opacity="0.6"/>
|
||||
<rect x="275" y="160" width="30" height="190" fill="#047857" opacity="0.6"/>
|
||||
<rect x="195" y="220" width="50" height="40" rx="10" fill="url(#goldGrad)" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<circle cx="220" cy="240" r="6" fill="#78350F"/>
|
||||
|
||||
<!-- 伸出的录取通知书 OFFER -->
|
||||
<g transform="translate(190, 60) rotate(10)">
|
||||
<rect x="0" y="0" width="140" height="170" rx="16" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="3"/>
|
||||
<rect x="20" y="24" width="70" height="12" rx="6" fill="#10B981"/>
|
||||
<rect x="20" y="48" width="100" height="8" rx="4" fill="#CBD5E1"/>
|
||||
<rect x="20" y="66" width="85" height="8" rx="4" fill="#CBD5E1"/>
|
||||
<rect x="20" y="84" width="60" height="8" rx="4" fill="#CBD5E1"/>
|
||||
<!-- 红色录取印章 -->
|
||||
<circle cx="95" cy="120" r="24" stroke="#EF4444" stroke-width="3" fill="rgba(239, 68, 68, 0.08)"/>
|
||||
<text x="95" y="126" font-size="13" font-weight="900" fill="#EF4444" text-anchor="middle">OFFER</text>
|
||||
</g>
|
||||
|
||||
<!-- 飞升上升箭头 -->
|
||||
<g transform="translate(320, 220)">
|
||||
<circle cx="36" cy="36" r="36" fill="url(#arrowBg)" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<path d="M24 44 L44 24 M44 24 H30 M44 24 V38" stroke="#FFFFFF" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
|
||||
<!-- 金色星级徽章 -->
|
||||
<g transform="translate(50, 240)">
|
||||
<polygon points="40,10 49,28 69,31 54,46 58,66 40,56 22,66 26,46 11,31 31,28" fill="url(#goldGrad)" stroke="#FFF" stroke-width="3"/>
|
||||
<circle cx="40" cy="38" r="14" fill="#F59E0B"/>
|
||||
<path d="M34 38 L38 42 L46 34" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="caseBody" x1="90" y1="160" x2="350" y2="350" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10B981"/>
|
||||
<stop offset="0.5" stop-color="#059669"/>
|
||||
<stop offset="1" stop-color="#047857"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="caseHandle" x1="160" y1="100" x2="280" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#34D399"/>
|
||||
<stop offset="1" stop-color="#047857"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="goldGrad" x1="0" y1="0" x2="70" y2="70" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDE047"/>
|
||||
<stop offset="1" stop-color="#F59E0B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="arrowBg" x1="0" y1="0" x2="72" y2="72" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3B82F6"/>
|
||||
<stop offset="1" stop-color="#1D4ED8"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
236
tools/banner3.html
普通文件
@@ -0,0 +1,236 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body {
|
||||
width: 1404px;
|
||||
height: 560px;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
}
|
||||
.banner-stage {
|
||||
width: 1404px;
|
||||
height: 560px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 80px;
|
||||
background: linear-gradient(135deg, #ECFAF2 0%, #DCF6E8 50%, #C4F1D9 100%);
|
||||
}
|
||||
.glow-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.glow-1 {
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
background: rgba(0, 210, 106, 0.32);
|
||||
right: -40px;
|
||||
top: -60px;
|
||||
}
|
||||
.glow-2 {
|
||||
width: 420px;
|
||||
height: 420px;
|
||||
background: rgba(245, 166, 35, 0.22);
|
||||
right: 320px;
|
||||
bottom: -80px;
|
||||
}
|
||||
.glow-subtle {
|
||||
width: 360px;
|
||||
height: 360px;
|
||||
background: rgba(255, 255, 255, 0.65);
|
||||
left: 80px;
|
||||
top: -60px;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 780px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 26px;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 2px solid rgba(0, 210, 106, 0.45);
|
||||
border-radius: 100px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #00A854;
|
||||
box-shadow: 0 6px 20px rgba(0, 168, 84, 0.08);
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.tag .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #00D26A;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.title {
|
||||
font-size: 64px;
|
||||
font-weight: 900;
|
||||
line-height: 1.18;
|
||||
color: #1E293B;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.title span.highlight {
|
||||
background: linear-gradient(135deg, #00A854 0%, #00D26A 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 34px;
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 16px 42px;
|
||||
background: linear-gradient(135deg, #00E070 0%, #00B85C 100%);
|
||||
border-radius: 100px;
|
||||
color: #FFFFFF;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 10px 24px rgba(0, 184, 92, 0.32);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.btn-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
}
|
||||
.badge-hot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: rgba(254, 243, 199, 0.95);
|
||||
border: 2px solid #FCD34D;
|
||||
border-radius: 100px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #B45309;
|
||||
}
|
||||
.badge-hot .fire {
|
||||
color: #F59E0B;
|
||||
font-size: 24px;
|
||||
}
|
||||
.visual {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 440px;
|
||||
height: 440px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.visual svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 20px 32px rgba(0, 168, 84, 0.16));
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="banner-stage">
|
||||
<div class="glow-circle glow-1"></div>
|
||||
<div class="glow-circle glow-2"></div>
|
||||
<div class="glow-circle glow-subtle"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tag"><span class="dot"></span>准时上门 · 售后无忧</div>
|
||||
<div class="title">本地便民到家 <span class="highlight">贴心省心</span></div>
|
||||
<div class="subtitle">家庭保洁开荒 · 维修拆洗搬家 · 专业师傅随叫随到</div>
|
||||
<div class="action-row">
|
||||
<div class="btn">一键预约 <span class="btn-arrow">→</span></div>
|
||||
<div class="badge-hot"><span class="fire">🎁</span> 新人立减 20元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual">
|
||||
<svg viewBox="0 0 440 440" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="220" cy="380" rx="160" ry="24" fill="rgba(0,168,84,0.12)"/>
|
||||
|
||||
<!-- 温馨小家立体房屋 -->
|
||||
<path d="M120 230 L230 140 L340 230 L340 360 L120 360 Z" fill="url(#houseFront)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<!-- 立体坡屋顶 -->
|
||||
<path d="M230 140 L340 230 L310 230 L230 160 Z" fill="#00A854"/>
|
||||
<path d="M100 230 L230 120 L360 230" stroke="#00D26A" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
|
||||
<!-- 暖阳发光窗户 -->
|
||||
<rect x="150" y="245" width="60" height="60" rx="14" fill="#FEF3C7" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<line x1="180" y1="245" x2="180" y2="305" stroke="#F59E0B" stroke-width="3"/>
|
||||
<line x1="150" y1="275" x2="210" y2="275" stroke="#F59E0B" stroke-width="3"/>
|
||||
|
||||
<!-- 翠绿入户大门 -->
|
||||
<rect x="245" y="255" width="65" height="105" rx="12" fill="#047857" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<circle cx="295" cy="305" r="6" fill="#FCD34D"/>
|
||||
|
||||
<!-- 立体便民工具箱 -->
|
||||
<g transform="translate(60, 260)">
|
||||
<rect x="0" y="30" width="115" height="80" rx="16" fill="url(#goldGrad)" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<path d="M30 30 C30 10, 85 10, 85 30" stroke="#B45309" stroke-width="8" stroke-linecap="round" fill="none"/>
|
||||
<line x1="0" y1="68" x2="115" y2="68" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<rect x="46" y="62" width="24" height="16" rx="4" fill="#78350F"/>
|
||||
<!-- 扳手小图标 -->
|
||||
<circle cx="90" cy="50" r="10" fill="rgba(255,255,255,0.4)"/>
|
||||
</g>
|
||||
|
||||
<!-- 官方认证绿色品质盾牌 -->
|
||||
<g transform="translate(315, 175)">
|
||||
<path d="M45 0 C68 0, 90 16, 90 50 C90 85, 45 105, 45 105 C45 105, 0 85, 0 50 C0 16, 22 0, 45 0 Z" fill="url(#shieldGrad)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<path d="M28 50 L40 62 L64 38" stroke="#FFFFFF" stroke-width="7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
|
||||
<defs>
|
||||
<linearGradient id="houseFront" x1="120" y1="140" x2="340" y2="360" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset="0.6" stop-color="#F0FDF4"/>
|
||||
<stop offset="1" stop-color="#DCFCE7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="shieldGrad" x1="0" y1="0" x2="90" y2="105" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00E070"/>
|
||||
<stop offset="1" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="goldGrad" x1="0" y1="10" x2="115" y2="110" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDE047"/>
|
||||
<stop offset="1" stop-color="#F59E0B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
398
tools/banners_render.html
普通文件
@@ -0,0 +1,398 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background: #f0f2f5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40px;
|
||||
padding: 40px;
|
||||
}
|
||||
.banner-stage {
|
||||
width: 1404px;
|
||||
height: 560px;
|
||||
border-radius: 48px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 80px;
|
||||
box-shadow: 0 16px 48px rgba(0, 168, 84, 0.12);
|
||||
}
|
||||
|
||||
/* Banner 1 翠绿微光 */
|
||||
.banner-1 {
|
||||
background: linear-gradient(135deg, #E6FBF0 0%, #D8F7E7 50%, #C4F3DC 100%);
|
||||
}
|
||||
/* Banner 2 清新薄荷蓝青 */
|
||||
.banner-2 {
|
||||
background: linear-gradient(135deg, #E8FBF7 0%, #DBF6F0 50%, #C6EFE6 100%);
|
||||
}
|
||||
/* Banner 3 春绿微暖阳 */
|
||||
.banner-3 {
|
||||
background: linear-gradient(135deg, #ECFAF2 0%, #DEF7EA 50%, #C8F3DD 100%);
|
||||
}
|
||||
|
||||
/* 背景装饰光球 */
|
||||
.glow-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
filter: blur(60px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.glow-1 {
|
||||
width: 480px;
|
||||
height: 480px;
|
||||
background: rgba(0, 224, 112, 0.35);
|
||||
right: -60px;
|
||||
top: -60px;
|
||||
}
|
||||
.glow-2 {
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
background: rgba(245, 166, 35, 0.22);
|
||||
right: 280px;
|
||||
bottom: -80px;
|
||||
}
|
||||
.glow-subtle {
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
left: 80px;
|
||||
top: -40px;
|
||||
}
|
||||
|
||||
/* 左侧文案区 */
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 760px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 24px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 2px solid rgba(0, 210, 106, 0.4);
|
||||
border-radius: 100px;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #00A854;
|
||||
box-shadow: 0 4px 16px rgba(0, 168, 84, 0.08);
|
||||
margin-bottom: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.tag .dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #00D26A;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.title {
|
||||
font-size: 64px;
|
||||
font-weight: 800;
|
||||
line-height: 1.18;
|
||||
color: #1E293B;
|
||||
letter-spacing: -0.5px;
|
||||
margin-bottom: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.title span.highlight {
|
||||
background: linear-gradient(135deg, #00A854 0%, #00D26A 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
.subtitle {
|
||||
font-size: 28px;
|
||||
font-weight: 500;
|
||||
color: #475569;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
}
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 40px;
|
||||
background: linear-gradient(135deg, #00E070 0%, #00B85C 100%);
|
||||
border-radius: 100px;
|
||||
color: #FFFFFF;
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 10px 24px rgba(0, 184, 92, 0.32);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.btn-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: rgba(255, 255, 255, 0.28);
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
}
|
||||
.badge-hot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: rgba(254, 243, 199, 0.95);
|
||||
border: 2px solid #FCD34D;
|
||||
border-radius: 100px;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #B45309;
|
||||
}
|
||||
.badge-hot .fire {
|
||||
color: #F59E0B;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* 右侧立体视觉插画区 */
|
||||
.visual {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 440px;
|
||||
height: 440px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.visual svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: drop-shadow(0 20px 32px rgba(0, 168, 84, 0.16));
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- BANNER 1: 二手好物 -->
|
||||
<div class="banner-stage banner-1" id="banner-1">
|
||||
<div class="glow-circle glow-1"></div>
|
||||
<div class="glow-circle glow-2"></div>
|
||||
<div class="glow-circle glow-subtle"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tag"><span class="dot"></span>同城优选 · 真实验货</div>
|
||||
<div class="title">同城好物节 <span class="highlight">二手直降</span></div>
|
||||
<div class="subtitle">闲置数码家居 · 当面验货超省心 · 邻里自提</div>
|
||||
<div class="action-row">
|
||||
<div class="btn">立即捡漏 <span class="btn-arrow">→</span></div>
|
||||
<div class="badge-hot"><span class="fire">★</span> 爆款 3 折起起</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual">
|
||||
<svg viewBox="0 0 440 440" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 柔光底座 -->
|
||||
<ellipse cx="220" cy="380" rx="160" ry="24" fill="rgba(0,168,84,0.12)"/>
|
||||
<!-- 立体快递/包裹箱 -->
|
||||
<path d="M140 210 L280 160 L360 210 L220 260 Z" fill="url(#boxTop)" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<path d="M140 210 L220 260 L220 370 L140 320 Z" fill="url(#boxLeft)"/>
|
||||
<path d="M220 260 L360 210 L360 320 L220 370 Z" fill="url(#boxRight)"/>
|
||||
<!-- 箱子胶带与绿丝带 -->
|
||||
<path d="M200 188 L300 224 L300 342 L200 306 Z" fill="url(#tapeGrad)" opacity="0.95"/>
|
||||
<path d="M246 172 L184 194 L184 312 L246 290 Z" fill="url(#tapeGrad2)" opacity="0.92"/>
|
||||
<!-- 立体相机 / 数码浮雕 -->
|
||||
<g transform="translate(60, 110) scale(0.95)">
|
||||
<rect x="20" y="50" width="160" height="110" rx="24" fill="url(#camBody)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<rect x="50" y="30" width="50" height="24" rx="8" fill="#00D26A"/>
|
||||
<circle cx="100" cy="105" r="42" fill="url(#camLensBg)" stroke="#FFFFFF" stroke-width="6"/>
|
||||
<circle cx="100" cy="105" r="26" fill="url(#camLensGlass)"/>
|
||||
<circle cx="86" cy="92" r="8" fill="#FFFFFF" opacity="0.8"/>
|
||||
<!-- 闪耀金色徽标 -->
|
||||
<circle cx="150" cy="65" r="14" fill="#F5A623"/>
|
||||
<circle cx="150" cy="65" r="6" fill="#FFF"/>
|
||||
</g>
|
||||
<!-- 漂浮金币与折扣卡 -->
|
||||
<g transform="translate(290, 80) rotate(15)">
|
||||
<ellipse cx="40" cy="40" rx="36" ry="36" fill="url(#goldGrad)" stroke="#FFE082" stroke-width="4"/>
|
||||
<text x="40" y="49" font-size="28" font-weight="900" fill="#B45309" text-anchor="middle">¥</text>
|
||||
</g>
|
||||
<g transform="translate(330, 240) rotate(-12)">
|
||||
<ellipse cx="26" cy="26" rx="24" ry="24" fill="url(#goldGrad)" stroke="#FFE082" stroke-width="3"/>
|
||||
<text x="26" y="33" font-size="20" font-weight="900" fill="#B45309" text-anchor="middle">%</text>
|
||||
</g>
|
||||
<!-- 渐变定义 -->
|
||||
<defs>
|
||||
<linearGradient id="boxTop" x1="140" y1="160" x2="360" y2="260" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset="1" stop-color="#E2F7EB"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="boxLeft" x1="140" y1="210" x2="220" y2="370" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D26A"/>
|
||||
<stop offset="1" stop-color="#009A4E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="boxRight" x1="220" y1="210" x2="360" y2="370" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00BA5E"/>
|
||||
<stop offset="1" stop-color="#007B3E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="tapeGrad" x1="200" y1="188" x2="300" y2="342" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FCD34D"/>
|
||||
<stop offset="1" stop-color="#F59E0B"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="tapeGrad2" x1="184" y1="172" x2="246" y2="312" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDE68A"/>
|
||||
<stop offset="1" stop-color="#D97706"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="camBody" x1="20" y1="50" x2="180" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#334155"/>
|
||||
<stop offset="1" stop-color="#0F172A"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="camLensBg" x1="58" y1="63" x2="142" y2="147" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00D26A"/>
|
||||
<stop offset="1" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="camLensGlass" x1="74" y1="79" x2="126" y2="131" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#064E3B"/>
|
||||
<stop offset="1" stop-color="#022C22"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="goldGrad" x1="4" y1="4" x2="76" y2="76" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FDE047"/>
|
||||
<stop offset="1" stop-color="#F59E0B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BANNER 2: 招聘求职 -->
|
||||
<div class="banner-stage banner-2" id="banner-2">
|
||||
<div class="glow-circle glow-1" style="background: rgba(16, 185, 129, 0.35);"></div>
|
||||
<div class="glow-circle glow-2" style="background: rgba(59, 130, 246, 0.18);"></div>
|
||||
<div class="glow-circle glow-subtle"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tag"><span class="dot" style="background: #10B981;"></span>真实企业 · 官方认证</div>
|
||||
<div class="title">同城高薪直聘 <span class="highlight" style="background: linear-gradient(135deg, #059669 0%, #10B981 100%); -webkit-background-clip: text;">急速入职</span></div>
|
||||
<div class="subtitle">海量好岗任选 · 直接聊老板 · 薪资透明全覆盖</div>
|
||||
<div class="action-row">
|
||||
<div class="btn" style="background: linear-gradient(135deg, #10B981 0%, #059669 100%);">去投简历 <span class="btn-arrow">→</span></div>
|
||||
<div class="badge-hot" style="background: rgba(220, 252, 231, 0.95); border-color: #86EFAC; color: #166534;"><span class="fire" style="color: #10B981;">⚡</span> 72h 急速反馈</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual">
|
||||
<svg viewBox="0 0 440 440" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="220" cy="380" rx="160" ry="24" fill="rgba(16,185,129,0.12)"/>
|
||||
<!-- 立体公文包 / 简历档案 -->
|
||||
<rect x="90" y="160" width="260" height="190" rx="32" fill="url(#caseBody)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<!-- 公文包手柄 -->
|
||||
<path d="M160 160 C160 110, 280 110, 280 160" stroke="url(#caseHandle)" stroke-width="18" stroke-linecap="round" fill="none"/>
|
||||
<!-- 包带和金锁扣 -->
|
||||
<rect x="135" y="160" width="30" height="190" fill="#047857" opacity="0.6"/>
|
||||
<rect x="275" y="160" width="30" height="190" fill="#047857" opacity="0.6"/>
|
||||
<rect x="195" y="220" width="50" height="40" rx="10" fill="url(#goldGrad)" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<circle cx="220" cy="240" r="6" fill="#78350F"/>
|
||||
<!-- 伸出的聘书 / OFFER -->
|
||||
<g transform="translate(190, 80) rotate(12)">
|
||||
<rect x="0" y="0" width="130" height="150" rx="16" fill="#FFFFFF" stroke="#E2E8F0" stroke-width="3"/>
|
||||
<rect x="20" y="24" width="70" height="12" rx="6" fill="#10B981"/>
|
||||
<rect x="20" y="48" width="90" height="8" rx="4" fill="#CBD5E1"/>
|
||||
<rect x="20" y="66" width="75" height="8" rx="4" fill="#CBD5E1"/>
|
||||
<rect x="20" y="84" width="50" height="8" rx="4" fill="#CBD5E1"/>
|
||||
<!-- 红色印章 -->
|
||||
<circle cx="85" cy="105" r="22" stroke="#EF4444" stroke-width="3" fill="rgba(239, 68, 68, 0.1)"/>
|
||||
<text x="85" y="110" font-size="12" font-weight="900" fill="#EF4444" text-anchor="middle">OFFER</text>
|
||||
</g>
|
||||
<!-- 金色认证勋章 -->
|
||||
<g transform="translate(60, 230)">
|
||||
<polygon points="40,10 49,28 69,31 54,46 58,66 40,56 22,66 26,46 11,31 31,28" fill="url(#goldGrad)" stroke="#FFF" stroke-width="3"/>
|
||||
<circle cx="40" cy="38" r="14" fill="#F59E0B"/>
|
||||
<path d="M34 38 L38 42 L46 34" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<!-- 渐变定义 -->
|
||||
<defs>
|
||||
<linearGradient id="caseBody" x1="90" y1="160" x2="350" y2="350" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#10B981"/>
|
||||
<stop offset="0.5" stop-color="#059669"/>
|
||||
<stop offset="1" stop-color="#047857"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="caseHandle" x1="160" y1="110" x2="280" y2="160" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#34D399"/>
|
||||
<stop offset="1" stop-color="#047857"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BANNER 3: 便民到家 -->
|
||||
<div class="banner-stage banner-3" id="banner-3">
|
||||
<div class="glow-circle glow-1" style="background: rgba(0, 210, 106, 0.32);"></div>
|
||||
<div class="glow-circle glow-2" style="background: rgba(245, 166, 35, 0.25);"></div>
|
||||
<div class="glow-circle glow-subtle"></div>
|
||||
|
||||
<div class="content">
|
||||
<div class="tag"><span class="dot" style="background: #00D26A;"></span>准时上门 · 售后保障</div>
|
||||
<div class="title">本地便民到家 <span class="highlight">贴心无忧</span></div>
|
||||
<div class="subtitle">家政保洁开荒 · 管道维修家电拆洗 · 随叫随到</div>
|
||||
<div class="action-row">
|
||||
<div class="btn">一键预约 <span class="btn-arrow">→</span></div>
|
||||
<div class="badge-hot"><span class="fire">🎁</span> 首单立减 20元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="visual">
|
||||
<svg viewBox="0 0 440 440" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="220" cy="380" rx="160" ry="24" fill="rgba(0,210,106,0.12)"/>
|
||||
<!-- 温馨小家立体模型 -->
|
||||
<path d="M120 230 L230 140 L340 230 L340 360 L120 360 Z" fill="url(#houseFront)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<!-- 屋顶侧面/立体坡屋顶 -->
|
||||
<path d="M230 140 L340 230 L310 230 L230 160 Z" fill="#00A854"/>
|
||||
<path d="M100 230 L230 120 L360 230" stroke="#00D26A" stroke-width="18" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
<!-- 窗户 / 发光微黄 -->
|
||||
<rect x="150" y="250" width="60" height="60" rx="14" fill="#FEF3C7" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<line x1="180" y1="250" x2="180" y2="310" stroke="#F59E0B" stroke-width="3"/>
|
||||
<line x1="150" y1="280" x2="210" y2="280" stroke="#F59E0B" stroke-width="3"/>
|
||||
<!-- 大门 -->
|
||||
<rect x="245" y="260" width="65" height="100" rx="12" fill="#047857" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<circle cx="295" cy="310" r="6" fill="#FCD34D"/>
|
||||
<!-- 立体维修工具箱与清洁喷雾 -->
|
||||
<g transform="translate(60, 270)">
|
||||
<rect x="0" y="30" width="110" height="75" rx="16" fill="url(#goldGrad)" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<path d="M30 30 C30 12, 80 12, 80 30" stroke="#B45309" stroke-width="8" stroke-linecap="round" fill="none"/>
|
||||
<line x1="0" y1="65" x2="110" y2="65" stroke="#FFFFFF" stroke-width="3"/>
|
||||
<rect x="44" y="60" width="22" height="14" rx="4" fill="#78350F"/>
|
||||
</g>
|
||||
<!-- 绿意小树苗 / 盾牌徽章 -->
|
||||
<g transform="translate(320, 190)">
|
||||
<path d="M40 0 C60 0, 80 15, 80 45 C80 75, 40 95, 40 95 C40 95, 0 75, 0 45 C0 15, 20 0, 40 0 Z" fill="url(#shieldGrad)" stroke="#FFFFFF" stroke-width="4"/>
|
||||
<path d="M26 44 L36 54 L56 34" stroke="#FFFFFF" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<!-- 渐变定义 -->
|
||||
<defs>
|
||||
<linearGradient id="houseFront" x1="120" y1="140" x2="340" y2="360" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FFFFFF"/>
|
||||
<stop offset="0.6" stop-color="#F0FDF4"/>
|
||||
<stop offset="1" stop-color="#DCFCE7"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="shieldGrad" x1="0" y1="0" x2="80" y2="95" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00E070"/>
|
||||
<stop offset="1" stop-color="#059669"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
196
tools/generate_assets.php
普通文件
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
/**
|
||||
* 生成测试用户头像与商家Logo资源
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function generateAvatarsAndLogos() {
|
||||
$avatarDir = __DIR__ . '/../public/uploads/avatars';
|
||||
$merchantDir = __DIR__ . '/../public/uploads/merchants';
|
||||
|
||||
if (!is_dir($avatarDir)) {
|
||||
mkdir($avatarDir, 0755, true);
|
||||
}
|
||||
if (!is_dir($merchantDir)) {
|
||||
mkdir($merchantDir, 0755, true);
|
||||
}
|
||||
|
||||
// 定义清新色盘(Mint Green 主题协调配色:翠绿、薄荷青、暖橙、湖蓝、薰衣草紫、青黛、珊瑚红等)
|
||||
$palette = [
|
||||
'green' => [[0, 210, 106], [0, 168, 84]], // 品牌浅翠绿
|
||||
'mint' => [[16, 185, 129], [5, 150, 105]], // 薄荷青
|
||||
'blue' => [[59, 130, 246], [37, 99, 235]], // 湖蓝
|
||||
'orange' => [[245, 158, 11], [217, 119, 6]], // 阳光金橙
|
||||
'teal' => [[20, 184, 166], [13, 148, 136]], // 青碧
|
||||
'purple' => [[139, 92, 246], [124, 58, 237]], // 浅紫
|
||||
'rose' => [[244, 63, 94], [225, 29, 72]], // 珊瑚粉
|
||||
'slate' => [[100, 116, 139], [71, 85, 105]], // 雅灰
|
||||
];
|
||||
|
||||
/**
|
||||
* 生成带首字徽标的现代化圆形/微圆角PNG头像
|
||||
*
|
||||
* @param string $filePath 输出文件绝对路径
|
||||
* @param string $char 显示的文字(1个汉字或英文)
|
||||
* @param array $theme 配色方案
|
||||
* @param int $size 图片尺寸
|
||||
* @param bool $isSquare 是否为商家微圆角方形
|
||||
* @return void
|
||||
*/
|
||||
$createBadge = function ($filePath, $char, $theme, $size = 128, $isSquare = false) {
|
||||
$img = imagecreatetruecolor($size, $size);
|
||||
imagealphablending($img, false);
|
||||
imagesavealpha($img, true);
|
||||
|
||||
$trans = imagecolorallocatealpha($img, 0, 0, 0, 127);
|
||||
imagefill($img, 0, 0, $trans);
|
||||
|
||||
$c1 = $theme[0];
|
||||
$c2 = $theme[1];
|
||||
|
||||
// 绘制渐变背景
|
||||
for ($y = 0; $y < $size; $y++) {
|
||||
$ratio = $y / $size;
|
||||
$r = (int)($c1[0] * (1 - $ratio) + $c2[0] * $ratio);
|
||||
$g = (int)($c1[1] * (1 - $ratio) + $c2[1] * $ratio);
|
||||
$b = (int)($c1[2] * (1 - $ratio) + $c2[2] * $ratio);
|
||||
$col = imagecolorallocate($img, $r, $g, $b);
|
||||
|
||||
for ($x = 0; $x < $size; $x++) {
|
||||
if ($isSquare) {
|
||||
// 圆角矩形(圆角半径 24)
|
||||
$rad = 24;
|
||||
$in = true;
|
||||
if ($x < $rad && $y < $rad) {
|
||||
$in = (($x - $rad) * ($x - $rad) + ($y - $rad) * ($y - $rad)) <= ($rad * $rad);
|
||||
} elseif ($x >= ($size - $rad) && $y < $rad) {
|
||||
$in = (($x - ($size - $rad)) * ($x - ($size - $rad)) + ($y - $rad) * ($y - $rad)) <= ($rad * $rad);
|
||||
} elseif ($x < $rad && $y >= ($size - $rad)) {
|
||||
$in = (($x - $rad) * ($x - $rad) + ($y - ($size - $rad)) * ($y - ($size - $rad))) <= ($rad * $rad);
|
||||
} elseif ($x >= ($size - $rad) && $y >= ($size - $rad)) {
|
||||
$in = (($x - ($size - $rad)) * ($x - ($size - $rad)) + ($y - ($size - $rad)) * ($y - ($size - $rad))) <= ($rad * $rad);
|
||||
}
|
||||
if ($in) {
|
||||
imagesetpixel($img, $x, $y, $col);
|
||||
}
|
||||
} else {
|
||||
// 圆形头像
|
||||
$cx = $size / 2;
|
||||
$cy = $size / 2;
|
||||
$distSq = ($x - $cx) * ($x - $cx) + ($y - $cy) * ($y - $cy);
|
||||
if ($distSq <= ($size / 2) * ($size / 2)) {
|
||||
imagesetpixel($img, $x, $y, $col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imagealphablending($img, true);
|
||||
|
||||
// 尝试加载系统常见中文字体,若无则使用 imagestring 兜底
|
||||
$fontCandidates = [
|
||||
'C:\\Windows\\Fonts\\msyh.ttc', // 微软雅黑
|
||||
'C:\\Windows\\Fonts\\msyh.ttf',
|
||||
'C:\\Windows\\Fonts\\simhei.ttf', // 黑体
|
||||
'C:\\Windows\\Fonts\\simsun.ttc',
|
||||
];
|
||||
$fontFile = null;
|
||||
foreach ($fontCandidates as $f) {
|
||||
if (file_exists($f)) {
|
||||
$fontFile = $f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$white = imagecolorallocate($img, 255, 255, 255);
|
||||
if ($fontFile) {
|
||||
$fontSize = $isSquare ? 46 : 42;
|
||||
$box = imagettfbbox($fontSize, 0, $fontFile, $char);
|
||||
$tw = $box[2] - $box[0];
|
||||
$th = $box[1] - $box[7];
|
||||
$tx = (int)(($size - $tw) / 2) - $box[0];
|
||||
$ty = (int)(($size - $th) / 2) - $box[7];
|
||||
imagettftext($img, $fontSize, 0, $tx, $ty, $white, $fontFile, $char);
|
||||
} else {
|
||||
$fSize = 5;
|
||||
$tw = imagefontwidth($fSize) * strlen($char);
|
||||
$th = imagefontheight($fSize);
|
||||
$tx = (int)(($size - $tw) / 2);
|
||||
$ty = (int)(($size - $th) / 2);
|
||||
imagestring($img, $fSize, $tx, $ty, $char, $white);
|
||||
}
|
||||
|
||||
imagepng($img, $filePath, 6);
|
||||
imagedestroy($img);
|
||||
};
|
||||
|
||||
// 1. 生成用户头像列表
|
||||
$userAvatars = [
|
||||
'user_1001.png' => ['助', $palette['green']],
|
||||
'user_1002.png' => ['王', $palette['blue']],
|
||||
'user_1003.png' => ['洁', $palette['teal']],
|
||||
'user_1004.png' => ['测', $palette['slate']],
|
||||
'user_2001.png' => ['顺', $palette['orange']],
|
||||
'user_2002.png' => ['安', $palette['green']],
|
||||
'user_2003.png' => ['骑', $palette['rose']],
|
||||
'user_2004.png' => ['车', $palette['blue']],
|
||||
'user_2005.png' => ['净', $palette['teal']],
|
||||
'user_2006.png' => ['豪', $palette['purple']],
|
||||
'user_2007.png' => ['优', $palette['orange']],
|
||||
'user_2008.png' => ['琴', $palette['rose']],
|
||||
'user_2009.png' => ['筑', $palette['green']],
|
||||
'user_2010.png' => ['黔', $palette['blue']],
|
||||
'user_2027.png' => ['林', $palette['teal']],
|
||||
'user_2028.png' => ['超', $palette['orange']],
|
||||
'user_default.png' => ['同', $palette['green']],
|
||||
];
|
||||
|
||||
foreach ($userAvatars as $fileName => $cfg) {
|
||||
$path = $avatarDir . '/' . $fileName;
|
||||
$createBadge($path, $cfg[0], $cfg[1], 128, false);
|
||||
}
|
||||
echo "已成功生成 " . count($userAvatars) . " 个用户头像到 public/uploads/avatars/" . PHP_EOL;
|
||||
|
||||
// 2. 生成商家 Logo 列表
|
||||
$merchantLogos = [
|
||||
'm_shunjie.png' => ['顺', $palette['orange']],
|
||||
'm_anjia.png' => ['安', $palette['green']],
|
||||
'm_jiejing.png' => ['洁', $palette['teal']],
|
||||
'm_chehang.png' => ['车', $palette['blue']],
|
||||
'm_youxuan.png' => ['优', $palette['purple']],
|
||||
'm_zaowang.png' => ['枣', $palette['rose']],
|
||||
];
|
||||
|
||||
foreach ($merchantLogos as $fileName => $cfg) {
|
||||
$path = $merchantDir . '/' . $fileName;
|
||||
$createBadge($path, $cfg[0], $cfg[1], 128, true);
|
||||
}
|
||||
echo "已成功生成 " . count($merchantLogos) . " 个商家Logo到 public/uploads/merchants/" . PHP_EOL;
|
||||
|
||||
// 3. 生成商家营业执照模拟图
|
||||
$licenseImg = imagecreatetruecolor(400, 280);
|
||||
$bg = imagecolorallocate($licenseImg, 252, 250, 242); // 羊皮纸色
|
||||
imagefill($licenseImg, 0, 0, $bg);
|
||||
$border = imagecolorallocate($licenseImg, 180, 160, 120);
|
||||
imagerectangle($licenseImg, 10, 10, 389, 269, $border);
|
||||
imagerectangle($licenseImg, 14, 14, 385, 265, $border);
|
||||
$textDark = imagecolorallocate($licenseImg, 50, 50, 50);
|
||||
$red = imagecolorallocate($licenseImg, 220, 38, 38);
|
||||
|
||||
$fontFile = 'C:\\Windows\\Fonts\\msyh.ttc';
|
||||
if (file_exists($fontFile)) {
|
||||
imagettftext($licenseImg, 18, 0, 140, 60, $textDark, $fontFile, '营业执照');
|
||||
imagettftext($licenseImg, 10, 0, 40, 100, $textDark, $fontFile, '统一社会信用代码:91520102MA6XYZ8888');
|
||||
imagettftext($licenseImg, 10, 0, 40, 130, $textDark, $fontFile, '类型:有限责任公司(自然人独资)');
|
||||
imagettftext($licenseImg, 10, 0, 40, 160, $textDark, $fontFile, '登记机关:贵阳市市场监督管理局');
|
||||
imagettftext($licenseImg, 10, 0, 40, 190, $textDark, $fontFile, '经营状态:开业(官方核验通过)');
|
||||
// 模拟红色印章
|
||||
imageellipse($licenseImg, 320, 200, 60, 60, $red);
|
||||
imagettftext($licenseImg, 9, 0, 298, 204, $red, $fontFile, '官方核验');
|
||||
}
|
||||
imagepng($licenseImg, $merchantDir . '/license_default.png', 6);
|
||||
imagedestroy($licenseImg);
|
||||
echo "已成功生成营业执照模拟图到 public/uploads/merchants/license_default.png" . PHP_EOL;
|
||||
}
|
||||
|
||||
generateAvatarsAndLogos();
|
||||
428
tools/repair_database_data.php
普通文件
@@ -0,0 +1,428 @@
|
||||
<?php
|
||||
/**
|
||||
* 数据库测试数据完整性综合修复脚本
|
||||
*
|
||||
* 覆盖:
|
||||
* 1. fa_user 用户表:补充头像、昵称、手机号、个性签名、性别等
|
||||
* 2. fa_fc_user_ext 扩展表:补全身份类型、实名状态、真实姓名
|
||||
* 3. fa_fc_merchant 商家表:补齐缺失商家(车行天下、洁净到家分部),补齐现有商家的 logo、banner、公告、地址、坐标、类目、执照等
|
||||
* 4. fa_fc_post 帖子表:纠正商家帖的 identity_type 为 2,补全空缺联系人、电话、微信、地址与坐标
|
||||
* 5. 同步所有计数(商家 post_count、分类 post_count、评论数等)
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function repairDatabaseData() {
|
||||
$pdo = new PDO("mysql:host=127.0.0.1;port=3307;dbname=fenleixinxi;charset=utf8mb4", "fenleixinxi", "fenleixinxi");
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$now = time();
|
||||
|
||||
echo "==================================================" . PHP_EOL;
|
||||
echo "步骤 1: 修复与补全 fa_user 用户表测试数据" . PHP_EOL;
|
||||
echo "==================================================" . PHP_EOL;
|
||||
|
||||
// 用户定义列表:[id, username, nickname, mobile, avatar, gender, bio]
|
||||
$usersConfig = [
|
||||
1001 => ['fc1001', '同城小助手', '13800001111', '/uploads/avatars/user_1001.png', 1, '同城便民服务与官方信息发布助手'],
|
||||
1002 => ['fc1002', '小王同学', '15033443344', '/uploads/avatars/user_1002.png', 1, '贵阳本地在校大学生,分享生活闲置与学习资讯'],
|
||||
1003 => ['fc1003', '洁净到家·陈姐', '13156785678', '/uploads/avatars/user_1003.png', 2, '10年家政保洁经验,诚信服务千家万户'],
|
||||
1004 => ['fc1004', '测试用户A', '13000000000', '/uploads/avatars/user_1004.png', 1, '同城测试体验账号'],
|
||||
2001 => ['fc2001', '顺捷物流·杨经理', '13988888888', '/uploads/avatars/user_2001.png', 1, '顺捷同城物流调度中心,高效货运日结保障'],
|
||||
2002 => ['fc2002', '安家房产·周主管', '13566666666', '/uploads/avatars/user_2002.png', 1, '安家房产资深经纪人,真实房源无中介费陷阱'],
|
||||
2003 => ['fc2003', '骑行爱好者·小周', '13712341234', '/uploads/avatars/user_2003.png', 1, '周末黔灵山与花溪十里河滩常客,爱骑行爱运动'],
|
||||
2004 => ['fc2004', '车行天下·张总', '13800000000', '/uploads/avatars/user_2004.png', 1, '专注高品质二手车买卖与全车深度质检'],
|
||||
2005 => ['fc2005', '洁净到家·王师傅', '13156785678', '/uploads/avatars/user_2005.png', 1, '深度家电清洗、管道开荒,专业设备上门'],
|
||||
2006 => ['fc2006', '数码达人·阿豪', '15033443344', '/uploads/avatars/user_2006.png', 1, '自用数码产品转让,品质如新,支持面交验机'],
|
||||
2007 => ['fc2007', '优选二手车·赵经理', '13700000000', '/uploads/avatars/user_2007.png', 1, '品质二手私家车,无事故支持第三方检测'],
|
||||
2008 => ['wx1a5fb068c3', '琴海飞扬', '13618512779', '/uploads/avatars/user_2008.png', 2, '热爱音乐与摄影,发现贵阳同城美好'],
|
||||
2009 => ['wx5d0e3c36ba', '筑城漫步', '13785104643', '/uploads/avatars/user_2009.png', 2, '闲置物品断舍离,环保慢生活'],
|
||||
2010 => ['wxc7bb372973', '黔中书虫', '13885106507', '/uploads/avatars/user_2010.png', 1, '书香满屋,同城爱书人常交流'],
|
||||
2011 => ['wx40cf7d49bf', '枣王府·王鹏', '18885795042', '/uploads/merchants/m_zaowang.png', 1, '南明区地道餐饮老店,味美价实'],
|
||||
2012 => ['wxedc3a78f3a', '黔山行者', '13985106138', '/uploads/avatars/user_default.png', 1, '周末爬山徒步搭子'],
|
||||
2025 => ['wxef1d83d35a', '灵秀筑城', '13685106579', '/uploads/avatars/user_default.png', 2, '贵阳本地美食与生活分享'],
|
||||
2026 => ['wx0e2dfdf782', '贵山小友', '13785109432', '/uploads/avatars/user_default.png', 1, '摄影搭子与同城骑行'],
|
||||
2027 => ['u18386351589', '林晓峰', '18386351589', '/uploads/avatars/user_2027.png', 1, '数码数车爱好者,同城真实发布'],
|
||||
2028 => ['wx568d124539', '机车阿超', '18386351589', '/uploads/avatars/user_2028.png', 1, '春风800nk车主,机车骑行交流'],
|
||||
];
|
||||
|
||||
$stmtUpdateUser = $pdo->prepare("UPDATE fa_user SET nickname = :nickname, mobile = :mobile, avatar = :avatar, gender = :gender, bio = :bio, updatetime = :updatetime WHERE id = :id");
|
||||
|
||||
foreach ($usersConfig as $uid => $cfg) {
|
||||
$chk = $pdo->query("SELECT id FROM fa_user WHERE id = $uid")->fetch();
|
||||
if ($chk) {
|
||||
$stmtUpdateUser->execute([
|
||||
':nickname' => $cfg[1],
|
||||
':mobile' => $cfg[2],
|
||||
':avatar' => $cfg[3],
|
||||
':gender' => $cfg[4],
|
||||
':bio' => $cfg[5],
|
||||
':updatetime' => $now,
|
||||
':id' => $uid,
|
||||
]);
|
||||
echo "已完善用户 ID {$uid} ({$cfg[1]})" . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
// 针对其它遗留未设头像的普通用户,设置默认头像兜底
|
||||
$pdo->query("UPDATE fa_user SET avatar = '/uploads/avatars/user_default.png' WHERE avatar IS NULL OR avatar = ''");
|
||||
echo "已为其余未设头像的用户填充默认头像。" . PHP_EOL;
|
||||
|
||||
echo PHP_EOL . "==================================================" . PHP_EOL;
|
||||
echo "步骤 2: 修复与补全 fa_fc_user_ext 用户扩展表数据" . PHP_EOL;
|
||||
echo "==================================================" . PHP_EOL;
|
||||
|
||||
// 扩展表配置:user_id => [user_type (1个人 2商家), realname, is_member]
|
||||
$extConfig = [
|
||||
1001 => [1, '同城小助手', 1],
|
||||
1002 => [1, '王梓轩', 0],
|
||||
1003 => [2, '陈秀英', 1],
|
||||
1004 => [1, '测试员', 0],
|
||||
2001 => [2, '杨顺捷', 1],
|
||||
2002 => [2, '周安家', 1],
|
||||
2003 => [1, '周逸尘', 0],
|
||||
2004 => [2, '张车行', 1],
|
||||
2005 => [2, '王保洁', 1],
|
||||
2006 => [1, '黄子豪', 0],
|
||||
2007 => [2, '赵优选', 1],
|
||||
2008 => [1, '琴飞飞', 0],
|
||||
2009 => [1, '漫小筑', 0],
|
||||
2010 => [1, '黔晓书', 0],
|
||||
2011 => [2, '王鹏', 1],
|
||||
2027 => [1, '林晓峰', 0],
|
||||
2028 => [1, '王超', 1],
|
||||
];
|
||||
|
||||
foreach ($extConfig as $uid => $cfg) {
|
||||
$ext = $pdo->query("SELECT id FROM fa_fc_user_ext WHERE user_id = $uid")->fetch();
|
||||
if ($ext) {
|
||||
$stmt = $pdo->prepare("UPDATE fa_fc_user_ext SET user_type = :user_type, realname = :realname, realname_status = 1, is_member = :is_member, updatetime = :now WHERE user_id = :uid");
|
||||
$stmt->execute([
|
||||
':user_type' => $cfg[0],
|
||||
':realname' => $cfg[1],
|
||||
':is_member' => $cfg[2],
|
||||
':now' => $now,
|
||||
':uid' => $uid,
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO fa_fc_user_ext (user_id, user_type, realname, realname_status, is_member, post_quota_left, post_quota_expire, createtime, updatetime) VALUES (:uid, :user_type, :realname, 1, :is_member, 10, :expire, :now, :now)");
|
||||
$stmt->execute([
|
||||
':uid' => $uid,
|
||||
':user_type' => $cfg[0],
|
||||
':realname' => $cfg[1],
|
||||
':is_member' => $cfg[2],
|
||||
':expire' => $now + 86400 * 30,
|
||||
':now' => $now,
|
||||
]);
|
||||
}
|
||||
echo "已同步扩展资料 user_id: {$uid} (身份: " . ($cfg[0] === 2 ? '商家' : '个人') . ", 实名: {$cfg[1]})" . PHP_EOL;
|
||||
}
|
||||
|
||||
echo PHP_EOL . "==================================================" . PHP_EOL;
|
||||
echo "步骤 3: 修复与补全 fa_fc_merchant 商家表档案数据" . PHP_EOL;
|
||||
echo "==================================================" . PHP_EOL;
|
||||
|
||||
// 商家详细配置列表
|
||||
$merchantsConfig = [
|
||||
2001 => [
|
||||
'name' => '顺捷同城物流园',
|
||||
'logo' => '/uploads/merchants/m_shunjie.png',
|
||||
'banner' => '/static/posts/job-warehouse.jpg',
|
||||
'notice' => '专业同城配送、日结货运搬家、冷链中转,全天候随叫随到,高效保质!',
|
||||
'recommend' => 1,
|
||||
'contact' => '杨经理 13988888888',
|
||||
'address' => '贵州省贵阳市南明区富源南路108号顺捷物流园A区',
|
||||
'lat' => 26.542100,
|
||||
'lng' => 106.732100,
|
||||
'category' => '同城货运',
|
||||
'license' => '/uploads/merchants/license_default.png',
|
||||
'shop_status' => 'normal',
|
||||
'audit_status'=> 1,
|
||||
],
|
||||
2002 => [
|
||||
'name' => '安家同城房产置业',
|
||||
'logo' => '/uploads/merchants/m_anjia.png',
|
||||
'banner' => '/static/posts/house-exterior.jpg',
|
||||
'notice' => '贵阳全城真实房源、精装整租/合租、二手房免佣买卖,资深经纪人1对1带看。',
|
||||
'recommend' => 1,
|
||||
'contact' => '周主管 13566666666',
|
||||
'address' => '贵州省贵阳市云岩区北京路128号安家房产置业中心',
|
||||
'lat' => 26.598200,
|
||||
'lng' => 106.708300,
|
||||
'category' => '房产中介',
|
||||
'license' => '/uploads/merchants/license_default.png',
|
||||
'shop_status' => 'normal',
|
||||
'audit_status'=> 1,
|
||||
],
|
||||
2005 => [
|
||||
'name' => '洁净到家品质家政',
|
||||
'logo' => '/uploads/merchants/m_jiejing.png',
|
||||
'banner' => '/static/posts/clean-window.jpg',
|
||||
'notice' => '专业家庭日常保洁、新房开荒保洁、油烟机家电深度拆洗、月嫂育儿,持证上岗。',
|
||||
'recommend' => 1,
|
||||
'contact' => '王师傅 13156785678',
|
||||
'address' => '贵州省贵阳市观山湖区林城东路88号洁净生活馆',
|
||||
'lat' => 26.631500,
|
||||
'lng' => 106.632800,
|
||||
'category' => '家政保洁',
|
||||
'license' => '/uploads/merchants/license_default.png',
|
||||
'shop_status' => 'normal',
|
||||
'audit_status'=> 1,
|
||||
],
|
||||
1003 => [
|
||||
'name' => '洁净到家·云岩服务中心',
|
||||
'logo' => '/uploads/merchants/m_jiejing.png',
|
||||
'banner' => '/static/posts/clean-bath.jpg',
|
||||
'notice' => '云岩老城区快速响应保洁与钟点服务,价格透明,满意后再付款。',
|
||||
'recommend' => 1,
|
||||
'contact' => '陈姐 13156785678',
|
||||
'address' => '贵州省贵阳市云岩区中华北路56号',
|
||||
'lat' => 26.589100,
|
||||
'lng' => 106.713500,
|
||||
'category' => '家政保洁',
|
||||
'license' => '/uploads/merchants/license_default.png',
|
||||
'shop_status' => 'normal',
|
||||
'audit_status'=> 1,
|
||||
],
|
||||
2004 => [
|
||||
'name' => '车行天下名车汇',
|
||||
'logo' => '/uploads/merchants/m_chehang.png',
|
||||
'banner' => '/static/posts/car-suv-white.jpg',
|
||||
'notice' => '精选准新二手私家车、第三方268项专业检测,支持分期置换,一年两万公里质保。',
|
||||
'recommend' => 1,
|
||||
'contact' => '张总 13800000000',
|
||||
'address' => '贵州省贵阳市花溪区花溪大道南段668号车行天下展厅',
|
||||
'lat' => 26.438200,
|
||||
'lng' => 106.678900,
|
||||
'category' => '汽车交易',
|
||||
'license' => '/uploads/merchants/license_default.png',
|
||||
'shop_status' => 'normal',
|
||||
'audit_status'=> 1,
|
||||
],
|
||||
2007 => [
|
||||
'name' => '优选二手车精品馆',
|
||||
'logo' => '/uploads/merchants/m_youxuan.png',
|
||||
'banner' => '/static/posts/car-sedan-white.jpg',
|
||||
'notice' => '专注中高端精品一手二手车,杜绝事故车、泡水车,提供全国联保与免费过户。',
|
||||
'recommend' => 1,
|
||||
'contact' => '赵经理 13700000000',
|
||||
'address' => '贵州省贵阳市观山湖区金阳北路218号优选名车馆',
|
||||
'lat' => 26.619800,
|
||||
'lng' => 106.621200,
|
||||
'category' => '汽车交易',
|
||||
'license' => '/uploads/merchants/license_default.png',
|
||||
'shop_status' => 'normal',
|
||||
'audit_status'=> 1,
|
||||
],
|
||||
2011 => [
|
||||
'name' => '枣王府地道特色餐饮',
|
||||
'logo' => '/uploads/merchants/m_zaowang.png',
|
||||
'banner' => '/static/posts/job-cafe.jpg',
|
||||
'notice' => '传承贵州传统地道风味,精选特色食材,支持堂食聚会与同城订餐配送。',
|
||||
'recommend' => 1,
|
||||
'contact' => '王鹏 18885795042',
|
||||
'address' => '贵州省贵阳市南明区箭道街52号(南明区人民政府南侧)',
|
||||
'lat' => 26.568220,
|
||||
'lng' => 106.714100,
|
||||
'category' => '美食餐饮',
|
||||
'license' => '/uploads/merchants/license_default.png',
|
||||
'shop_status' => 'normal',
|
||||
'audit_status'=> 1,
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($merchantsConfig as $uid => $m) {
|
||||
$exist = $pdo->query("SELECT id FROM fa_fc_merchant WHERE user_id = $uid")->fetch();
|
||||
if ($exist) {
|
||||
$stmt = $pdo->prepare("UPDATE fa_fc_merchant SET
|
||||
name = :name,
|
||||
logo = :logo,
|
||||
banner = :banner,
|
||||
notice = :notice,
|
||||
recommend = :recommend,
|
||||
contact = :contact,
|
||||
address = :address,
|
||||
lat = :lat,
|
||||
lng = :lng,
|
||||
category = :category,
|
||||
license = :license,
|
||||
shop_status = :shop_status,
|
||||
audit_status = :audit_status,
|
||||
updatetime = :now
|
||||
WHERE id = :id");
|
||||
$stmt->execute([
|
||||
':name' => $m['name'],
|
||||
':logo' => $m['logo'],
|
||||
':banner' => $m['banner'],
|
||||
':notice' => $m['notice'],
|
||||
':recommend' => $m['recommend'],
|
||||
':contact' => $m['contact'],
|
||||
':address' => $m['address'],
|
||||
':lat' => $m['lat'],
|
||||
':lng' => $m['lng'],
|
||||
':category' => $m['category'],
|
||||
':license' => $m['license'],
|
||||
':shop_status' => $m['shop_status'],
|
||||
':audit_status'=> $m['audit_status'],
|
||||
':now' => $now,
|
||||
':id' => $exist['id'],
|
||||
]);
|
||||
echo "已升级更新商家档案: {$m['name']} (ID: {$exist['id']}, user_id: {$uid})" . PHP_EOL;
|
||||
} else {
|
||||
$stmt = $pdo->prepare("INSERT INTO fa_fc_merchant (
|
||||
user_id, name, logo, banner, notice, recommend, contact, address, lat, lng, category, license, audit_status, shop_status, post_count, createtime, updatetime
|
||||
) VALUES (
|
||||
:uid, :name, :logo, :banner, :notice, :recommend, :contact, :address, :lat, :lng, :category, :license, :audit_status, :shop_status, 0, :now, :now
|
||||
)");
|
||||
$stmt->execute([
|
||||
':uid' => $uid,
|
||||
':name' => $m['name'],
|
||||
':logo' => $m['logo'],
|
||||
':banner' => $m['banner'],
|
||||
':notice' => $m['notice'],
|
||||
':recommend' => $m['recommend'],
|
||||
':contact' => $m['contact'],
|
||||
':address' => $m['address'],
|
||||
':lat' => $m['lat'],
|
||||
':lng' => $m['lng'],
|
||||
':category' => $m['category'],
|
||||
':license' => $m['license'],
|
||||
':audit_status'=> $m['audit_status'],
|
||||
':shop_status' => $m['shop_status'],
|
||||
':now' => $now,
|
||||
]);
|
||||
echo "已补全新增商家档案: {$m['name']} (user_id: {$uid})" . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
echo PHP_EOL . "==================================================" . PHP_EOL;
|
||||
echo "步骤 4: 修复与补全 fa_fc_post 帖子信息与身份归属" . PHP_EOL;
|
||||
echo "==================================================" . PHP_EOL;
|
||||
|
||||
// 1. 商家身份列表 (1003, 2001, 2002, 2004, 2005, 2007, 2011)
|
||||
$merchantUserIds = [1003, 2001, 2002, 2004, 2005, 2007, 2011];
|
||||
$inStr = implode(',', $merchantUserIds);
|
||||
|
||||
// 将这些商家账号发布的帖子的 identity_type 修正为 2 (商家)
|
||||
$stmtMerchantPost = $pdo->query("UPDATE fa_fc_post SET identity_type = 2 WHERE user_id IN ($inStr)");
|
||||
$affected = $stmtMerchantPost->rowCount();
|
||||
echo "已纠正 {$affected} 条商家帖子的 identity_type 为 2 (商家身份)。" . PHP_EOL;
|
||||
|
||||
// 其余个人账号 (如 1001, 1002, 2003, 2006, 2008, 2009, 2010, 2027, 2028) 确保为 1 (个人)
|
||||
$stmtPersonPost = $pdo->query("UPDATE fa_fc_post SET identity_type = 1 WHERE user_id NOT IN ($inStr)");
|
||||
echo "已确认个人帖子的 identity_type 为 1。" . PHP_EOL;
|
||||
|
||||
// 2. 检查并补全每条帖子的 contact, phone, wechat, address, lat, lng
|
||||
$posts = $pdo->query("SELECT p.*, u.nickname, u.mobile FROM fa_fc_post p LEFT JOIN fa_user u ON p.user_id = u.id WHERE p.deletetime IS NULL")->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$areaDefaultCoord = [
|
||||
'南明区' => [26.568, 106.716, '贵阳市南明区花果园大街'],
|
||||
'云岩区' => [26.605, 106.710, '贵阳市云岩区北京路128号'],
|
||||
'观山湖区' => [26.625, 106.625, '贵阳市观山湖区金融城金阳北路'],
|
||||
'花溪区' => [26.430, 106.670, '贵阳市花溪区花溪大道南段'],
|
||||
];
|
||||
|
||||
$stmtFixPost = $pdo->prepare("UPDATE fa_fc_post SET
|
||||
contact = :contact,
|
||||
phone = :phone,
|
||||
wechat = :wechat,
|
||||
address = :address,
|
||||
lat = :lat,
|
||||
lng = :lng,
|
||||
cover = :cover
|
||||
WHERE id = :id");
|
||||
|
||||
$fixedPostCount = 0;
|
||||
foreach ($posts as $p) {
|
||||
$pid = $p['id'];
|
||||
$needUpdate = false;
|
||||
|
||||
$contact = $p['contact'];
|
||||
$phone = $p['phone'];
|
||||
$wechat = $p['wechat'];
|
||||
$address = $p['address'];
|
||||
$lat = $p['lat'];
|
||||
$lng = $p['lng'];
|
||||
$cover = $p['cover'];
|
||||
|
||||
// 联系人缺失时按昵称或角色填充
|
||||
if (empty($contact) || $contact === '测试') {
|
||||
$contact = !empty($p['nickname']) ? $p['nickname'] : '同城房主';
|
||||
$needUpdate = true;
|
||||
}
|
||||
|
||||
// 电话缺失时用用户手机号
|
||||
if (empty($phone) || $phone === '13000000000') {
|
||||
$phone = !empty($p['mobile']) ? $p['mobile'] : '13885108888';
|
||||
$needUpdate = true;
|
||||
}
|
||||
|
||||
// 微信缺失时填充微信号
|
||||
if (empty($wechat)) {
|
||||
$wechat = 'gyfc' . $pid;
|
||||
$needUpdate = true;
|
||||
}
|
||||
|
||||
// 地址缺失时填充所在区域代表地址
|
||||
$areaKey = !empty($p['area']) ? $p['area'] : '云岩区';
|
||||
$def = isset($areaDefaultCoord[$areaKey]) ? $areaDefaultCoord[$areaKey] : $areaDefaultCoord['云岩区'];
|
||||
|
||||
if (empty($address)) {
|
||||
$address = $def[2];
|
||||
$needUpdate = true;
|
||||
}
|
||||
|
||||
// 坐标为空时补充默认坐标并带有少许偏移
|
||||
if (empty($lat) || empty($lng) || (float)$lat == 0.0) {
|
||||
$offset = (($pid % 7) - 3) * 0.004;
|
||||
$lat = round($def[0] + $offset, 6);
|
||||
$lng = round($def[1] + $offset, 6);
|
||||
$needUpdate = true;
|
||||
}
|
||||
|
||||
// 封面缺失时兜底
|
||||
if (empty($cover)) {
|
||||
$cover = '/static/posts/job-warehouse.jpg';
|
||||
$needUpdate = true;
|
||||
}
|
||||
|
||||
if ($needUpdate) {
|
||||
$stmtFixPost->execute([
|
||||
':contact' => $contact,
|
||||
':phone' => $phone,
|
||||
':wechat' => $wechat,
|
||||
':address' => $address,
|
||||
':lat' => $lat,
|
||||
':lng' => $lng,
|
||||
':cover' => $cover,
|
||||
':id' => $pid,
|
||||
]);
|
||||
$fixedPostCount++;
|
||||
}
|
||||
}
|
||||
echo "已检查并补全 {$fixedPostCount} 条帖子的联系信息、地址与坐标。" . PHP_EOL;
|
||||
|
||||
echo PHP_EOL . "==================================================" . PHP_EOL;
|
||||
echo "步骤 5: 同步商家发帖计数与分类信息计数" . PHP_EOL;
|
||||
echo "==================================================" . PHP_EOL;
|
||||
|
||||
// 1. 同步商家 post_count
|
||||
$merchants = $pdo->query("SELECT id, user_id FROM fa_fc_merchant")->fetchAll(PDO::FETCH_ASSOC);
|
||||
foreach ($merchants as $m) {
|
||||
$c = (int)$pdo->query("SELECT COUNT(*) FROM fa_fc_post WHERE user_id = {$m['user_id']} AND status = 1 AND audit_status = 1 AND deletetime IS NULL")->fetchColumn();
|
||||
$pdo->query("UPDATE fa_fc_merchant SET post_count = {$c} WHERE id = {$m['id']}");
|
||||
echo "商家 ID {$m['id']} (user_id={$m['user_id']}) 在招/在售有效信息量: {$c}" . PHP_EOL;
|
||||
}
|
||||
|
||||
// 2. 同步分类 post_count
|
||||
for ($cid = 1; $cid <= 8; $cid++) {
|
||||
$cc = (int)$pdo->query("SELECT COUNT(*) FROM fa_fc_post WHERE category_id = {$cid} AND status = 1 AND audit_status = 1 AND deletetime IS NULL")->fetchColumn();
|
||||
$pdo->query("UPDATE fa_fc_category SET post_count = {$cc} WHERE id = {$cid}");
|
||||
}
|
||||
echo "已成功同步所有 8 个主分类的有效发帖总数。" . PHP_EOL;
|
||||
|
||||
echo PHP_EOL . "==================================================" . PHP_EOL;
|
||||
echo "数据库测试数据修复完毕!" . PHP_EOL;
|
||||
echo "==================================================" . PHP_EOL;
|
||||
}
|
||||
|
||||
repairDatabaseData();
|
||||
61
tools/verify_api_data.php
普通文件
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* 验证 Post 与 Merchant API 返回结构完整性
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function verifyApiOutputs() {
|
||||
$pdo = new PDO("mysql:host=127.0.0.1;port=3307;dbname=fenleixinxi;charset=utf8mb4", "fenleixinxi", "fenleixinxi");
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
echo "=== 1. 验证商家帖子详情 (如 ID: 101 - 顺捷同城物流园) ===" . PHP_EOL;
|
||||
$post101 = $pdo->query("SELECT * FROM fa_fc_post WHERE id = 101")->fetch(PDO::FETCH_ASSOC);
|
||||
$user101 = $pdo->query("SELECT * FROM fa_user WHERE id = {$post101['user_id']}")->fetch(PDO::FETCH_ASSOC);
|
||||
$merchant101 = $pdo->query("SELECT * FROM fa_fc_merchant WHERE user_id = {$post101['user_id']}")->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
echo "标题: {$post101['title']}" . PHP_EOL;
|
||||
echo "identity_type: {$post101['identity_type']} (应为2)" . PHP_EOL;
|
||||
echo "发布人: {$user101['nickname']}, 头像: {$user101['avatar']}" . PHP_EOL;
|
||||
echo "关联商家: {$merchant101['name']}" . PHP_EOL;
|
||||
echo " - logo: {$merchant101['logo']}" . PHP_EOL;
|
||||
echo " - banner: {$merchant101['banner']}" . PHP_EOL;
|
||||
echo " - address: {$merchant101['address']}" . PHP_EOL;
|
||||
echo " - contact: {$merchant101['contact']}" . PHP_EOL;
|
||||
echo " - category: {$merchant101['category']}" . PHP_EOL;
|
||||
echo " - notice: {$merchant101['notice']}" . PHP_EOL;
|
||||
echo " - post_count: {$merchant101['post_count']}" . PHP_EOL;
|
||||
|
||||
echo PHP_EOL . "=== 2. 验证个人帖子详情 (如 ID: 102 - 小王同学) ===" . PHP_EOL;
|
||||
$post102 = $pdo->query("SELECT * FROM fa_fc_post WHERE id = 102")->fetch(PDO::FETCH_ASSOC);
|
||||
$user102 = $pdo->query("SELECT * FROM fa_user WHERE id = {$post102['user_id']}")->fetch(PDO::FETCH_ASSOC);
|
||||
echo "标题: {$post102['title']}" . PHP_EOL;
|
||||
echo "identity_type: {$post102['identity_type']} (应为1)" . PHP_EOL;
|
||||
echo "发布人: {$user102['nickname']}, 头像: {$user102['avatar']}" . PHP_EOL;
|
||||
echo "联系人: {$post102['contact']}, 手机: {$post102['phone']}, 微信: {$post102['wechat']}" . PHP_EOL;
|
||||
|
||||
echo PHP_EOL . "=== 3. 验证所有商家的档案完整性 ===" . PHP_EOL;
|
||||
$merchants = $pdo->query("SELECT * FROM fa_fc_merchant")->fetchAll(PDO::FETCH_ASSOC);
|
||||
$allComplete = true;
|
||||
foreach ($merchants as $m) {
|
||||
$missing = [];
|
||||
if (empty($m['logo'])) $missing[] = 'logo';
|
||||
if (empty($m['banner'])) $missing[] = 'banner';
|
||||
if (empty($m['address'])) $missing[] = 'address';
|
||||
if (empty($m['lat']) || empty($m['lng'])) $missing[] = 'lat/lng';
|
||||
if (empty($m['contact'])) $missing[] = 'contact';
|
||||
if (empty($m['category'])) $missing[] = 'category';
|
||||
if (empty($m['notice'])) $missing[] = 'notice';
|
||||
|
||||
if (!empty($missing)) {
|
||||
echo "商家 {$m['name']} 缺失字段: " . implode(', ', $missing) . PHP_EOL;
|
||||
$allComplete = false;
|
||||
} else {
|
||||
echo "✓ 商家 [{$m['id']}] {$m['name']} 数据完整 (有效发帖: {$m['post_count']})" . PHP_EOL;
|
||||
}
|
||||
}
|
||||
if ($allComplete) {
|
||||
echo ">>> 全部商家数据 100% 完整!<<<" . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
verifyApiOutputs();
|
||||
@@ -53,8 +53,10 @@ view,
|
||||
text,
|
||||
scroll-view,
|
||||
swiper,
|
||||
image {
|
||||
image,
|
||||
button {
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
/* 通用容器 */
|
||||
@@ -129,6 +131,7 @@ image {
|
||||
}
|
||||
|
||||
.fc-safe-bottom {
|
||||
height: constant(safe-area-inset-bottom);
|
||||
height: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,46 @@
|
||||
import { get, post } from '../utils/request.js'
|
||||
|
||||
export const messageApi = {
|
||||
/** 消息列表(type: system / interact) */
|
||||
/**
|
||||
* 消息列表
|
||||
* @param {Object} params { type?: string, page?: number, size?: number }
|
||||
* @returns {Promise<Object>} 分页列表数据
|
||||
*/
|
||||
list: (params = {}) => get('/message/list', params, { silent: true }),
|
||||
/** 未读消息数(首页角标) → { count } */
|
||||
|
||||
/**
|
||||
* 未读消息数(首页角标)
|
||||
* @returns {Promise<Object>} { count: number }
|
||||
*/
|
||||
unreadCount: () => get('/message/unread-count', {}, { silent: true }),
|
||||
/** 标记已读 */
|
||||
|
||||
/**
|
||||
* 标记单条已读
|
||||
* @param {number|string} id 消息ID
|
||||
* @returns {Promise<Object>} { read: boolean }
|
||||
*/
|
||||
read: (id) => post(`/message/${id}/read`, {}, { silent: true }),
|
||||
|
||||
/**
|
||||
* 一键全部已读
|
||||
* @param {Object} params { type?: string }
|
||||
* @returns {Promise<Object>} { success: boolean, affected: number }
|
||||
*/
|
||||
readAll: (params = {}) => post('/message/read-all', params),
|
||||
|
||||
/**
|
||||
* 一键清理消息
|
||||
* @param {Object} params { type?: string }
|
||||
* @returns {Promise<Object>} { success: boolean, affected: number }
|
||||
*/
|
||||
clear: (params = {}) => post('/message/clear', params),
|
||||
|
||||
/**
|
||||
* 删除单条消息
|
||||
* @param {number|string} id 消息ID
|
||||
* @returns {Promise<Object>} { success: boolean }
|
||||
*/
|
||||
delete: (id) => post(`/message/${id}/delete`, {}),
|
||||
}
|
||||
|
||||
export default messageApi
|
||||
|
||||
@@ -14,9 +14,9 @@ export const BANNER_ART = ['banner-art-1.png', 'banner-art-2.png', 'banner-art-3
|
||||
|
||||
/** 内置生态绿渐变(1/2/3 号,取自 uni.scss 主色家族),后台未配置颜色时按位置取用 */
|
||||
export const BANNER_BG = [
|
||||
'linear-gradient(105deg, #00e070 0%, #00c860 42%, #046b3b 100%)',
|
||||
'linear-gradient(105deg, #00d96c 0%, #00b85c 40%, #023a21 100%)',
|
||||
'linear-gradient(105deg, #00e878 0%, #049e52 45%, #045c34 100%)',
|
||||
'linear-gradient(105deg, #00d26a 0%, #10b981 50%, #34d399 100%)',
|
||||
'linear-gradient(105deg, #10b981 0%, #34d399 50%, #6ee7b7 100%)',
|
||||
'linear-gradient(105deg, #059669 0%, #10b981 50%, #34d399 100%)',
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,9 +24,9 @@ export const datasets = {
|
||||
|
||||
/** 首页轮播广告位(color 为生态绿家族渐变,image 为空时前端用「绿底 + 白色插画」内置配图) */
|
||||
banners: [
|
||||
{ id: 1, title: '同城好物节 · 二手直降', image: '', link: '/pages/category/channel?categoryId=3', color: 'linear-gradient(105deg, #00e070 0%, #00c860 42%, #046b3b 100%)' },
|
||||
{ id: 2, title: '招聘旺季 · 免费发布', image: '', link: '/pages/post/create', color: 'linear-gradient(105deg, #00d96c 0%, #00b85c 40%, #023a21 100%)' },
|
||||
{ id: 3, title: '本地服务 · 上门更省心', image: '', link: '/pages/category/channel?categoryId=5', color: 'linear-gradient(105deg, #00e878 0%, #049e52 45%, #045c34 100%)' },
|
||||
{ id: 1, title: '同城好物节 · 二手直降', image: '/static/banners/banner-1.png', link: '/pages/category/channel?categoryId=3', color: 'linear-gradient(105deg, #10b981 0%, #34d399 50%, #a7f3d0 100%)' },
|
||||
{ id: 2, title: '招聘旺季 · 免费发布', image: '/static/banners/banner-2.png', link: '/pages/post/create', color: 'linear-gradient(105deg, #059669 0%, #10b981 50%, #6ee7b7 100%)' },
|
||||
{ id: 3, title: '本地服务 · 上门更省心', image: '/static/banners/banner-3.png', link: '/pages/category/channel?categoryId=5', color: 'linear-gradient(105deg, #00d26a 0%, #34d399 50%, #bbf7d0 100%)' },
|
||||
],
|
||||
|
||||
/** 分类导航 */
|
||||
|
||||
@@ -77,10 +77,9 @@ export default {
|
||||
syncStatusBarStyle() {
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && uni.setNavigationBarColor) {
|
||||
const isLightBg = this.theme === 'white' || this.theme === 'transparent'
|
||||
uni.setNavigationBarColor({
|
||||
frontColor: isLightBg ? '#000000' : '#ffffff',
|
||||
backgroundColor: isLightBg ? '#ffffff' : '#04874a',
|
||||
frontColor: '#000000',
|
||||
backgroundColor: this.theme === 'gradient' ? '#eafbf1' : '#ffffff',
|
||||
})
|
||||
}
|
||||
} catch (e) {}
|
||||
@@ -152,10 +151,10 @@ export default {
|
||||
&--gradient {
|
||||
background: $fc-gradient-hero;
|
||||
.fc-navbar__title-text {
|
||||
color: #ffffff;
|
||||
color: $fc-text;
|
||||
}
|
||||
.fc-navbar__back-icon {
|
||||
color: #ffffff;
|
||||
color: $fc-text;
|
||||
}
|
||||
}
|
||||
&--white {
|
||||
|
||||
@@ -216,9 +216,12 @@ export default {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.fc-post-card__tag--top {
|
||||
background: $fc-gradient-primary;
|
||||
color: $fc-primary-ink;
|
||||
background: linear-gradient(135deg, #00d26a 0%, #00bd5f 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
border-radius: 6rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 210, 106, 0.35);
|
||||
}
|
||||
.fc-post-card__meta {
|
||||
display: flex;
|
||||
|
||||
@@ -5,14 +5,41 @@
|
||||
v-for="(item, idx) in tabs"
|
||||
:key="item.path"
|
||||
class="fc-tabbar__item"
|
||||
:class="{ 'fc-tabbar__item--active': current === idx, 'fc-tabbar__item--primary': item.primary }"
|
||||
:class="{
|
||||
'fc-tabbar__item--active': current === idx,
|
||||
'fc-tabbar__item--primary': item.primary
|
||||
}"
|
||||
hover-class="none"
|
||||
@click="switchTo(item, idx)"
|
||||
>
|
||||
<view class="fc-tabbar__icon" :class="{ 'fc-tabbar__icon--circle': item.primary }">
|
||||
<image v-if="item.primary" class="fc-tabbar__plus" :src="iconUrl('plus-ink')" mode="aspectFit" />
|
||||
<image v-else class="fc-tabbar__img" :src="iconUrl(current === idx ? item.active : item.icon)" mode="aspectFit" />
|
||||
</view>
|
||||
<text class="fc-tabbar__text">{{ item.text }}</text>
|
||||
<!-- 中间突出加号悬浮球(发布) -->
|
||||
<template v-if="item.primary">
|
||||
<view class="fc-tabbar__circle-wrap">
|
||||
<view class="fc-tabbar__circle-btn">
|
||||
<image class="fc-tabbar__plus-icon" :src="iconUrl('plus-white')" mode="aspectFit" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="fc-tabbar__text fc-tabbar__text--primary">{{ item.text }}</text>
|
||||
</template>
|
||||
|
||||
<!-- 普通常驻导航项 -->
|
||||
<template v-else>
|
||||
<view class="fc-tabbar__icon">
|
||||
<image
|
||||
class="fc-tabbar__img"
|
||||
:src="iconUrl(current === idx ? item.active : item.icon)"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<!-- 未读通知红点/数字徽章 -->
|
||||
<view
|
||||
v-if="item.badgeKey === 'message' && unreadCount > 0"
|
||||
class="fc-tabbar__badge"
|
||||
>
|
||||
<text class="fc-tabbar__badge-text">{{ badgeText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="fc-tabbar__text">{{ item.text }}</text>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fc-safe-bottom"></view>
|
||||
@@ -21,6 +48,8 @@
|
||||
|
||||
<script>
|
||||
import { iconUrl } from '@/common/store/ui.js'
|
||||
import messageApi from '@/common/api/message.js'
|
||||
import { useUserStore } from '@/common/store/index.js'
|
||||
|
||||
export default {
|
||||
name: 'FcTabbar',
|
||||
@@ -34,18 +63,59 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
unreadCount: 0,
|
||||
tabs: [
|
||||
{ text: '首页', icon: 'tab-home', active: 'tab-home-on', path: '/pages/index/index' },
|
||||
{ text: '分类', icon: 'tab-category', active: 'tab-category-on', path: '/pages/category/index' },
|
||||
{ text: '发布', icon: 'tab-post', active: 'tab-post-on', path: '/pages/post/create', primary: true },
|
||||
{ text: '通知', icon: 'tab-message', active: 'tab-message-on', path: '/pages/message/index' },
|
||||
{ text: '通知', icon: 'tab-message', active: 'tab-message-on', path: '/pages/message/index', badgeKey: 'message' },
|
||||
{ text: '我的', icon: 'tab-mine', active: 'tab-mine-on', path: '/pages/mine/index' },
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 格式化未读角标文本(超过99显示99+)
|
||||
* @returns {string} 角标数字文本
|
||||
*/
|
||||
badgeText() {
|
||||
if (this.unreadCount > 99) return '99+'
|
||||
return String(this.unreadCount)
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.fetchUnread()
|
||||
uni.$on('refreshUnreadCount', this.fetchUnread)
|
||||
},
|
||||
beforeDestroy() {
|
||||
uni.$off('refreshUnreadCount', this.fetchUnread)
|
||||
},
|
||||
methods: {
|
||||
/** 暴露给模板:图标地址由后台配置决定(未配置则用内置图标) */
|
||||
/**
|
||||
* 暴露给模板:获取对应图标的远程或本地地址
|
||||
* @param {string} key 图标标识键
|
||||
* @returns {string} 完整的图标 URL
|
||||
*/
|
||||
iconUrl,
|
||||
/**
|
||||
* 静默拉取当前用户未读消息总数
|
||||
*/
|
||||
async fetchUnread() {
|
||||
let logged = false
|
||||
try {
|
||||
logged = !!useUserStore().isLogin.value
|
||||
} catch (e) {}
|
||||
if (!logged) {
|
||||
this.unreadCount = 0
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await messageApi.unreadCount()
|
||||
this.unreadCount = Number((res && res.count) || 0)
|
||||
} catch (e) {
|
||||
this.unreadCount = 0
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 切换底部导航目标页面
|
||||
* @param {Object} item 目标 Tab 配置对象
|
||||
@@ -54,13 +124,13 @@ export default {
|
||||
switchTo(item, idx) {
|
||||
if (idx === this.current) return
|
||||
|
||||
// 🌟 中间突出加号(发布):作为独立模态表单页推入,保留前序页面栈,支持原路返回
|
||||
// 中间突出加号(发布):作为独立模态表单页推入,保留前序页面栈,支持原路返回
|
||||
if (item.primary) {
|
||||
uni.navigateTo({ url: item.path })
|
||||
return
|
||||
}
|
||||
|
||||
// 其余常驻 Tab 页面平滑切换:H5 环境优先 redirectTo 避免 reLaunch 导致的页面整体闪屏
|
||||
// 其余常驻 Tab 页面平滑切换:H5 环境优先 redirectTo 避免 reLaunch 闪屏
|
||||
// #ifdef H5
|
||||
uni.redirectTo({
|
||||
url: item.path,
|
||||
@@ -84,70 +154,165 @@ export default {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
background: #ffffff;
|
||||
border-top: 2rpx solid $fc-border;
|
||||
box-shadow: 0 -6rpx 24rpx rgba(4, 48, 30, 0.08);
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
backdrop-filter: blur(20px);
|
||||
border-top: 1rpx solid rgba(0, 210, 106, 0.08);
|
||||
box-shadow: 0 -4rpx 24rpx rgba(0, 50, 25, 0.04);
|
||||
box-sizing: border-box;
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.fc-tabbar__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
height: 104rpx;
|
||||
padding-top: 8rpx;
|
||||
padding-top: 4rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.fc-tabbar__item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-bottom: 8rpx;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease;
|
||||
position: relative;
|
||||
background: transparent !important;
|
||||
outline: none !important;
|
||||
-webkit-tap-highlight-color: transparent !important;
|
||||
|
||||
// 普通导航项点击时仅对图标进行柔和的弹性下压微动效,防止整格形变引起方框闪烁
|
||||
&:active {
|
||||
transform: scale(0.94);
|
||||
.fc-tabbar__img {
|
||||
transform: scale(0.88);
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
.fc-tabbar__text {
|
||||
color: $fc-primary-text;
|
||||
color: #00A854;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
// 发布按钮项:点击时仅触发内部圆形球的物理下压弹性动效与光影收敛
|
||||
&--primary {
|
||||
.fc-tabbar__text {
|
||||
color: $fc-primary-text;
|
||||
&:active {
|
||||
.fc-tabbar__circle-btn {
|
||||
transform: scale(0.91) translateY(4rpx);
|
||||
box-shadow: 0 4rpx 14rpx rgba(0, 210, 106, 0.42);
|
||||
}
|
||||
.fc-tabbar__text--primary {
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 普通导航图标区 */
|
||||
.fc-tabbar__icon {
|
||||
height: 52rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&--circle {
|
||||
width: 82rpx;
|
||||
height: 82rpx;
|
||||
margin-top: -42rpx;
|
||||
border-radius: 50%;
|
||||
background: $fc-gradient-primary;
|
||||
border: 4rpx solid #ffffff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 224, 112, 0.45);
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fc-tabbar__img {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
transition: transform 0.16s cubic-bezier(0.2, 0, 0.2, 1);
|
||||
}
|
||||
.fc-tabbar__plus {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
|
||||
/* 未读消息数字/红点徽章(Badge) */
|
||||
.fc-tabbar__badge {
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
right: -16rpx;
|
||||
z-index: 10;
|
||||
min-width: 28rpx;
|
||||
height: 28rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 14rpx;
|
||||
background: linear-gradient(135deg, #FF5B5B 0%, #FF3333 100%);
|
||||
border: 2rpx solid #ffffff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(255, 51, 51, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
animation: badgePop 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.fc-tabbar__badge-text {
|
||||
font-size: 18rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
@keyframes badgePop {
|
||||
0% {
|
||||
transform: scale(0.4);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 导航文字 */
|
||||
.fc-tabbar__text {
|
||||
font-size: $fc-font-xs;
|
||||
color: $fc-text-secondary;
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
margin-top: 4rpx;
|
||||
line-height: 1.2;
|
||||
transition: color 0.16s ease;
|
||||
|
||||
&--primary {
|
||||
color: #00A854;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
/* 中间突出圆形发布按钮(FAB 浮动发布球) */
|
||||
.fc-tabbar__circle-wrap {
|
||||
position: relative;
|
||||
width: 96rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.fc-tabbar__circle-btn {
|
||||
position: absolute;
|
||||
top: -44rpx;
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #00E070 0%, #00D26A 100%);
|
||||
border: 6rpx solid #ffffff;
|
||||
box-shadow: 0 10rpx 26rpx rgba(0, 210, 106, 0.35), 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.16s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.16s ease;
|
||||
box-sizing: border-box;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.fc-tabbar__plus-icon {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 60, 30, 0.18));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -225,9 +225,9 @@
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "white",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "同城分类信息",
|
||||
"navigationBarBackgroundColor": "#04874a",
|
||||
"navigationBarBackgroundColor": "#eafbf1",
|
||||
"backgroundColor": "#f2fef7"
|
||||
},
|
||||
"condition": {
|
||||
|
||||
@@ -252,17 +252,18 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 未解锁态(普通非会员):极简纯净,无多余口号标签 -->
|
||||
<!-- 未解锁态:会员特权与普通付费分流,权益真实呈现 -->
|
||||
<view v-else class="detail__contact-locked">
|
||||
<view class="detail__locked-row">
|
||||
<text class="detail__contact-key">联系电话</text>
|
||||
<text class="detail__locked-mask">{{ post.phone || post.contact || '188****5042' }}</text>
|
||||
</view>
|
||||
<view class="detail__locked-action-bar">
|
||||
<view class="detail__btn-unlock" @click="openPayModal">
|
||||
<text>解锁完整内容 (¥2)</text>
|
||||
<view class="detail__btn-unlock" :class="{ 'detail__btn-unlock--vip': isVipUser && freeViewLeft > 0 }" @click="handleUnlockClick">
|
||||
<text>{{ unlockBtnText }}</text>
|
||||
</view>
|
||||
<text class="detail__vip-link" @click="goVip">开通会员免费看 ›</text>
|
||||
<text v-if="!isVipUser" class="detail__vip-link" @click="goVip">开通会员免费看 ›</text>
|
||||
<text v-else class="detail__vip-link detail__vip-link--member">今日剩余 {{ freeViewLeft }} 次免费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -328,7 +329,7 @@
|
||||
<text class="detail__bar-text">海报</text>
|
||||
</view>
|
||||
<view class="detail__bar-main" @click="onMainAction">
|
||||
<text>{{ loading && !loaded ? '加载中…' : (unlocked ? '拨打电话' : '解锁完整内容 (¥2)') }}</text>
|
||||
<text>{{ bottomBarText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -343,7 +344,7 @@
|
||||
<text class="detail__cashier-target-title fc-ellipsis">{{ post.title }}</text>
|
||||
<view class="detail__cashier-price-row">
|
||||
<text class="detail__cashier-price-symbol">¥</text>
|
||||
<text class="detail__cashier-price-val">2.00</text>
|
||||
<text class="detail__cashier-price-val">{{ Number(payViewPrice).toFixed(2) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="detail__cashier-methods">
|
||||
@@ -382,7 +383,7 @@
|
||||
<text class="detail__cashier-vip-arrow">去开通 ›</text>
|
||||
</view>
|
||||
<view class="detail__cashier-btn" :class="{ 'detail__cashier-btn--disabled': submitting }" @click="confirmPay">
|
||||
<text>{{ submitting ? '处理中…' : '立即支付 ¥2 解锁' }}</text>
|
||||
<text>{{ submitting ? '处理中…' : `立即支付 ¥${payViewPrice} 解锁` }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -506,6 +507,9 @@ export default {
|
||||
id: 0,
|
||||
post: { publisher: {}, tags: [] },
|
||||
unlocked: false,
|
||||
isVipUser: false,
|
||||
freeViewLeft: 0,
|
||||
payViewPrice: 2,
|
||||
payType: config.defaultPayType || 'wxpay',
|
||||
contact: { name: '', phone: '', wechat: '' },
|
||||
comments: [],
|
||||
@@ -585,6 +589,34 @@ export default {
|
||||
}
|
||||
return this.hasLocation
|
||||
},
|
||||
/**
|
||||
* 联系方式卡片解锁按钮文案
|
||||
* @returns {string}
|
||||
*/
|
||||
unlockBtnText() {
|
||||
if (this.isVipUser && this.freeViewLeft > 0) {
|
||||
return `VIP免费解锁 (剩${this.freeViewLeft}次)`
|
||||
}
|
||||
if (this.isVipUser && this.freeViewLeft === 0) {
|
||||
return `免费额度已用完 (¥${this.payViewPrice}解锁)`
|
||||
}
|
||||
return `解锁完整内容 (¥${this.payViewPrice})`
|
||||
},
|
||||
/**
|
||||
* 底部主操作栏文案
|
||||
* @returns {string}
|
||||
*/
|
||||
bottomBarText() {
|
||||
if (this.loading && !this.loaded) return '加载中…'
|
||||
if (this.unlocked) return '拨打电话'
|
||||
if (this.isVipUser && this.freeViewLeft > 0) {
|
||||
return `VIP免费解锁 (今日剩余${this.freeViewLeft}次)`
|
||||
}
|
||||
if (this.isVipUser && this.freeViewLeft === 0) {
|
||||
return `今日免费已用完 · ¥${this.payViewPrice} 解锁`
|
||||
}
|
||||
return `解锁完整内容 (¥${this.payViewPrice})`
|
||||
},
|
||||
timeText() {
|
||||
return fromNow(this.post.createdAt)
|
||||
},
|
||||
@@ -771,11 +803,16 @@ export default {
|
||||
return false
|
||||
},
|
||||
/**
|
||||
* 回显解锁数据与联系方式
|
||||
* 回显解锁数据与联系方式(支持真实会员特权与商家电话下发)
|
||||
* @param {Object} res 接口返回数据
|
||||
*/
|
||||
applyAccess(res) {
|
||||
this.unlocked = !!(res && res.unlocked)
|
||||
if (res) {
|
||||
if (res.isMember != null) this.isVipUser = !!res.isMember
|
||||
if (res.freeViewLeft != null) this.freeViewLeft = Number(res.freeViewLeft)
|
||||
if (res.payViewPrice != null) this.payViewPrice = Number(res.payViewPrice)
|
||||
}
|
||||
this.contact = this.unlocked
|
||||
? {
|
||||
name: (res && (res.contactName || res.publisher?.name)) || '',
|
||||
@@ -783,6 +820,38 @@ export default {
|
||||
wechat: (res && res.wechat) || '',
|
||||
}
|
||||
: { name: '', phone: '', wechat: '' }
|
||||
|
||||
// 若发帖人有关联商家,解锁态下同步刷新商家实体门店电话
|
||||
if (this.unlocked && res && res.merchant && res.merchant.contact && this.post.merchant) {
|
||||
this.post.merchant.contact = res.merchant.contact
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 点击联系方式解锁按钮(智能区分会员免密免费解锁与非会员收银台)
|
||||
*/
|
||||
handleUnlockClick() {
|
||||
if (!this.ensureLogin()) return
|
||||
if (this.isVipUser && this.freeViewLeft > 0) {
|
||||
this.unlockWithVip()
|
||||
} else {
|
||||
this.openPayModal()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 会员每日免密免费解锁(真实扣减会员当日权益)
|
||||
*/
|
||||
async unlockWithVip() {
|
||||
if (this.submitting) return
|
||||
this.submitting = true
|
||||
try {
|
||||
const res = await postApi.payView(this.id, { payType: 'free' })
|
||||
this.applyAccess(res)
|
||||
uni.showToast({ title: '已使用会员免费权益解锁', icon: 'none' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: e.message || '解锁失败', icon: 'none' })
|
||||
} finally {
|
||||
this.submitting = false
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 页面回显静默刷新解锁权限
|
||||
@@ -818,6 +887,8 @@ export default {
|
||||
if (this.loading && !this.loaded) return
|
||||
if (this.unlocked) {
|
||||
this.callPhone()
|
||||
} else if (this.isVipUser && this.freeViewLeft > 0) {
|
||||
this.unlockWithVip()
|
||||
} else {
|
||||
this.openPayModal()
|
||||
}
|
||||
@@ -2083,6 +2154,9 @@ export default {
|
||||
color: #f5a623;
|
||||
font-weight: 600;
|
||||
}
|
||||
.detail__vip-link--member {
|
||||
color: #00a854;
|
||||
}
|
||||
|
||||
/* 档案与编号 */
|
||||
.detail__archive {
|
||||
|
||||
@@ -747,14 +747,14 @@ export default {
|
||||
flex-direction: column;
|
||||
}
|
||||
.dist__hero-title {
|
||||
color: #ffffff;
|
||||
color: $fc-text;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
.dist__hero-sub {
|
||||
margin-top: 10rpx;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
color: $fc-text-secondary;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<view class="fc-page home">
|
||||
<!-- 顶部:LOGO + 可手动替换定位 + 消息 + 搜索 -->
|
||||
<view class="home__header">
|
||||
<!-- 顶部吸顶导航栏:状态栏 + 定位切换 + 消息通知(向上滚动时常驻吸顶,与胶囊完美对齐) -->
|
||||
<view class="home__nav">
|
||||
<view class="home__header-status" :style="{ height: statusBarHeight + 'px' }"></view>
|
||||
<view
|
||||
class="home__header-bar"
|
||||
@@ -10,18 +10,21 @@
|
||||
minHeight: (navBarHeight || 44) + 'px',
|
||||
}"
|
||||
>
|
||||
<image class="fc-logo fc-logo--sm" :src="logoUrl()" mode="aspectFit" />
|
||||
<!-- 左上角位置:点击弹出位置手动切换面板 -->
|
||||
<view class="home__location" @click="openLocationModal">
|
||||
<image class="home__location-icon" :src="iconUrl('home-location')" mode="aspectFit" />
|
||||
<text class="home__location-text fc-ellipsis">{{ currentCityText }}</text>
|
||||
<text class="home__location-arrow">▾</text>
|
||||
<text class="home__location-text fc-ellipsis" style="color: #1E293B !important; font-weight: 700;">{{ currentCityText }}</text>
|
||||
<text class="home__location-arrow" style="color: #64748B !important;">▾</text>
|
||||
</view>
|
||||
<view class="home__msg" @click="go('/pages/message/index')">
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('home-bell')" mode="aspectFit" />
|
||||
<view v-if="unread > 0" class="home__msg-dot">{{ unread }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 首页 Hero 区域:搜索框 + 渐变底衬衔接 -->
|
||||
<view class="home__header">
|
||||
<view class="home__search" @click="goSearch">
|
||||
<image class="fc-icon fc-icon--sm" :src="iconUrl('home-search')" mode="aspectFit" />
|
||||
<text class="home__search-ph">搜索招聘、租房、二手…</text>
|
||||
@@ -37,48 +40,15 @@
|
||||
class="home__banner"
|
||||
circular
|
||||
autoplay
|
||||
:interval="4000"
|
||||
:interval="4500"
|
||||
:duration="500"
|
||||
indicator-dots
|
||||
indicator-color="rgba(255,255,255,0.45)"
|
||||
indicator-color="rgba(255, 255, 255, 0.45)"
|
||||
indicator-active-color="#ffffff"
|
||||
>
|
||||
<swiper-item v-for="(b, i) in banners" :key="b.id">
|
||||
<view
|
||||
class="home__banner-item"
|
||||
:class="{ 'home__banner-item--custom': !!b.image }"
|
||||
:style="{ background: bannerBg(b, i) }"
|
||||
@click="goBanner(b)"
|
||||
>
|
||||
<!-- 1. 后台已配置自定义海报图:全幅高清展示 -->
|
||||
<image v-if="b.image" class="home__banner-photo" :src="b.image" mode="aspectFill" />
|
||||
|
||||
<!-- 2. 未配置图片:启用内置「春绿渐变底 + 白色徽章插画 + 精选文案」兜底卡片 -->
|
||||
<template v-else>
|
||||
<view class="home__banner-deco">
|
||||
<view class="home__banner-glow"></view>
|
||||
<view class="home__banner-vein"></view>
|
||||
<view class="home__banner-hill home__banner-hill--far"></view>
|
||||
<view class="home__banner-hill home__banner-hill--near"></view>
|
||||
<view class="home__banner-badge">
|
||||
<view class="home__banner-ring home__banner-ring--outer"></view>
|
||||
<view class="home__banner-ring home__banner-ring--inner"></view>
|
||||
<image class="home__banner-art" :src="bannerArtUrl(i)" mode="aspectFit" />
|
||||
<view v-if="bannerHasValue(i)" class="home__banner-value">
|
||||
<view class="home__banner-value-dot"></view>
|
||||
<view class="home__banner-value-dot home__banner-value-dot--sm"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="home__banner-shade"></view>
|
||||
<view class="home__banner-text">
|
||||
<text class="home__banner-tag">同城精选</text>
|
||||
<text class="home__banner-title">{{ b.title }}</text>
|
||||
<view class="home__banner-cta">
|
||||
<text class="home__banner-cta-text">立即查看 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<swiper-item v-for="(b, i) in (banners && banners.length ? banners : defaultBanners)" :key="b.id || i">
|
||||
<view class="home__banner-item" @click="goBanner(b)">
|
||||
<image class="home__banner-photo home__banner-img" :src="bannerImageUrl(b, i)" mode="aspectFill" />
|
||||
</view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
@@ -134,15 +104,6 @@
|
||||
>
|
||||
<text>{{ s.label }}</text>
|
||||
</view>
|
||||
<fc-area-picker
|
||||
class="home__filter-area"
|
||||
:areas="areaRows"
|
||||
:value="areaId"
|
||||
allow-all
|
||||
variant="chip"
|
||||
placeholder="全部区域"
|
||||
@change="onFilterAreaChange"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 列表加载体验:拟真卡片骨架屏(Shimmer 呼吸扫光) -->
|
||||
@@ -293,7 +254,7 @@ import { getNavMetrics } from '@/common/utils/navbar.js'
|
||||
import { groupAreaTree } from '@/common/utils/area.js'
|
||||
|
||||
// 本地离线快照缓存 Key(Stale-While-Revalidate,实现首屏 0ms 秒开)
|
||||
const STORAGE_KEY_BANNERS = 'fc_cache_home_banners'
|
||||
const STORAGE_KEY_BANNERS = 'fc_cache_home_banners_v2'
|
||||
const STORAGE_KEY_CATEGORIES = 'fc_cache_home_categories'
|
||||
const STORAGE_KEY_AREAS = 'fc_cache_home_areas'
|
||||
const STORAGE_KEY_SELECTED_AREA = 'fc_user_selected_area'
|
||||
@@ -340,6 +301,11 @@ export default {
|
||||
location: savedUserArea ? (savedUserArea.city || '') : '',
|
||||
unread: 0,
|
||||
banners: Array.isArray(cachedBanners) ? cachedBanners : [],
|
||||
defaultBanners: [
|
||||
{ id: 1, title: '同城好物节', image: '/static/banners/banner-1.png', link: '/pages/category/channel?categoryId=3' },
|
||||
{ id: 2, title: '同城高薪直招', image: '/static/banners/banner-2.png', link: '/pages/post/create' },
|
||||
{ id: 3, title: '本地便民到家', image: '/static/banners/banner-3.png', link: '/pages/category/channel?categoryId=5' },
|
||||
],
|
||||
categories: Array.isArray(cachedCats) ? cachedCats : [],
|
||||
list: [],
|
||||
page: 1,
|
||||
@@ -471,11 +437,17 @@ export default {
|
||||
|
||||
// 启动并行加载流水线
|
||||
this.bootstrap()
|
||||
uni.$on('refreshUnreadCount', () => {
|
||||
this.loadUnread()
|
||||
})
|
||||
},
|
||||
onShow() {
|
||||
loadUiConfig(true)
|
||||
this.loadUnread()
|
||||
},
|
||||
onUnload() {
|
||||
uni.$off('refreshUnreadCount')
|
||||
},
|
||||
onShareAppMessage() {
|
||||
return {
|
||||
title: uiState.shareTitle || uiState.siteName || '同城分类信息',
|
||||
@@ -506,6 +478,22 @@ export default {
|
||||
bannerArtUrl,
|
||||
bannerHasValue,
|
||||
bannerBg,
|
||||
/**
|
||||
* 轮播图图片地址解析(优先取后台配置的 image,缺省回退到本地规范占位图)
|
||||
* @param {Object} b 轮播数据对象
|
||||
* @param {number} i 索引
|
||||
*/
|
||||
bannerImageUrl(b, i) {
|
||||
if (b && b.image && typeof b.image === 'string' && b.image.trim()) {
|
||||
const img = b.image.trim()
|
||||
if (img.startsWith('http://') || img.startsWith('https://')) return img
|
||||
if (img.startsWith('/static/')) return img
|
||||
if (img.startsWith('/uploads/')) return img
|
||||
return img
|
||||
}
|
||||
const idx = (Math.abs(i) % 3) + 1
|
||||
return `/static/banners/banner-${idx}.png`
|
||||
},
|
||||
/**
|
||||
* 获取分类图标地址
|
||||
* @param {Object} c 分类对象
|
||||
@@ -997,10 +985,19 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 首页生态深绿 Hero 顶栏 */
|
||||
/* 首页吸顶导航栏(状态栏 + 定位/消息胶囊栏向上滚动时固定) */
|
||||
.home__nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
background: #d6f7e5;
|
||||
background: linear-gradient(180deg, #d6f7e5 0%, #dcf8e8 100%);
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 168, 84, 0.05);
|
||||
}
|
||||
.home__header {
|
||||
background: $fc-gradient-hero;
|
||||
padding-bottom: 70rpx;
|
||||
padding: 8rpx 0 70rpx;
|
||||
}
|
||||
.home__header-bar {
|
||||
display: flex;
|
||||
@@ -1012,10 +1009,11 @@ export default {
|
||||
.home__location {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 54rpx;
|
||||
height: 56rpx;
|
||||
padding: 0 18rpx 0 14rpx;
|
||||
background: rgba(255, 255, 255, 0.22);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.4);
|
||||
background: #ffffff;
|
||||
border: 1rpx solid rgba(0, 168, 84, 0.18);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 168, 84, 0.08);
|
||||
border-radius: 999rpx;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@@ -1023,7 +1021,7 @@ export default {
|
||||
&:active {
|
||||
opacity: 0.82;
|
||||
transform: scale(0.97);
|
||||
background: rgba(255, 255, 255, 0.32);
|
||||
background: #f8fafc;
|
||||
}
|
||||
}
|
||||
.home__location-icon {
|
||||
@@ -1034,13 +1032,13 @@ export default {
|
||||
}
|
||||
.home__location-text {
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
color: #1e293b !important; /* 彻底告别白字,高对比现代炭黑 */
|
||||
font-weight: 700;
|
||||
max-width: 210rpx;
|
||||
}
|
||||
.home__location-arrow {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: #64748b !important; /* 中性现代灰 */
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
.home__search {
|
||||
@@ -1083,180 +1081,43 @@ export default {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 轮播广告位 */
|
||||
/* 规范化纯图片轮播卡片(702×280rpx) */
|
||||
.home__banner {
|
||||
height: 300rpx;
|
||||
width: 702rpx;
|
||||
height: 280rpx;
|
||||
margin: -50rpx 24rpx 0;
|
||||
border-radius: $fc-radius-lg;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: $fc-shadow-lg;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 168, 84, 0.08);
|
||||
background: #eafbf1;
|
||||
}
|
||||
.home__banner--skeleton {
|
||||
width: 702rpx;
|
||||
height: 280rpx;
|
||||
margin: -50rpx 24rpx 0;
|
||||
background: #ffffff;
|
||||
padding: 16rpx;
|
||||
border-radius: 24rpx;
|
||||
padding: 12rpx;
|
||||
}
|
||||
.home__banner-sk-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 20rpx;
|
||||
border-radius: 18rpx;
|
||||
}
|
||||
.home__banner-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
padding: 26rpx 0 26rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.home__banner-item--custom {
|
||||
padding: 0;
|
||||
}
|
||||
.home__banner-photo {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
.home__banner-photo,
|
||||
.home__banner-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.home__banner-deco {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.home__banner-glow {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: radial-gradient(circle at 80% 44%, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255, 255, 0.04) 46%, rgba(255, 255, 255, 0) 72%);
|
||||
}
|
||||
.home__banner-vein {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(118deg, rgba(255, 255, 255, 0.04) 0, rgba(255, 255, 255, 0.04) 1rpx, rgba(255, 255, 255, 0) 1rpx, rgba(255, 255, 255, 0) 54rpx);
|
||||
}
|
||||
.home__banner-hill {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.home__banner-hill--far {
|
||||
left: -26%;
|
||||
width: 152%;
|
||||
top: 194rpx;
|
||||
height: 260rpx;
|
||||
background: rgba(4, 48, 42, 0.22);
|
||||
}
|
||||
.home__banner-hill--near {
|
||||
left: -14%;
|
||||
width: 128%;
|
||||
top: 236rpx;
|
||||
height: 240rpx;
|
||||
background: rgba(4, 48, 42, 0.26);
|
||||
}
|
||||
.home__banner-badge {
|
||||
position: absolute;
|
||||
right: 14rpx;
|
||||
top: 50%;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
margin-top: -110rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.home__banner-ring {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.home__banner-ring--outer {
|
||||
width: 218rpx;
|
||||
height: 218rpx;
|
||||
margin: -109rpx 0 0 -109rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.14);
|
||||
}
|
||||
.home__banner-ring--inner {
|
||||
width: 164rpx;
|
||||
height: 164rpx;
|
||||
margin: -82rpx 0 0 -82rpx;
|
||||
border: 2rpx dotted rgba(255, 255, 255, 0.26);
|
||||
}
|
||||
.home__banner-art {
|
||||
width: 116rpx;
|
||||
height: 116rpx;
|
||||
}
|
||||
.home__banner-value {
|
||||
position: absolute;
|
||||
right: 10rpx;
|
||||
top: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.home__banner-value-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: $fc-accent-light;
|
||||
box-shadow: 0 0 16rpx rgba(245, 166, 35, 0.7);
|
||||
}
|
||||
.home__banner-value-dot--sm {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
margin-left: 12rpx;
|
||||
background: $fc-accent;
|
||||
box-shadow: none;
|
||||
}
|
||||
.home__banner-shade {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: linear-gradient(90deg, rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.1) 44%, rgba(255, 255, 255, 0) 70%);
|
||||
}
|
||||
.home__banner-text {
|
||||
position: relative;
|
||||
width: 62%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.home__banner-tag {
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: $fc-radius-pill;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.45);
|
||||
color: $fc-primary-ink;
|
||||
font-size: 22rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.home__banner-title {
|
||||
margin-top: 14rpx;
|
||||
color: $fc-primary-ink;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
line-height: $fc-leading-tight;
|
||||
text-shadow: 0 2rpx 10rpx rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
.home__banner-cta {
|
||||
margin-top: 16rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: $fc-radius-pill;
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
box-shadow: 0 6rpx 18rpx rgba(4, 48, 42, 0.18);
|
||||
}
|
||||
.home__banner-cta-text {
|
||||
color: $fc-primary-text;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
/* 分类金刚区 */
|
||||
|
||||
@@ -512,13 +512,13 @@ export default {
|
||||
.coupon-page__tab-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
color: $fc-text-secondary;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.coupon-page__tab-item--active .coupon-page__tab-text {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
color: $fc-primary-text;
|
||||
}
|
||||
.coupon-page__tab-badge {
|
||||
margin-left: 8rpx;
|
||||
|
||||
@@ -79,14 +79,16 @@
|
||||
</view>
|
||||
|
||||
<view class="vip__privileges-grid">
|
||||
<!-- 特权 1:每日免费看 -->
|
||||
<!-- 特权 1:每日免费看(真实呈现已用与剩余) -->
|
||||
<view class="vip__priv-item">
|
||||
<view class="vip__priv-icon-wrap vip__priv-icon-wrap--green">
|
||||
<image class="vip__priv-icon" :src="iconUrl('eye')" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="vip__priv-content">
|
||||
<text class="vip__priv-name">每日免费看</text>
|
||||
<text class="vip__priv-desc">每日享 {{ benefits.freeView || 20 }} 条免费看</text>
|
||||
<text class="vip__priv-desc">
|
||||
{{ isMember ? `今日已看 ${benefits.freeViewUsed || 0} / 剩 ${benefits.freeViewLeft != null ? benefits.freeViewLeft : benefits.freeView} 次` : `每日享 ${benefits.freeView || 20} 条免费看` }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -101,14 +103,16 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 特权 3:赠送发布额度 -->
|
||||
<!-- 特权 3:赠送发布额度(真实呈现剩余额度) -->
|
||||
<view class="vip__priv-item">
|
||||
<view class="vip__priv-icon-wrap vip__priv-icon-wrap--green">
|
||||
<image class="vip__priv-icon" :src="iconUrl('edit')" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="vip__priv-content">
|
||||
<text class="vip__priv-name">赠送发布额度</text>
|
||||
<text class="vip__priv-desc">当前套餐送 {{ selectedPlanQuota }} 条发布</text>
|
||||
<text class="vip__priv-desc">
|
||||
{{ isMember ? `当前剩余 ${benefits.postQuotaLeft != null ? benefits.postQuotaLeft : 0} 条额度` : `开通套餐送 ${selectedPlanQuota} 条发布` }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -740,12 +744,12 @@ export default {
|
||||
}
|
||||
|
||||
.vip__backdrop {
|
||||
background: $fc-gradient-hero;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 380rpx;
|
||||
background: $fc-gradient-hero;
|
||||
overflow: hidden;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@@ -138,18 +138,20 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
.recharge__hero-label {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
color: $fc-text-secondary;
|
||||
font-weight: 500;
|
||||
font-size: $fc-font-sm;
|
||||
}
|
||||
.recharge__hero-num {
|
||||
margin-top: 16rpx;
|
||||
color: #ffffff;
|
||||
color: $fc-text;
|
||||
letter-spacing: -1rpx;
|
||||
font-size: 72rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.recharge__hero-tip {
|
||||
margin-top: 16rpx;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
color: $fc-text-placeholder;
|
||||
font-size: $fc-font-xs;
|
||||
}
|
||||
.recharge__title {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<view class="msg fc-page">
|
||||
<view class="msg fc-page" @touchstart="onGlobalTouch">
|
||||
<!-- 规范化顶部导航:与首页、分类、我的统一沉浸式渐变顶栏 -->
|
||||
<fc-navbar title="通知中心" theme="gradient" />
|
||||
|
||||
<!-- 分类 tab -->
|
||||
<!-- 分类 Tab -->
|
||||
<view class="msg__tabs">
|
||||
<view
|
||||
v-for="t in tabs"
|
||||
@@ -17,27 +17,74 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作工具条:未读统计 + 一键已读 + 一键清理 -->
|
||||
<view class="msg__toolbar">
|
||||
<view class="msg__stats">
|
||||
<text class="msg__stats-text">共 {{ list.length }} 条</text>
|
||||
<text v-if="unreadCount > 0" class="msg__stats-sep">·</text>
|
||||
<text v-if="unreadCount > 0" class="msg__stats-unread">{{ unreadCount }} 条未读</text>
|
||||
</view>
|
||||
<view class="msg__actions">
|
||||
<view
|
||||
class="msg__btn"
|
||||
:class="{ 'msg__btn--disabled': unreadCount === 0 }"
|
||||
@click="handleReadAll"
|
||||
>
|
||||
<text class="msg__btn-text">一键已读</text>
|
||||
</view>
|
||||
<view
|
||||
class="msg__btn msg__btn--clear"
|
||||
:class="{ 'msg__btn--disabled': !list.length }"
|
||||
@click="handleClearAll"
|
||||
>
|
||||
<text class="msg__btn-text">一键清理</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空数据提示 -->
|
||||
<fc-empty v-if="!loading && !list.length" icon="inbox" text="暂无通知" />
|
||||
|
||||
<!-- 通知卡片列表(支持左滑显示删除按钮) -->
|
||||
<view v-else class="msg__list">
|
||||
<view
|
||||
v-for="m in list"
|
||||
v-for="(m, index) in list"
|
||||
:key="m.id"
|
||||
class="msg__item"
|
||||
:class="{ 'msg__item--unread': !m.read }"
|
||||
@click="openMsg(m)"
|
||||
class="msg__swipe-box"
|
||||
>
|
||||
<view class="msg__icon">
|
||||
<image class="fc-icon--badge-sm" :src="iconUrl(meta(m.type).iconKey)" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="msg__body">
|
||||
<view class="msg__head">
|
||||
<text class="msg__title fc-ellipsis">{{ m.title }}</text>
|
||||
<text class="msg__time">{{ timeText(m.createdAt) }}</text>
|
||||
<!-- 底层动作区:删除按钮 -->
|
||||
<view class="msg__swipe-actions">
|
||||
<view class="msg__delete-btn" @click.stop="handleDelete(m, index)">
|
||||
<text class="msg__delete-text">删除</text>
|
||||
</view>
|
||||
<text class="msg__content fc-ellipsis-2">{{ m.content }}</text>
|
||||
</view>
|
||||
<view v-if="!m.read" class="msg__dot"></view>
|
||||
|
||||
<!-- 表层滑动内容卡片 -->
|
||||
<view
|
||||
class="msg__swipe-content"
|
||||
:class="{
|
||||
'msg__swipe-content--unread': !m.read,
|
||||
'msg__swipe-content--no-transition': m.isSwiping
|
||||
}"
|
||||
:style="{ transform: 'translateX(' + (m.offset || 0) + 'px)' }"
|
||||
@touchstart="onTouchStart($event, m)"
|
||||
@touchmove="onTouchMove($event, m)"
|
||||
@touchend="onTouchEnd($event, m)"
|
||||
@touchcancel="onTouchEnd($event, m)"
|
||||
@click="handleCardClick(m)"
|
||||
>
|
||||
<view class="msg__icon">
|
||||
<image class="fc-icon--badge-sm" :src="iconUrl(meta(m.type).iconKey)" mode="aspectFit" />
|
||||
</view>
|
||||
<view class="msg__body">
|
||||
<view class="msg__head">
|
||||
<text class="msg__title fc-ellipsis">{{ m.title }}</text>
|
||||
<text class="msg__time">{{ timeText(m.createdAt) }}</text>
|
||||
</view>
|
||||
<text class="msg__content fc-ellipsis-2">{{ m.content }}</text>
|
||||
</view>
|
||||
<view v-if="!m.read" class="msg__dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -67,11 +114,42 @@ export default {
|
||||
{ label: '系统通知', value: 'system' },
|
||||
{ label: '互动提醒', value: 'interact' },
|
||||
],
|
||||
// 手势滑动追踪状态
|
||||
touchState: {
|
||||
startX: 0,
|
||||
startY: 0,
|
||||
startTime: 0,
|
||||
direction: '',
|
||||
initOffset: 0,
|
||||
activeId: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* 计算当前列表中的未读通知总数
|
||||
* @returns {number} 未读数量
|
||||
*/
|
||||
unreadCount() {
|
||||
return this.list.filter(m => !m.read).length
|
||||
},
|
||||
/**
|
||||
* 当前选中的分类名称
|
||||
* @returns {string} 分类标签名
|
||||
*/
|
||||
currentTabLabel() {
|
||||
const found = this.tabs.find(t => t.value === this.type)
|
||||
return found ? found.label : '通知'
|
||||
},
|
||||
},
|
||||
onLoad() {
|
||||
this.fetch()
|
||||
},
|
||||
onPullDownRefresh() {
|
||||
this.fetch().finally(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
/** 暴露给模板:获取图标完整地址 */
|
||||
iconUrl,
|
||||
@@ -91,14 +169,36 @@ export default {
|
||||
timeText(t) {
|
||||
return fromNow(t)
|
||||
},
|
||||
/**
|
||||
* 获取左滑操作区最大展开宽度(px)
|
||||
* @returns {number} 展开距离,设计稿 140rpx 转真实 px
|
||||
*/
|
||||
getMaxSwipePx() {
|
||||
try {
|
||||
return uni.upx2px(140)
|
||||
} catch (e) {
|
||||
return 70
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 全局触摸事件:当点击其它位置时关闭所有已滑开的卡片
|
||||
*/
|
||||
onGlobalTouch() {
|
||||
// 若有滑开的卡片且当前非手势拖拽中,不做强制重置,由具体区域判定收起
|
||||
},
|
||||
/**
|
||||
* 拉取当前类型的通知列表
|
||||
*/
|
||||
async fetch() {
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await messageApi.list({ type: this.type, page: 1, size: 20 })
|
||||
this.list = res.list || []
|
||||
const res = await messageApi.list({ type: this.type, page: 1, size: 50 })
|
||||
const rows = res.list || []
|
||||
this.list = rows.map(item => ({
|
||||
...item,
|
||||
offset: 0,
|
||||
isSwiping: false,
|
||||
}))
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '通知加载失败', icon: 'none' })
|
||||
} finally {
|
||||
@@ -111,9 +211,231 @@ export default {
|
||||
*/
|
||||
changeType(v) {
|
||||
if (this.type === v) return
|
||||
this.closeAllSwipe()
|
||||
this.type = v
|
||||
this.fetch()
|
||||
},
|
||||
/**
|
||||
* 收起所有已滑开的卡片
|
||||
* @param {number|null} excludeId 排除收起的消息ID(可选)
|
||||
*/
|
||||
closeAllSwipe(excludeId = null) {
|
||||
this.list.forEach(item => {
|
||||
if (item.id !== excludeId && item.offset !== 0) {
|
||||
this.$set(item, 'offset', 0)
|
||||
this.$set(item, 'isSwiping', false)
|
||||
}
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 一键标记全部已读
|
||||
*/
|
||||
async handleReadAll() {
|
||||
if (this.unreadCount === 0) {
|
||||
uni.showToast({ title: '暂无未读通知', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.closeAllSwipe()
|
||||
uni.showLoading({ title: '处理中...', mask: true })
|
||||
try {
|
||||
await messageApi.readAll({ type: this.type })
|
||||
this.list.forEach(m => {
|
||||
m.read = true
|
||||
})
|
||||
uni.$emit('refreshUnreadCount')
|
||||
uni.showToast({ title: '已全部标为已读', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 一键清理当前分类通知
|
||||
*/
|
||||
handleClearAll() {
|
||||
if (!this.list.length) {
|
||||
uni.showToast({ title: '暂无可清理通知', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.closeAllSwipe()
|
||||
uni.showModal({
|
||||
title: '一键清理',
|
||||
content: `确定清空当前${this.currentTabLabel}的全部通知记录吗?`,
|
||||
confirmText: '清空',
|
||||
confirmColor: '#FF4D4F',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
uni.showLoading({ title: '清理中...', mask: true })
|
||||
try {
|
||||
await messageApi.clear({ type: this.type })
|
||||
this.list = []
|
||||
uni.$emit('refreshUnreadCount')
|
||||
uni.showToast({ title: '已清空通知', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '清理失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 卡片触摸开始手势
|
||||
* @param {TouchEvent} e 触摸事件对象
|
||||
* @param {Object} m 当前触摸的通知对象
|
||||
*/
|
||||
onTouchStart(e, m) {
|
||||
if (!e.touches || !e.touches[0]) return
|
||||
const touch = e.touches[0]
|
||||
this.touchState = {
|
||||
startX: touch.clientX,
|
||||
startY: touch.clientY,
|
||||
startTime: Date.now(),
|
||||
direction: '',
|
||||
initOffset: m.offset || 0,
|
||||
activeId: m.id,
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 卡片触摸滑动中手势
|
||||
* @param {TouchEvent} e 触摸事件对象
|
||||
* @param {Object} m 当前触摸的通知对象
|
||||
*/
|
||||
onTouchMove(e, m) {
|
||||
if (!this.touchState.activeId || this.touchState.activeId !== m.id) return
|
||||
if (!e.touches || !e.touches[0]) return
|
||||
const touch = e.touches[0]
|
||||
const deltaX = touch.clientX - this.touchState.startX
|
||||
const deltaY = touch.clientY - this.touchState.startY
|
||||
|
||||
// 滑动方向判定锁:避免干扰页面垂直滚动
|
||||
if (!this.touchState.direction) {
|
||||
if (Math.abs(deltaX) > 6 || Math.abs(deltaY) > 6) {
|
||||
if (Math.abs(deltaX) >= Math.abs(deltaY)) {
|
||||
this.touchState.direction = 'horizontal'
|
||||
// 确定为横向滑动时,收起其他已滑开的卡片
|
||||
this.closeAllSwipe(m.id)
|
||||
} else {
|
||||
this.touchState.direction = 'vertical'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 纵向滚动不阻拦、不处理位移
|
||||
if (this.touchState.direction !== 'horizontal') return
|
||||
|
||||
// 标记当前处于手势拖拽中,关闭 CSS 动效以达到 60fps 灵敏跟手
|
||||
if (!m.isSwiping) {
|
||||
this.$set(m, 'isSwiping', true)
|
||||
}
|
||||
|
||||
const maxSwipePx = this.getMaxSwipePx()
|
||||
let targetOffset = this.touchState.initOffset + deltaX
|
||||
|
||||
// 边界限制:不能向右拉出卡片(最大为 0)
|
||||
if (targetOffset > 0) {
|
||||
targetOffset = 0
|
||||
}
|
||||
// 向左滑动超出最大展开宽度时加阻尼效果
|
||||
if (targetOffset < -maxSwipePx) {
|
||||
const overstep = -maxSwipePx - targetOffset
|
||||
targetOffset = -maxSwipePx - overstep * 0.2
|
||||
}
|
||||
|
||||
this.$set(m, 'offset', targetOffset)
|
||||
},
|
||||
/**
|
||||
* 卡片触摸结束手势
|
||||
* @param {TouchEvent} e 触摸事件对象
|
||||
* @param {Object} m 当前触摸的通知对象
|
||||
*/
|
||||
onTouchEnd(e, m) {
|
||||
if (!this.touchState.activeId || this.touchState.activeId !== m.id) return
|
||||
this.$set(m, 'isSwiping', false)
|
||||
|
||||
const currentOffset = m.offset || 0
|
||||
const maxSwipePx = this.getMaxSwipePx()
|
||||
const moveTime = Date.now() - this.touchState.startTime
|
||||
const movedX = currentOffset - this.touchState.initOffset
|
||||
|
||||
// 判定是否展开:
|
||||
// 1. 快速轻扫手势(时间短且位移达到一定幅度)
|
||||
// 2. 释放位置超过一半阈值
|
||||
let shouldOpen = false
|
||||
if (moveTime < 280 && movedX < -25) {
|
||||
shouldOpen = true
|
||||
} else if (moveTime < 280 && movedX > 25) {
|
||||
shouldOpen = false
|
||||
} else if (currentOffset <= -maxSwipePx / 2) {
|
||||
shouldOpen = true
|
||||
}
|
||||
|
||||
if (shouldOpen) {
|
||||
this.$set(m, 'offset', -maxSwipePx)
|
||||
} else {
|
||||
this.$set(m, 'offset', 0)
|
||||
}
|
||||
|
||||
this.touchState.activeId = null
|
||||
this.touchState.direction = ''
|
||||
},
|
||||
/**
|
||||
* 卡片点击事件:包含已滑开卡片的安全收起判断与详情查看
|
||||
* @param {Object} m 当前点击的通知对象
|
||||
*/
|
||||
handleCardClick(m) {
|
||||
// 若当前卡片处于滑开状态,则先收起卡片,不触发打开详情
|
||||
if (m.offset && m.offset < 0) {
|
||||
this.$set(m, 'offset', 0)
|
||||
return
|
||||
}
|
||||
// 若存在其他卡片处于滑开状态,收起其他卡片并拦截点击
|
||||
let hasOtherOpen = false
|
||||
this.list.forEach(item => {
|
||||
if (item.offset && item.offset < 0) {
|
||||
this.$set(item, 'offset', 0)
|
||||
hasOtherOpen = true
|
||||
}
|
||||
})
|
||||
if (hasOtherOpen) return
|
||||
|
||||
// 正常打开通知详情
|
||||
this.openMsg(m)
|
||||
},
|
||||
/**
|
||||
* 删除单条通知
|
||||
* @param {Object} m 待删除的通知对象
|
||||
* @param {number} index 当前项在列表中的索引
|
||||
*/
|
||||
handleDelete(m, index) {
|
||||
uni.showModal({
|
||||
title: '删除通知',
|
||||
content: '确定要删除此条通知吗?',
|
||||
confirmText: '删除',
|
||||
confirmColor: '#FF4D4F',
|
||||
cancelText: '取消',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) {
|
||||
// 取消则收起滑开的卡片
|
||||
this.$set(m, 'offset', 0)
|
||||
return
|
||||
}
|
||||
uni.showLoading({ title: '删除中...', mask: true })
|
||||
try {
|
||||
await messageApi.delete(m.id)
|
||||
this.list.splice(index, 1)
|
||||
uni.$emit('refreshUnreadCount')
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '删除失败', icon: 'none' })
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 点击查看通知详情并标记已读
|
||||
* @param {Object} m 通知项对象
|
||||
@@ -123,6 +445,7 @@ export default {
|
||||
m.read = true
|
||||
try {
|
||||
await messageApi.read(m.id)
|
||||
uni.$emit('refreshUnreadCount')
|
||||
} catch (e) {}
|
||||
}
|
||||
uni.showModal({
|
||||
@@ -130,7 +453,7 @@ export default {
|
||||
content: m.content || '',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
confirmColor: '#04874a',
|
||||
confirmColor: '#00A854',
|
||||
})
|
||||
},
|
||||
},
|
||||
@@ -138,12 +461,18 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.msg {
|
||||
min-height: 100vh;
|
||||
background: #F2FEF7;
|
||||
}
|
||||
|
||||
/* 分类 Tab */
|
||||
.msg__tabs {
|
||||
display: flex;
|
||||
background: #ffffff;
|
||||
padding: 0 32rpx;
|
||||
border-bottom: 2rpx solid $fc-border;
|
||||
box-shadow: $fc-shadow-sm;
|
||||
border-bottom: 1rpx solid rgba(0, 210, 106, 0.08);
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.02);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
@@ -155,8 +484,8 @@ export default {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: $fc-font;
|
||||
color: $fc-text-secondary;
|
||||
font-size: 30rpx;
|
||||
color: #64748B;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
@@ -173,28 +502,139 @@ export default {
|
||||
border-radius: 4rpx;
|
||||
background: $fc-gradient-primary;
|
||||
}
|
||||
.msg__list {
|
||||
padding: 20rpx 0;
|
||||
|
||||
/* 顶部操作工具条 */
|
||||
.msg__toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 28rpx 12rpx;
|
||||
}
|
||||
.msg__item {
|
||||
.msg__stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 24rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
.msg__stats-sep {
|
||||
margin: 0 8rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.msg__stats-unread {
|
||||
color: $fc-primary-text;
|
||||
font-weight: 600;
|
||||
}
|
||||
.msg__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.msg__btn {
|
||||
padding: 8rpx 22rpx;
|
||||
margin-left: 14rpx;
|
||||
border-radius: 26rpx;
|
||||
background: #ffffff;
|
||||
border: 1rpx solid rgba(0, 210, 106, 0.3);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 210, 106, 0.06);
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
&:active {
|
||||
transform: scale(0.96);
|
||||
background: #E6F9F0;
|
||||
}
|
||||
&--disabled {
|
||||
opacity: 0.45;
|
||||
pointer-events: none;
|
||||
}
|
||||
&--clear {
|
||||
border-color: rgba(100, 116, 139, 0.2);
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.02);
|
||||
.msg__btn-text {
|
||||
color: #64748B;
|
||||
}
|
||||
&:active {
|
||||
background: #F1F5F9;
|
||||
}
|
||||
}
|
||||
}
|
||||
.msg__btn-text {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: $fc-primary-text;
|
||||
}
|
||||
|
||||
/* 消息列表 */
|
||||
.msg__list {
|
||||
padding: 10rpx 0 20rpx;
|
||||
}
|
||||
|
||||
/* 左滑卡片外层容器 */
|
||||
.msg__swipe-box {
|
||||
position: relative;
|
||||
margin: 0 24rpx 20rpx;
|
||||
padding: 24rpx;
|
||||
background: #ffffff;
|
||||
border-radius: $fc-radius-lg;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 80, 40, 0.04);
|
||||
}
|
||||
|
||||
/* 底层操作区:删除按钮 */
|
||||
.msg__swipe-actions {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 140rpx;
|
||||
background: linear-gradient(135deg, #FF6262, #FF3D3D);
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
box-shadow: $fc-shadow-sm;
|
||||
border: 1rpx solid rgba(4, 48, 30, 0.05);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.msg__delete-btn {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
&:active {
|
||||
transform: scale(0.985);
|
||||
}
|
||||
&--unread {
|
||||
background: #f7fffb;
|
||||
border-left: 6rpx solid $fc-primary;
|
||||
opacity: 0.88;
|
||||
}
|
||||
}
|
||||
.msg__delete-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
/* 表层卡片内容 */
|
||||
.msg__swipe-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 100%;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
background: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(4, 48, 30, 0.05);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
cursor: pointer;
|
||||
transition: transform 0.22s cubic-bezier(0.25, 1, 0.5, 1);
|
||||
&--no-transition {
|
||||
transition: none !important;
|
||||
}
|
||||
&--unread {
|
||||
background: #F8FFF9;
|
||||
border-left: 8rpx solid $fc-primary;
|
||||
}
|
||||
&:active {
|
||||
background-color: #FAFCFA;
|
||||
}
|
||||
}
|
||||
|
||||
.msg__icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
@@ -216,19 +656,19 @@ export default {
|
||||
.msg__title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: $fc-text;
|
||||
color: #1E293B;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.msg__time {
|
||||
font-size: 22rpx;
|
||||
color: $fc-text-placeholder;
|
||||
color: #94A3B8;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.msg__content {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 26rpx;
|
||||
color: $fc-text-regular;
|
||||
color: #334155;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.msg__dot {
|
||||
@@ -241,6 +681,7 @@ export default {
|
||||
background: $fc-primary;
|
||||
box-shadow: 0 0 8rpx rgba(0, 224, 112, 0.6);
|
||||
}
|
||||
|
||||
.msg__tabbar-space {
|
||||
height: 140rpx;
|
||||
}
|
||||
|
||||
@@ -329,6 +329,11 @@ export default {
|
||||
* 页面展示生命周期
|
||||
*/
|
||||
onShow() {
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && uni.setNavigationBarColor) {
|
||||
uni.setNavigationBarColor({ frontColor: '#000000', backgroundColor: '#d6f7e5' })
|
||||
}
|
||||
} catch (e) {}
|
||||
this.fetchData()
|
||||
},
|
||||
|
||||
@@ -587,7 +592,7 @@ export default {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
color: $fc-text;
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5rpx;
|
||||
@@ -639,7 +644,7 @@ export default {
|
||||
}
|
||||
|
||||
.mine__user-name {
|
||||
color: #ffffff;
|
||||
color: $fc-text;
|
||||
font-size: 38rpx;
|
||||
font-weight: 700;
|
||||
max-width: 300rpx;
|
||||
@@ -704,7 +709,7 @@ export default {
|
||||
}
|
||||
|
||||
.mine__user-phone {
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
color: $fc-text-secondary;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
@@ -728,15 +733,16 @@ export default {
|
||||
}
|
||||
|
||||
.mine__asset-num {
|
||||
color: #ffffff;
|
||||
font-size: 38rpx;
|
||||
color: $fc-text;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.mine__asset-label {
|
||||
margin-top: 8rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 22rpx;
|
||||
color: $fc-text-secondary;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ==================== 会员卡片 ==================== */
|
||||
@@ -774,24 +780,26 @@ export default {
|
||||
}
|
||||
|
||||
.mine__vip-title {
|
||||
color: #ffffff;
|
||||
color: #4a2600; /* 深褐黑金字 */
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.mine__vip-sub {
|
||||
margin-top: 4rpx;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
color: #7a4600; /* 高清晰深暖金副标题 */
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mine__vip-btn {
|
||||
padding: 10rpx 26rpx;
|
||||
border-radius: $fc-radius-pill;
|
||||
background: #ffffff;
|
||||
color: #8f5800;
|
||||
color: #4a2600;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 4rpx 12rpx rgba(74, 38, 0, 0.12);
|
||||
}
|
||||
|
||||
/* ==================== 卡片容器 ==================== */
|
||||
@@ -893,8 +901,8 @@ export default {
|
||||
margin-right: 6rpx;
|
||||
|
||||
&--white {
|
||||
border-top-color: rgba(255, 255, 255, 0.7);
|
||||
border-right-color: rgba(255, 255, 255, 0.7);
|
||||
border-top-color: $fc-text-secondary;
|
||||
border-right-color: $fc-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1384,7 +1384,8 @@ export default {
|
||||
|
||||
&--approved {
|
||||
background: #f5a623;
|
||||
color: #ffffff;
|
||||
color: #4a2600;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
&--unauth {
|
||||
|
||||
@@ -169,7 +169,7 @@ export default {
|
||||
this.statusBarHeight = m.statusBarHeight
|
||||
try {
|
||||
if (typeof uni !== 'undefined' && uni.setNavigationBarColor) {
|
||||
uni.setNavigationBarColor({ frontColor: '#ffffff', backgroundColor: '#04874a' })
|
||||
uni.setNavigationBarColor({ frontColor: '#000000', backgroundColor: '#eafbf1' })
|
||||
}
|
||||
} catch (e) {}
|
||||
this.capsulePadding = m.capsulePadding
|
||||
|
||||
|
之前 宽度: | 高度: | 大小: 168 KiB 之后 宽度: | 高度: | 大小: 40 KiB |
|
之前 宽度: | 高度: | 大小: 170 KiB 之后 宽度: | 高度: | 大小: 37 KiB |
|
之前 宽度: | 高度: | 大小: 171 KiB 之后 宽度: | 高度: | 大小: 35 KiB |
|
之前 宽度: | 高度: | 大小: 2.5 KiB 之后 宽度: | 高度: | 大小: 5.7 KiB |
|
之前 宽度: | 高度: | 大小: 1.6 KiB 之后 宽度: | 高度: | 大小: 4.3 KiB |
|
之前 宽度: | 高度: | 大小: 2.0 KiB 之后 宽度: | 高度: | 大小: 4.1 KiB |
|
之后 宽度: | 高度: | 大小: 477 KiB |
|
之后 宽度: | 高度: | 大小: 456 KiB |
|
之后 宽度: | 高度: | 大小: 468 KiB |
|
之前 宽度: | 高度: | 大小: 11 KiB 之后 宽度: | 高度: | 大小: 9.9 KiB |
|
之前 宽度: | 高度: | 大小: 11 KiB 之后 宽度: | 高度: | 大小: 10 KiB |
|
之前 宽度: | 高度: | 大小: 9.7 KiB 之后 宽度: | 高度: | 大小: 9.0 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 9.8 KiB 之后 宽度: | 高度: | 大小: 9.2 KiB |
|
之前 宽度: | 高度: | 大小: 9.6 KiB 之后 宽度: | 高度: | 大小: 9.2 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 1.3 KiB 之后 宽度: | 高度: | 大小: 1.2 KiB |
|
之前 宽度: | 高度: | 大小: 1.9 KiB 之后 宽度: | 高度: | 大小: 1.8 KiB |
|
之前 宽度: | 高度: | 大小: 822 B 之后 宽度: | 高度: | 大小: 3.0 KiB |
|
之前 宽度: | 高度: | 大小: 2.8 KiB 之后 宽度: | 高度: | 大小: 2.5 KiB |
|
之前 宽度: | 高度: | 大小: 9.1 KiB 之后 宽度: | 高度: | 大小: 8.6 KiB |
|
之前 宽度: | 高度: | 大小: 837 B 之后 宽度: | 高度: | 大小: 3.0 KiB |
|
之前 宽度: | 高度: | 大小: 9.2 KiB 之后 宽度: | 高度: | 大小: 8.7 KiB |
|
之前 宽度: | 高度: | 大小: 11 KiB 之后 宽度: | 高度: | 大小: 9.7 KiB |
|
之前 宽度: | 高度: | 大小: 8.6 KiB 之后 宽度: | 高度: | 大小: 8.2 KiB |
|
之前 宽度: | 高度: | 大小: 9.8 KiB 之后 宽度: | 高度: | 大小: 8.7 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.6 KiB |
|
之前 宽度: | 高度: | 大小: 10 KiB 之后 宽度: | 高度: | 大小: 9.5 KiB |
|
之前 宽度: | 高度: | 大小: 9.5 KiB 之后 宽度: | 高度: | 大小: 9.2 KiB |
|
之前 宽度: | 高度: | 大小: 9.7 KiB 之后 宽度: | 高度: | 大小: 9.1 KiB |
|
之前 宽度: | 高度: | 大小: 3.3 KiB 之后 宽度: | 高度: | 大小: 2.8 KiB |