中国象棋小游戏(C版)
|
freeflydom
2025年6月2日 9:25
本文热度 294
|
! 此文仅展示此游戏的最简单版本,可以实现中国象棋双人对战的基本功能。更多功能体验可访问上方链接。
说明: #include<graphics.h> 一个在 C/C++ 中用于图形编程的头文件,主要用于创建和操作图形界面。具有绘制图形、设置颜色、鼠标和键盘时间处理等功能。#include<conio.h> 提供了对控制台输入/输出的简单操作,如字符读取和屏幕刷新。#include<windows.h> 包含了 Windows API 的各种函数和数据结构定义,允许程序直接调用 Windows 操作系统的功能。可用于窗口创建与管理、进程和线程操作、文件操作、系统信息获取等。#include<mmsystem.h> 提供了多媒体功能,包括音频和定时器功能。同时需要链接winmm.lib 库,通过#pragma comment(lib,"winmm.lib") 实现
整体思路- 创建图形窗口,绘制中国象棋棋局图案
- 定义棋子,并在棋局上绘制初始化棋子
- 实现游戏控制功能。即可通过鼠标操作实现棋子的移动以及红黑双方的交换操作
- 添加棋子的走法规则,将军的判定以及胜负判定
- 添加背景音乐、走棋音效等
- 添加红黑双方计时功能
- 打包软件
实现过程创建图形窗口,绘制中国象棋棋局图案。
我们先看此步骤完成后的结果。

只要对graphics.h 有所了解,创建图形窗口并不困难。
我们首先在主函数中直接初始化图形窗口。
#include<stdio.h>
#include<graphics.h>
int main(){
initgraph(800,800);
}
接下来我们需要定义一个绘制游戏的函数。首先我们可以定义一个行数、列数、间隔以及棋盘格子大小
#define INTERVAL 50
#define CHESS_GRID_SIZE 70
#define ROW 10
#define COL 9
接下来绘制棋局的格子
void GameDraw() {
setbkcolor(RGB(252, 215, 162));
cleardevice();
setlinecolor(BLACK);
setlinestyle(PS_SOLID, 2);
setfillcolor(RGB(252, 215, 162));
fillrectangle(INTERVAL - 5, INTERVAL - 5, CHESS_GRID_SIZE * 8 + INTERVAL + 5, CHESS_GRID_SIZE * 9 + INTERVAL + 5);
for (int i = 0;i < 10;i++) {
line(INTERVAL, i * CHESS_GRID_SIZE + INTERVAL, CHESS_GRID_SIZE * 8 + INTERVAL, i * CHESS_GRID_SIZE + INTERVAL);
if (i < 9) {
line(i * CHESS_GRID_SIZE + INTERVAL, INTERVAL, i * CHESS_GRID_SIZE + INTERVAL, 9 * CHESS_GRID_SIZE + INTERVAL);
}
}
}
接着显示“楚河汉界”文字
void GameDraw(){
fillrectangle(INTERVAL, 4 * CHESS_GRID_SIZE + INTERVAL, 8 * CHESS_GRID_SIZE + INTERVAL, 5 * CHESS_GRID_SIZE + INTERVAL);
settextcolor(BLACK);
settextstyle(50, 0, "楷体");
char river[25] = "楚 河 汉 界";
int twidth = textwidth(river);
int theight = textheight(river);
twidth = (8 * CHESS_GRID_SIZE - twidth) / 2;
theight = (CHESS_GRID_SIZE - theight) / 2;
outtextxy(INTERVAL + twidth, 4 * CHESS_GRID_SIZE + theight + INTERVAL, river);
}
最后画米字完成第一步。
void GameDraw(){
line(3 * CHESS_GRID_SIZE + INTERVAL, INTERVAL, 5 * CHESS_GRID_SIZE + INTERVAL, 2 * CHESS_GRID_SIZE + INTERVAL);
line(5 * CHESS_GRID_SIZE + INTERVAL, INTERVAL, 3 * CHESS_GRID_SIZE + INTERVAL, 2 * CHESS_GRID_SIZE + INTERVAL);
line(3 * CHESS_GRID_SIZE + INTERVAL, 7 * CHESS_GRID_SIZE + INTERVAL, 5 * CHESS_GRID_SIZE + INTERVAL, 9 * CHESS_GRID_SIZE + INTERVAL);
line(5 * CHESS_GRID_SIZE + INTERVAL, 7 * CHESS_GRID_SIZE + INTERVAL, 3 * CHESS_GRID_SIZE + INTERVAL, 9 * CHESS_GRID_SIZE + INTERVAL);
}
定义棋子,并在棋局上绘制初始化棋子
我们先看此步骤完成后的结果。

