feat: 优化消息互动中心与首页Banner呈现,同步全量业务数据与高清图标

这个提交包含在:
2026-10-24 11:39:44 +08:00
父节点 0f84426c33
当前提交 d18b597010
共有 109 个文件被更改,包括 3385 次插入 和 526 次删除

查看文件

@@ -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,15 +3,15 @@
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';
@@ -21,26 +21,27 @@ class FcMessage extends Model
// 定义时间戳字段名
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();

查看文件

@@ -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

某些文件未显示,因为此 diff 中更改的文件太多 显示更多