diff --git a/docs/sql/production_remaining_20260803.sql b/docs/sql/production_remaining_20260803.sql
new file mode 100644
index 00000000..baa856e4
--- /dev/null
+++ b/docs/sql/production_remaining_20260803.sql
@@ -0,0 +1,120 @@
+-- 生产库增量 SQL(2026-08-03)
+-- 已排除:社区菜单、帖子分类及其权限 SQL。
+-- 本脚本可重复执行,不会删除既有表或数据。
+
+-- ============================================================
+-- 1. 社区帖子推荐排序字段与索引
+-- ============================================================
+SET @has_post_sort = (
+ SELECT COUNT(*)
+ FROM `information_schema`.`columns`
+ WHERE `table_schema` = DATABASE()
+ AND `table_name` = 'la_community_post'
+ AND `column_name` = 'sort'
+);
+SET @sql = IF(
+ @has_post_sort = 0,
+ 'ALTER TABLE `la_community_post` ADD COLUMN `sort` int unsigned NOT NULL DEFAULT 0 COMMENT ''推荐流排序值,越大越靠前'' AFTER `is_topic`',
+ 'SELECT ''la_community_post.sort already exists'''
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @has_recommend_sort_index = (
+ SELECT COUNT(*)
+ FROM `information_schema`.`statistics`
+ WHERE `table_schema` = DATABASE()
+ AND `table_name` = 'la_community_post'
+ AND `index_name` = 'idx_status_recommend_sort'
+);
+SET @sql = IF(
+ @has_recommend_sort_index = 0,
+ 'ALTER TABLE `la_community_post` ADD INDEX `idx_status_recommend_sort` (`status`, `is_recommend`, `sort`, `create_time`)',
+ 'SELECT ''la_community_post.idx_status_recommend_sort already exists'''
+);
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ============================================================
+-- 2. 联赛热门配置字段
+-- ============================================================
+SET @schema_name = DATABASE();
+SET @has_is_hot = (
+ SELECT COUNT(*)
+ FROM `information_schema`.`COLUMNS`
+ WHERE `TABLE_SCHEMA` = @schema_name
+ AND `TABLE_NAME` = 'la_league'
+ AND `COLUMN_NAME` = 'is_hot'
+);
+SET @alter_sql = IF(
+ @has_is_hot = 0,
+ 'ALTER TABLE `la_league` ADD COLUMN `is_hot` tinyint unsigned NOT NULL DEFAULT 0 COMMENT ''是否热门联赛: 0=否 1=是'' AFTER `is_show`',
+ 'SELECT ''la_league.is_hot already exists'''
+);
+PREPARE alter_stmt FROM @alter_sql;
+EXECUTE alter_stmt;
+DEALLOCATE PREPARE alter_stmt;
+
+-- ============================================================
+-- 3. 加密行情自选表及行情配置
+-- ============================================================
+CREATE TABLE IF NOT EXISTS `la_crypto_favorite` (
+ `id` int UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键',
+ `user_id` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户ID',
+ `symbol` varchar(20) NOT NULL DEFAULT '' COMMENT '币种代码',
+ `status` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '自选状态 0-取消 1-自选',
+ `create_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间',
+ `update_time` int UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间',
+ `delete_time` int NULL DEFAULT NULL COMMENT '删除时间',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `uk_user_symbol` (`user_id`, `symbol`),
+ KEY `idx_user_status` (`user_id`, `status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户加密行情自选表';
+
+INSERT INTO `la_config` (`type`, `name`, `value`, `create_time`, `update_time`)
+SELECT 'crypto_market', 'api_base', 'https://api.coingecko.com/api/v3', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `la_config` WHERE `type` = 'crypto_market' AND `name` = 'api_base'
+);
+
+INSERT INTO `la_config` (`type`, `name`, `value`, `create_time`, `update_time`)
+SELECT 'crypto_market', 'cache_seconds', '60', UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `la_config` WHERE `type` = 'crypto_market' AND `name` = 'cache_seconds'
+);
+
+SET @crypto_market_coins = JSON_ARRAY(
+ JSON_OBJECT('id', 'bitcoin', 'symbol', 'BTC', 'name_zh', '比特币'),
+ JSON_OBJECT('id', 'ethereum', 'symbol', 'ETH', 'name_zh', '以太坊'),
+ JSON_OBJECT('id', 'binancecoin', 'symbol', 'BNB', 'name_zh', '币安币'),
+ JSON_OBJECT('id', 'solana', 'symbol', 'SOL', 'name_zh', 'Solana'),
+ JSON_OBJECT('id', 'ripple', 'symbol', 'XRP', 'name_zh', '瑞波币'),
+ JSON_OBJECT('id', 'cardano', 'symbol', 'ADA', 'name_zh', '艾达币'),
+ JSON_OBJECT('id', 'dogecoin', 'symbol', 'DOGE', 'name_zh', '狗狗币'),
+ JSON_OBJECT('id', 'tron', 'symbol', 'TRX', 'name_zh', '波场'),
+ JSON_OBJECT('id', 'avalanche-2', 'symbol', 'AVAX', 'name_zh', 'Avalanche'),
+ JSON_OBJECT('id', 'chainlink', 'symbol', 'LINK', 'name_zh', 'Chainlink'),
+ JSON_OBJECT('id', 'the-open-network', 'symbol', 'TON', 'name_zh', 'Toncoin'),
+ JSON_OBJECT('id', 'shiba-inu', 'symbol', 'SHIB', 'name_zh', '柴犬币'),
+ JSON_OBJECT('id', 'polkadot', 'symbol', 'DOT', 'name_zh', '波卡币'),
+ JSON_OBJECT('id', 'sui', 'symbol', 'SUI', 'name_zh', 'Sui'),
+ JSON_OBJECT('id', 'near', 'symbol', 'NEAR', 'name_zh', 'NEAR'),
+ JSON_OBJECT('id', 'litecoin', 'symbol', 'LTC', 'name_zh', '莱特币'),
+ JSON_OBJECT('id', 'bitcoin-cash', 'symbol', 'BCH', 'name_zh', '比特币现金'),
+ JSON_OBJECT('id', 'uniswap', 'symbol', 'UNI', 'name_zh', 'Uniswap'),
+ JSON_OBJECT('id', 'aave', 'symbol', 'AAVE', 'name_zh', 'Aave'),
+ JSON_OBJECT('id', 'ethereum-classic', 'symbol', 'ETC', 'name_zh', '以太坊经典')
+);
+
+INSERT INTO `la_config` (`type`, `name`, `value`, `create_time`, `update_time`)
+SELECT 'crypto_market', 'coins', CAST(@crypto_market_coins AS CHAR), UNIX_TIMESTAMP(), UNIX_TIMESTAMP()
+WHERE NOT EXISTS (
+ SELECT 1 FROM `la_config` WHERE `type` = 'crypto_market' AND `name` = 'coins'
+);
+
+UPDATE `la_decorate_tabbar`
+SET `name` = '行情', `update_time` = UNIX_TIMESTAMP()
+WHERE `link` LIKE '%pages%crypto%crypto%'
+ AND `name` <> '行情';
diff --git a/docs/设计稿映射.md b/docs/设计稿映射.md
index 77ecfc9c..17bb62f1 100644
--- a/docs/设计稿映射.md
+++ b/docs/设计稿映射.md
@@ -24,7 +24,7 @@
| --- | --- | --- | --- |
| 积分中心 | `uniapp/src/packages/pages/points_center/points_center.vue` | ✅ 已完成 | 2026-08-03 按设计稿实现积分卡片、快捷入口、最近明细、积分规则与底部明细入口;复用真实用户积分与积分流水接口 |
| 加密行情 | `uniapp/src/pages/crypto/crypto.vue` | ✅ 已完成 | 2026-08-02 按设计稿重构行情导航、市场概览、美元行情列表和币种 Logo;新增账号级跨设备自选、星标收藏与搜索;金额统一使用 `$` 前缀及 `K / M / B / T` 缩写;底部 Tab 文案统一为“行情”,430×932 H5 页面已复核 |
-| 赛事中心 | `uniapp/src/pages/match/match.vue` | 🚧 部分完成 | 2026-07-11 赛程、直播、赔率改为在赛事中心当前内容区域切换;2026-08-01 `/packages_match/pages/worldcup` 作为赛事 Tab 入口时默认展示赛事主页;2026-08-02 按新版设计稿重构居中标题、三段导航、日期选择、运动/联赛筛选、赛事分组与赛程卡片;“热门联赛”改为读取管理端联赛配置,不再作为世界杯专题入口;2026-08-03 其“全部”筛选改由接口返回热门联赛赛事,避免日期与本地二次筛选导致空列表,待 H5 页面验收 |
+| 赛事中心 | `uniapp/src/pages/match/match.vue` | 🚧 部分完成 | 2026-07-11 赛程、直播、赔率改为在赛事中心当前内容区域切换;2026-08-01 `/packages_match/pages/worldcup` 作为赛事 Tab 入口时默认展示赛事主页;2026-08-02 按新版设计稿重构居中标题、三段导航、日期选择、运动/联赛筛选、赛事分组与赛程卡片;“热门联赛”改为读取管理端联赛配置,不再作为世界杯专题入口;2026-08-03 其“全部”筛选改由接口返回热门联赛赛事,避免日期与本地二次筛选导致空列表;日期筛选扩展为 7 天并支持横向滑动,待 H5 页面验收 |
| 赛事详情页 | `uniapp/src/pages/match_detail/match_detail.vue` | 🚧 部分完成 | 2026-05-29 优化顶部赛事摘要头部,补充联赛、对阵、状态与直播入口信息;2026-08-03 顶部改为设计稿的沉浸式直播大图与比分区,详情接口可按已绑定关系或直播标题中的主客队名称匹配房间,多房间可横向切换 |
| 世界杯专题页(排名导航) | `uniapp/src/packages_match/pages/worldcup.vue` | 🚧 部分完成 | 2026-05-20 已按设计稿拆分排名导航、积分榜、球员榜组件;2026-08-03 赛程卡会按赛事 ID、主客队名和直播标题匹配直播列表,命中后显示“看直播”入口;后续可继续细化表格视觉与新闻页样式 |
| 首页 | `uniapp/src/pages/index/index.vue` | 🚧 部分完成 | 2026-07-31 已按当前设计图重构为“今日焦点赛程、主推大图、最新资讯”三段式首页;2026-08-02 频道栏改为可横向滑动,频道项固定宽度并统一间距;频道仍复用后台配置分类,待实际 H5 页面验收后确认最终状态 |
diff --git a/server/app/api/controller/LotteryController.php b/server/app/api/controller/LotteryController.php
index f70e3e39..892fc3a4 100644
--- a/server/app/api/controller/LotteryController.php
+++ b/server/app/api/controller/LotteryController.php
@@ -130,28 +130,77 @@ class LotteryController extends BaseApiController
public function drawDetail()
{
$id = $this->request->get('id/d', 0);
- if (empty($id)) {
+ if ($id > 0) {
+ $draw = LotteryDraw::where('id', $id)->where('is_show', 1)->findOrEmpty();
+ if ($draw->isEmpty()) {
+ return $this->fail('记录不存在');
+ }
+
+ $data = $draw->toArray();
+ $data['numbers'] = is_string($data['numbers']) ? json_decode($data['numbers'], true) : $data['numbers'];
+ $data['zodiac'] = is_string($data['zodiac']) ? json_decode($data['zodiac'], true) : $data['zodiac'];
+ $data['elements'] = is_string($data['elements']) ? json_decode($data['elements'], true) : $data['elements'];
+ $data['color'] = is_string($data['color']) ? json_decode($data['color'], true) : $data['color'];
+ $data['prize_info'] = is_string($data['prize_info']) ? json_decode($data['prize_info'], true) : $data['prize_info'];
+
+ // 附带分类信息
+ $category = LotteryCategory::where('id', $data['category_id'])->findOrEmpty();
+ if (!$category->isEmpty()) {
+ $data['category'] = $category->toArray();
+ }
+
+ return $this->data($data);
+ }
+
+ $code = trim((string) $this->request->get('code', ''));
+ $issue = trim((string) $this->request->get('issue', ''));
+ if ($code === '' || $issue === '') {
return $this->fail('参数缺失');
}
- $draw = LotteryDraw::where('id', $id)->where('is_show', 1)->findOrEmpty();
- if ($draw->isEmpty()) {
+ // 新彩票表使用 code + issue 定位开奖记录,兼容开奖列表的跳转参数。
+ $drawResult = LotteryDrawResult::where('code', $code)
+ ->where('issue', $issue)
+ ->findOrEmpty();
+ if ($drawResult->isEmpty()) {
return $this->fail('记录不存在');
}
- $data = $draw->toArray();
- $data['numbers'] = is_string($data['numbers']) ? json_decode($data['numbers'], true) : $data['numbers'];
- $data['zodiac'] = is_string($data['zodiac']) ? json_decode($data['zodiac'], true) : $data['zodiac'];
- $data['elements'] = is_string($data['elements']) ? json_decode($data['elements'], true) : $data['elements'];
- $data['color'] = is_string($data['color']) ? json_decode($data['color'], true) : $data['color'];
- $data['prize_info'] = is_string($data['prize_info']) ? json_decode($data['prize_info'], true) : $data['prize_info'];
-
- // 附带分类信息
- $category = LotteryCategory::where('id', $data['category_id'])->findOrEmpty();
- if (!$category->isEmpty()) {
- $data['category'] = $category->toArray();
+ $data = $drawResult->toArray();
+ $game = LotteryGame::where('code', $code)
+ ->field('code,name,template,icon')
+ ->findOrEmpty();
+ $gameData = $game->isEmpty() ? [] : $game->toArray();
+ $template = (string) ($gameData['template'] ?? '');
+ $rawNumbers = array_values(array_filter(
+ array_map('trim', explode(',', (string) ($data['draw_code'] ?? ''))),
+ static fn($number) => $number !== ''
+ ));
+ $enrichedNumbers = $this->enrichNumbers($rawNumbers, $template);
+ $isHk6 = stripos($template, 'hk6') !== false || stripos($template, 'lhc') !== false;
+ $specialNumber = '';
+ if ($isHk6 && $rawNumbers) {
+ $specialNumber = (string) array_pop($rawNumbers);
}
+ $data['period'] = (string) ($data['issue'] ?? '');
+ $data['draw_date'] = !empty($data['draw_time'])
+ ? date('Y-m-d', strtotime((string) $data['draw_time']))
+ : '';
+ $data['status'] = !empty($data['draw_code']) || (int) ($data['status'] ?? 0) === 1 ? 1 : 0;
+ $data['numbers'] = $rawNumbers;
+ $data['special_number'] = $specialNumber;
+ $data['zodiac'] = $isHk6 ? array_column($enrichedNumbers, 'zodiac') : [];
+ $data['color'] = $isHk6 ? array_column($enrichedNumbers, 'color') : [];
+ $data['elements'] = [];
+ $data['category'] = [
+ 'code' => $code,
+ 'name' => (string) ($gameData['name'] ?? $code),
+ 'icon' => (string) ($gameData['icon'] ?? ''),
+ 'draw_rule' => '',
+ 'draw_time' => (string) ($data['next_time'] ?? ''),
+ ];
+
return $this->data($data);
}
diff --git a/uniapp/src/api/lottery.ts b/uniapp/src/api/lottery.ts
index 7dba9b93..1ea92df8 100644
--- a/uniapp/src/api/lottery.ts
+++ b/uniapp/src/api/lottery.ts
@@ -23,7 +23,7 @@ export function getLotteryDrawList(data: {
/**
* @description 开奖详情
*/
-export function getLotteryDrawDetail(data: { id: number }) {
+export function getLotteryDrawDetail(data: { id?: number; code?: string; issue?: string }) {
return request.get({ url: '/lottery/drawDetail', data }, { withToken: false })
}
diff --git a/uniapp/src/packages_lottery/pages/detail.vue b/uniapp/src/packages_lottery/pages/detail.vue
index 721e66cb..ac022760 100644
--- a/uniapp/src/packages_lottery/pages/detail.vue
+++ b/uniapp/src/packages_lottery/pages/detail.vue
@@ -36,8 +36,8 @@
{{ drawData.zodiac?.[i] || '' }}
- +
-
+ +
+
{{ formatNum(drawData.special_number) }}
@@ -214,11 +214,11 @@ const formatNum = (n: any) => {
return num < 10 ? '0' + num : String(num)
}
-const fetchDetail = async (id: number) => {
+const fetchDetail = async (params: { id?: number; code?: string; issue?: string }) => {
loading.value = true
errorMsg.value = ''
try {
- const data = await getLotteryDrawDetail({ id })
+ const data = await getLotteryDrawDetail(params)
drawData.value = data
} catch (e: any) {
errorMsg.value = e?.msg || '加载失败'
@@ -248,8 +248,12 @@ const goBack = () => {
onLoad((options: any) => {
const id = Number(options.id || 0)
+ const code = String(options.code || '').trim()
+ const issue = String(options.issue || '').trim()
if (id > 0) {
- fetchDetail(id)
+ fetchDetail({ id })
+ } else if (code && issue) {
+ fetchDetail({ code, issue })
} else {
loading.value = false
errorMsg.value = '参数错误'
diff --git a/uniapp/src/packages_match/components/MatchCenterTabsPanel.vue b/uniapp/src/packages_match/components/MatchCenterTabsPanel.vue
index a20d8733..d9d191ce 100644
--- a/uniapp/src/packages_match/components/MatchCenterTabsPanel.vue
+++ b/uniapp/src/packages_match/components/MatchCenterTabsPanel.vue
@@ -16,14 +16,16 @@
‹
-
-
- {{ item.label }}
- {{ item.week }}
+
+
+
+ {{ item.label }}
+ {{ item.week }}
+
-
+
@@ -361,7 +363,7 @@ const getMatchDateKey = (match: any) => {
const dateTabs = computed(() => {
const anchorKey = dateWindowAnchorKey.value || selectedDateKey.value || formatDateKey(new Date())
- return [-1, 0, 1].map((offset) => {
+ return [-3, -2, -1, 0, 1, 2, 3].map((offset) => {
const key = addDateDays(anchorKey, offset)
const date = parseDateKey(key)
const week = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'][date.getDay()]
@@ -896,13 +898,25 @@ onMounted(() => {
&__dates {
flex: 1;
min-width: 0;
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
+ height: 76rpx;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ &__dates-inner {
+ display: inline-flex;
+ align-items: center;
gap: 8rpx;
+ height: 76rpx;
+ min-width: 100%;
+ padding: 0 2rpx;
+ box-sizing: border-box;
}
&__date {
position: relative;
+ width: 112rpx;
+ flex: 0 0 112rpx;
height: 56rpx;
display: flex;
align-items: center;