根据中国象棋游戏规则,棋子分为红黑双方,不同棋子有不同的走法和不同的过河标准。
于是我们首先需要定义红黑双方的棋子名称,这里使用两个指向常量的指针数组分别代表红方棋子和黑方棋子。(用指针数组存储每个棋子名称字符串的起始地址,且是只读模式。更灵活高效且易于修改和维护)
const char* redChess[7] = { "車","馬","相","仕","帥","炮","兵" };
const char* blackChess[7] = { "车","马","象","士","将","砲","卒" };
每个棋子的位置由横纵坐标确定,且每个棋子都有名称、坐标、红黑方、是否过河等属性。于是我们定义一个棋子结构体如下:
struct Chess {
char name[4];
int x;
int y;
char type;
bool flag;
}map[ROW][COL];
接下来就需要对结构体进行初始化,在相应的位置放入相应的棋子。
void GameInit() {
for (int i = 0;i < ROW;i++) {
int temp = 0, temp1 = 0, temp2 = 1;
for (int k = 0;k < COL;k++) {
char chessname[4] = "";
char mcolor = 'B';
if (i <= 4) {
if (i == 0) {
if (temp <= 4) temp++;
else {
temp1 = 4 - temp2;
temp2++;
}
sprintf(chessname, "%s", blackChess[temp1]);
temp1++;
}
if (i == 2 && (k == 1 || k == 7)) {
strcpy(chessname, blackChess[5]);
}
if (i == 3 && (k % 2 == 0)) {
strcpy(chessname, blackChess[6]);
}
}
else {
mcolor = 'R';
if (i == 9) {
if (temp <= 4) temp++;
else {
temp1 = 4 - temp2;
temp2++;
}
sprintf(chessname, "%s", redChess[temp1]);
temp1++;
}
if (i == 7 && (k == 1 || k == 7)) {
strcpy(chessname, redChess[5]);
}
if (i == 6 && (k % 2 == 0)) {
strcpy(chessname, redChess[6]);
}
}
map[i][k].type = mcolor;
strcpy(map[i][k].name, chessname);
map[i][k].flag = false;
map[i][k].x = k * CHESS_GRID_SIZE + INTERVAL;
map[i][k].y = i * CHESS_GRID_SIZE + INTERVAL;
}
}
}
最后,我们在绘制图形函数中继续添加关于绘制棋子的代码,同时这里以圆圈代表一个棋子。
void DameDraw(){
settextstyle(40, 0, "楷体");
for (int i = 0;i < ROW;i++) {
for (int k = 0;k < COL;k++) {
if (strcmp(map[i][k].name, "") != 0) {
if (map[i][k].type == 'B') {
settextcolor(BLACK);
setlinecolor(BLACK);
}
else {
settextcolor(RED);
setlinecolor(RED);
}
fillcircle(map[i][k].x, map[i][k].y, 30);
outtextxy(map[i][k].x - 20, map[i][k].y - 20, map[i][k].name);
}
}
}
}
实现游戏控制功能。即可通过鼠标操作实现棋子的移动以及红黑双方的交换操作。
我们先看此步骤完成后的结果。

在实现此功能前我们需要了解#include<windows.h> 头文件。 MOUSEMSG :是该头文件下的一个结构体,有以下成员
成员变量 | 类型 | 描述 |
---|
uMsg | uint | 鼠标消息类型,例如 WM_MOUSEMOVE (鼠标移动)、WM_LBUTTONDOWN (左按下键)、WM_RBUTTONDOWN (右键按下)等。 | x | int | 鼠标事件发生时的 X 坐标。 | y | int | 鼠标事件发生时的 Y 坐标。 | time | uint | 鼠标事件发生时的时间戳。 | dwExtraInfo | uint | 额外信息,通常用于区分相同消息的不同实例。 |
我们首先做出如下定义:
POINT begin = { -1,-1 }, end = { -1,-1 };
MOUSEMSG msg;
bool isRedTurn = true;
接下来实现游戏控制功能。
首先是检测鼠标的点击
if (MouseHit) {
msg = GetMouseMsg();
if (msg.uMsg == WM_LBUTTONDOWN) {
}
}
再转换鼠标坐标为棋盘坐标以及检查点击位置是否合法
int k = (msg.x - INTERVAL + CHESS_GRID_SIZE / 2) / CHESS_GRID_SIZE;
int i = (msg.y - INTERVAL + CHESS_GRID_SIZE / 2) / CHESS_GRID_SIZE;
if (k < 0 || k >= COL || i < 0 || i >= ROW) return;
接下来是第一次点击选择棋子
if (begin.x == -1) {
if ((isRedTurn && map[i][k].type == 'R') || (!isRedTurn && map[i][k].type == 'B')) {
begin.x = k;
begin.y = i;
}
}
最后是第二次点击移动棋子
else {
end.x = k;
end.y = i;
int flagg = 0;
if (strcmp(map[end.y][end.x].name, "") == 0) flagg++;
strcpy(map[end.y][end.x].name, map[begin.y][begin.x].name);
map[end.y][end.x].type = map[begin.y][begin.x].type;
map[end.y][end.x].flag = map[begin.y][begin.x].flag;
strcpy(map[begin.y][begin.x].name, "");
isRedTurn = !isRedTurn;
begin.x = -1;
}
完整的函数如下:
void GameControl() {
if (MouseHit {
msg = GetMouseMsg();
if (msg.uMsg == WM_LBUTTONDOWN) {
int k = (msg.x - INTERVAL + CHESS_GRID_SIZE / 2) / CHESS_GRID_SIZE;
int i = (msg.y - INTERVAL + CHESS_GRID_SIZE / 2) / CHESS_GRID_SIZE;
if (k < 0 || k >= COL || i < 0 || i >= ROW) return;
if (begin.x == -1) {
if ((isRedTurn && map[i][k].type == 'R') || (!isRedTurn && map[i][k].type == 'B')) {
begin.x = k;
begin.y = i;
}
}else {
end.x = k;
end.y = i;
int flagg = 0;
if (strcmp(map[end.y][end.x].name, "") == 0) flagg++;
strcpy(map[end.y][end.x].name, map[begin.y][begin.x].name);
map[end.y][end.x].type = map[begin.y][begin.x].type;
map[end.y][end.x].flag = map[begin.y][begin.x].flag;
strcpy(map[begin.y][begin.x].name, "");
isRedTurn = !isRedTurn;
begin.x = -1;
}
}
}
}
最后,我们继续在绘制函数中添加对棋子的选中效果
void GameDraw(){
if (i == begin.y && k == begin.x) {
setlinecolor(BLUE);
circle(map[i][k].x, map[i][k].y, 30);
circle(map[i][k].x, map[i][k].y, 32);
}
}
添加棋子的走法规则,将军的判定以及胜负判定
这一步是整个象棋游戏的关键步骤。我们需要定义一个检验函数,对每一个棋子进行走法检验。
首先定义该检验函数:
bool CheckMove(int fromI, int fromK, int toI, int toK) { }
第一步需要规定禁止吃己方棋子:
这个不能实现,只需要当判断目标位置存在棋子且该棋子的阵营属性和移动的棋子相同,则禁止移动即可。
struct Chess fromChess = map[fromI][fromK];
struct Chess toChess = map[toI][toK];
if (toChess.type == fromChess.type && strcmp(toChess.name, "") != 0)
return false;
第二步检验车的走法 - 车的走法就是必须横向或纵向移动(
fromI == toI 或 fromK == toK ) - 路径上所经过的格子都必须为空即可。
if (strcmp(fromChess.name, "车") == 0 || strcmp(fromChess.name, "車") == 0) {
if (fromI != toI && fromK != toK) return false;
int step = (fromI == toI) ? (toK > fromK ? 1 : -1) : (toI > fromI ? 1 : -1);
int distance = (fromI == toI) ? abs(toK - fromK) : abs(toI - fromI);
for (int i = 1; i < distance; i++) {
int x = (fromI == toI) ? fromI : fromI + step * i;
int y = (fromI == toI) ? fromK + step * i : fromK;
if (strcmp(map[x][y].name, "") != 0) return false;
}
return true;
}
第三步检验马的走法 - 马的移动方式是日字格(横向1格,纵向2格,或横向2格,纵向1格)。
- 判断其移动是否存在蹩马脚的情况(即相邻格子必须为空)
else if (strcmp(fromChess.name, "马") == 0 || strcmp(fromChess.name, "馬") == 0) {
int dx = abs(toK - fromK);
int dy = abs(toI - fromI);
if (!((dx == 1 && dy == 2) || (dx == 2 && dy == 1))) return false;
int blockX = fromI + (toI - fromI) / 2;
int blockY = fromK + (toK - fromK) / 2;
if (strcmp(map[blockX][blockY].name, "") != 0) return false;
return true;
}
第四步检验象的走法 - 象的移动是田字格(横向和纵向均移动2格)
- 田字中间必须为空。
- 象无法过河。
else if (strcmp(fromChess.name, "相") == 0 || strcmp(fromChess.name, "象") == 0) {
int dx = abs(toK - fromK);
int dy = abs(toI - fromI);
if (dx != 2 || dy != 2) return false;
int centerX = (fromI + toI) / 2;
int centerY = (fromK + toK) / 2;
if (strcmp(map[centerX][centerY].name, "") != 0) return false;
if (fromChess.type == 'B' && toI > 4) return false;
if (fromChess.type == 'R' && toI < 5) return false;
return true;
}
第五步检验士的走法 - 士在己方九宫格内移动
- 每次只能斜向移动一格(横向和纵向均移动1格)
else if (strcmp(fromChess.name, "仕") == 0 || strcmp(fromChess.name, "士") == 0) {
if (fromChess.type == 'B') {
if (toI > 2 || toK < 3 || toK > 5) return false;
} else {
if (toI < 7 || toK < 3 || toK > 5) return false;
}
return (abs(toK - fromK) == 1 && abs(toI - fromI) == 1);
}
第六步检验将的走法 else if (strcmp(fromChess.name, "帥") == 0 || strcmp(fromChess.name, "将") == 0) {
if (fromChess.type == 'B') {
if (toI > 2 || toK < 3 || toK > 5) return false;
} else {
if (toI < 7 || toK < 3 || toK > 5) return false;
}
if ((abs(toK - fromK) + abs(toI - fromI)) != 1) return false;
return true;
}
第七步检验炮的走法 - 必须直线移动
- 若目标为空,路径上不能有任何子
- 若目标位敌方棋子,路径上必须恰好有一个棋子
else if (strcmp(fromChess.name, "砲") == 0 || strcmp(fromChess.name, "炮") == 0) {
if (fromI != toI && fromK != toK) return false;
int count = 0;
int step = (fromI == toI) ? (toK > fromK ? 1 : -1) : (toI > fromI ? 1 : -1);
int distance = (fromI == toI) ? abs(toK - fromK) : abs(toI - fromI);
for (int i = 1; i < distance; i++) {
int x = (fromI == toI) ? fromI : fromI + step * i;
int y = (fromI == toI) ? fromK + step * i : fromK;
if (strcmp(map[x][y].name, "") != 0) count++;
}
if (strcmp(toChess.name, "") == 0) {
return (count == 0);
} else {
return (count == 1);
}
}
第八步检验兵的走法 else if (strcmp(fromChess.name, "卒") == 0 || strcmp(fromChess.name, "兵") == 0) {
int direction = (fromChess.type == 'B') ? 1 : -1;
if (!fromChess.flag) {
if (toI != fromI + direction || toK != fromK) return false;
if ((fromChess.type == 'B' && toI >= 5) || (fromChess.type == 'R' && toI <= 4)) {
map[fromI][fromK].flag = true;
}
} else {
bool valid = false;
if (toI == fromI + direction && toK == fromK) valid = true;
if (toI == fromI && abs(toK - fromK) == 1) valid = true;
return valid;
}
return true;
}
综上,我们将CheckMove() 函数插入游戏控制函数中,当且仅当移动合法时可对棋子进行移动。
接下来定义一个将军状态检测函数。即当一方走棋后,检验接下来走棋一方是否处于被将军状态。
bool CheckGeneral() {
POINT generalPos = { -1,1 };
char targetType = isRedTurn ? 'B' : 'R';
const char* generalName = (targetType == 'B') ? "将" : "帥";
for (int i = 0; i < ROW; ++i) {
for (int j = 0; j < COL; ++j) {
if (map[i][j].type == targetType &&
strcmp(map[i][j].name, generalName) == 0) {
generalPos.x = j;
generalPos.y = i;
break;
}
}
}
char enemyType = isRedTurn ? 'R' : 'B';
for (int i = 0; i < ROW; ++i) {
for (int j = 0; j < COL; ++j) {
if (map[i][j].type == enemyType &&
strcmp(map[i][j].name, "") != 0) {
struct Chess temp = map[generalPos.y][generalPos.x];
strcpy(map[generalPos.y][generalPos.x].name, map[i][j].name);
bool canAttack = CheckMove(i, j, generalPos.y, generalPos.x);
map[generalPos.y][generalPos.x] = temp;
if (canAttack) return true;
}
}
}
return false;
}
接下来我们进行胜利条件判断。
当将帅面对面或者将帅被吃时,游戏结束。
bool CheckWin() {
bool redExist = false, blackExist = false;
POINT redGeneral = { -1, -1 }, blackGeneral = { -1, -1 };
for (int i = 0; i < ROW; i++) {
for (int k = 0; k < COL; k++) {
if (strcmp(map[i][k].name, "帥") == 0) {
redExist = true;
redGeneral.x = k;
redGeneral.y = i;
}
if (strcmp(map[i][k].name, "将") == 0) {
blackExist = true;
blackGeneral.x = k;
blackGeneral.y = i;
}
}
}
if (!redExist) {
MessageBox(GetHWnd(), "黑方胜利!", "游戏结束", MB_OK);
return true;
}
if (!blackExist) {
MessageBox(GetHWnd(), "红方胜利!", "游戏结束", MB_OK);
return true;
}
if (redGeneral.x == blackGeneral.x) {
int minY = (redGeneral.y < blackGeneral.y) ? redGeneral.y : blackGeneral.y;
int maxY = (redGeneral.y > blackGeneral.y) ? redGeneral.y : blackGeneral.y;
bool hasBlock = false;
for (int y = minY + 1; y < maxY; y++) {
if (strcmp(map[y][redGeneral.x].name, "") != 0) {
hasBlock = true;
break;
}
}
if (!hasBlock) {
const char* winner = isRedTurn ? "黑方" : "红方";
char message[50];
sprintf(message, "%s 胜利!将帅对面!", winner);
MessageBox(GetHWnd(), message, "游戏结束", MB_OK);
return true;
}
}
return false;
}
综上,我们将这些函数插入游戏控制函数内即可。
添加背景音乐、走棋音效等
这一步很简单,直接给出代码。
MCI_OPEN_PARMS openBGM;
DWORD bgmld;
bool isMusicPlaying = false;
void SoundInit() {
mciSendString("open \"./sounds/bgm1.wav\" type mpegvideo alias bgm", NULL, 0, NULL);
mciSendString("open \"./sounds/click.wav\" alias click", NULL, 0, NULL);
mciSendString("open \"./sounds/move.wav\" alias move", NULL, 0, NULL);
mciSendString("open \"./sounds/eat.wav\" alias eat", NULL, 0, NULL);
mciSendString("open \"./sounds/check.wav\" alias check", NULL, 0, NULL);
}
void PlayBGM() {
mciSendString("play bgm repeat", NULL, 0, NULL);
mciSendString("setaudio bgm volume to 1000", NULL, 0, NULL);
isMusicPlaying = true;
}
void PlaySoundEffect(const char* alias) {
char cmd[50];
sprintf(cmd, "play %s from 0", alias);
mciSendString(cmd, NULL, 0, NULL);
}
添加红黑双方计时功能
我们先看此步骤完成后的结果。

这里我们只是实现最简单的计时功能,即双方步时60秒,总时长10分钟。
首先我们需要定义时间结构体包括总时间和当前步时。
struct Timer {
int totalTime;
int stepTime;
}redTimer, blackTimer;
DWORD lastUpdateTime = 0;
const int INIT_TOTAL_TIME = 600;
const int INIT_STEP_TIME = 60;
接着我们在游戏初始化函数中添加初始化计时器。
void GameInit(){
redTimer.totalTime = INIT_TOTAL_TIME;
redTimer.stepTime = INIT_STEP_TIME;
blackTimer.totalTime = INIT_TOTAL_TIME;
blackTimer.stepTime = INIT_STEP_TIME;
lastUpdateTime = GetTickCount();
}
在游戏控制函数中添加双方交换时步时的重置
void GameControl(){
if (isRedTurn) {
redTimer.stepTime = INIT_STEP_TIME;
}
else {
blackTimer.stepTime = INIT_STEP_TIME;
}
}
然后我们在游戏绘制函数中将时间显示在棋盘右侧。
void GameGraw(){
settextstyle(20, 0, "楷体");
settextcolor(BLACK);
settextstyle(20, 0, "楷体");
settextcolor(BLACK);
char redTimeStr1[50], redTimeStr2[50],blackTimeStr1[50], blackTimeStr2[50];
sprintf(redTimeStr1, "红方: 总 %02d:%02d",
redTimer.totalTime / 60, redTimer.totalTime % 60);
sprintf(redTimeStr2, " 步时: %02d",redTimer.stepTime);
sprintf(blackTimeStr1, "黑方: 总 %02d:%02d",
blackTimer.totalTime / 60, blackTimer.totalTime % 60);
sprintf(blackTimeStr2, " 步时: %02d", blackTimer.stepTime);
outtextxy(650, 650, redTimeStr1);
outtextxy(650, 675, redTimeStr2);
outtextxy(650, 50, blackTimeStr1);
outtextxy(650, 75, blackTimeStr2);
}
最后,在主函数中实现对时间的更新。
再结合之前的函数,我们写出主函数即可实现中国象棋的最简化版。
int main() {
initgraph(800, 800, SHOWCONSOLE);
SoundInit();
PlayBGM();
GameInit();
while (true) {
GameControl();
DWORD currentTime = GetTickCount();
DWORD elapsed = currentTime - lastUpdateTime;
if (elapsed >= 1000) {
int seconds = elapsed / 1000;
if (isRedTurn) {
redTimer.totalTime -= seconds;
redTimer.stepTime -= seconds;
}
else {
blackTimer.totalTime -= seconds;
blackTimer.stepTime -= seconds;
}
lastUpdateTime = currentTime;
if (redTimer.totalTime <= 0 || redTimer.stepTime <= 0) {
MessageBox(GetHWnd(), "红方超时,黑方胜利!", "游戏结束", MB_OK);
exit(0);
}
if (blackTimer.totalTime <= 0 || blackTimer.stepTime <= 0) {
MessageBox(GetHWnd(), "黑方超时,红方胜利!", "游戏结束", MB_OK);
exit(0);
}
}
BeginBatchDraw();
GameDraw();
EndBatchDraw();
}
EndBatchDraw();
closegraph();
mciSendString("close all", NULL, 0, NULL);
return 0;
}
到这里中国象棋的基本功能我们就实现了。由于这是本作者的第一个项目有点小激动,于是准备先进性项目打包处理。
这里使用传统打包方式,使用Microsoft Visual Studio Installer Projects打包。 - 安装扩展插件
- 在VS中安装Microsoft Visual Studio Installer Projects扩展
- 操作路径:扩展 > 管理扩展 > 搜索安装 > 重启VS
- 创建安装项目
- 右键解决方案>添加>新建项目
- 搜索选择"setup Project"模板
- 配置项目名称
- 配置安装内容
- 主程序添加:右键Application Folder>Add>项目输出>选择主输出
- 资源文件处理:将"sound"音效文件夹拖入Application Folder(确保所有依赖的DLL被包含)
- 配置快捷方式
- 右键Application>Add>文件>选择所需的快捷方式图案.ico文件(图片转换成.ico格式可以通过PS转换,需要提前配置插件)
- 右键主输出>创建快捷方式
- 将快捷方式拖入User's Desktop和User's Programs Menu
- 右键快捷方式>属性窗口>Icon>Browse>Browse>选择Application Folder文件中的ico文件>OK
- 处理依赖项
- 右键Application Folder>Add>文件>找到vcredist_x64.exe(路径:VS安装目录\VC\Redist\MSVC\版本号)
- 生成安装包
- 右键安装项目>生成
- 在输出目录即可获取Setup.exe和.msi文件
转自https://www.cnblogs.com/Yygz314/p/18899387
该文章在 2025/6/2 9:25:04 编辑过
|
|