项目初始化

This commit is contained in:
2026-06-22 09:08:59 +08:00
parent bfe39d22e0
commit 4f9c28364d
6 changed files with 168 additions and 41 deletions
+26 -2
View File
@@ -15,11 +15,35 @@ var Drama = new(drama)
// ==================== Drama CRUD ====================
func (c *drama) List(ctx context.Context, req *dto.ListDramaReq) (res *dto.ListDramaRes, err error) {
list, err := service.DramaService.List(ctx)
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize < 1 {
pageSize = 20
}
list, total, err := service.DramaService.List(ctx, page, pageSize)
if err != nil {
return nil, err
}
return &dto.ListDramaRes{List: list}, nil
return &dto.ListDramaRes{List: list, Total: total, Page: page, PageSize: pageSize}, nil
}
func (c *drama) ListEpisode(ctx context.Context, req *dto.ListEpisodeReq) (res *dto.ListEpisodeRes, err error) {
page := req.Page
if page < 1 {
page = 1
}
pageSize := req.PageSize
if pageSize < 1 {
pageSize = 10
}
list, total, err := service.DramaService.ListEpisode(ctx, req.DramaId, page, pageSize)
if err != nil {
return nil, err
}
return &dto.ListEpisodeRes{List: list, Total: total, Page: page, PageSize: pageSize}, nil
}
func (c *drama) Create(ctx context.Context, req *dto.CreateDramaReq) (res *dto.CreateDramaRes, err error) {
+21
View File
@@ -79,6 +79,27 @@ func (d *dramaDao) List(ctx context.Context) (res []*entity.Drama, err error) {
return
}
func (d *dramaDao) ListPage(ctx context.Context, page, pageSize int) (res []*entity.Drama, total int, err error) {
m := g.DB().Model(public.TableNameDrama).Ctx(ctx)
total, err = m.Count()
if err != nil {
return
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
r, err := m.OrderDesc("id").Limit(pageSize).Offset((page - 1) * pageSize).All()
if err != nil {
return
}
res = make([]*entity.Drama, 0)
err = r.Structs(&res)
return
}
// GetCharacters 查询短剧的所有演员
func (d *dramaDao) GetCharacters(ctx context.Context, dramaId int64) (res []*entity.Character, err error) {
r, err := g.DB().Model(public.TableNameCharacter).Ctx(ctx).Where("drama_id", dramaId).OrderAsc("id").All()
+25
View File
@@ -71,3 +71,28 @@ func (d *episodeDao) ListByDrama(ctx context.Context, dramaId int64) (res []*ent
err = r.Structs(&res)
return
}
func (d *episodeDao) ListPageByDrama(ctx context.Context, dramaId int64, page, pageSize int) (res []*entity.Episode, total int, err error) {
m := g.DB().Model(public.TableNameEpisode).Ctx(ctx).Where("drama_id", dramaId)
total, err = m.Count()
if err != nil {
return
}
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 10
}
r, err := m.OrderAsc("idx").Limit(pageSize).Offset((page - 1) * pageSize).All()
if err != nil {
return
}
res = make([]*entity.Episode, 0)
err = r.Structs(&res)
return
}
func (d *episodeDao) CountByDrama(ctx context.Context, dramaId int64) (int, error) {
return g.DB().Model(public.TableNameEpisode).Ctx(ctx).Where("drama_id", dramaId).Count()
}
+23 -2
View File
@@ -9,11 +9,32 @@ import (
// ==================== List ====================
type ListDramaReq struct {
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"短剧列表"`
g.Meta `path:"/list" method:"get" tags:"短剧管理" summary:"短剧列表"`
Page int `json:"page" dc:"页码,从1开始"`
PageSize int `json:"pageSize" dc:"每页条数"`
}
type ListDramaRes struct {
List []*entity.Drama `json:"list"`
List []*entity.Drama `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
// ==================== 剧集分页 ====================
type ListEpisodeReq struct {
g.Meta `path:"/episode/list" method:"get" tags:"短剧管理" summary:"剧集列表"`
DramaId int64 `json:"dramaId"`
Page int `json:"page" dc:"页码,从1开始"`
PageSize int `json:"pageSize" dc:"每页条数"`
}
type ListEpisodeRes struct {
List []*entity.Episode `json:"list"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
// ==================== Create ====================
+6 -2
View File
@@ -36,8 +36,12 @@ func (s *dramaService) Create(ctx context.Context, title, style string, episodeD
})
}
func (s *dramaService) List(ctx context.Context) ([]*entity.Drama, error) {
return dao.Drama.List(ctx)
func (s *dramaService) List(ctx context.Context, page, pageSize int) (list []*entity.Drama, total int, err error) {
return dao.Drama.ListPage(ctx, page, pageSize)
}
func (s *dramaService) ListEpisode(ctx context.Context, dramaId int64, page, pageSize int) (list []*entity.Episode, total int, err error) {
return dao.Episode.ListPageByDrama(ctx, dramaId, page, pageSize)
}
func (s *dramaService) Get(ctx context.Context, id int64) (*entity.Drama, []*entity.Character, []*entity.Episode, []*entity.GenerationTask, error) {
+67 -35
View File
@@ -258,17 +258,32 @@ let currentTaskId = null;
// ==================== 短剧列表 ====================
var dramaPage = 1;
var dramaTotal = 0;
var dramaPageSize = 20;
async function loadList() {
try {
const resp = await fetch('/drama/list');
const resp = await fetch('/drama/list?page=' + dramaPage + '&pageSize=' + dramaPageSize);
const data = await resp.json();
if (data.code !== 0) throw new Error(data.message);
dramaTotal = data.data.total;
renderList(data.data.list);
} catch (err) {
document.getElementById('dramaList').innerHTML = '<div class="card" style="color:red;cursor:default">加载失败: ' + err.message + '</div>';
}
}
function renderPageInfo(total, page, pageSize, onPrev, onNext) {
const totalPages = Math.ceil(total / pageSize) || 1;
if (total <= pageSize) return '';
return '<div style="display:flex;justify-content:center;align-items:center;gap:10px;padding:12px 0 4px;font-size:13px;color:#888;">'
+ '<button class="btn btn-sm btn-outline" onclick="' + onPrev + '" ' + (page <= 1 ? 'disabled' : '') + '>上一页</button>'
+ '<span>第 ' + page + ' / ' + totalPages + ' 页(共 ' + total + ' 条)</span>'
+ '<button class="btn btn-sm btn-outline" onclick="' + onNext + '" ' + (page >= totalPages ? 'disabled' : '') + '>下一页</button>'
+ '</div>';
}
function renderList(list) {
const container = document.getElementById('dramaList');
const empty = document.getElementById('emptyState');
@@ -296,9 +311,12 @@ function renderList(list) {
<button class="btn btn-sm btn-danger" onclick="confirmDelete('${d.id}','${escHtml(d.title)}')">删除</button>
</div>
</div>`;
}).join('');
}).join('') + renderPageInfo(dramaTotal, dramaPage, dramaPageSize, 'dramaPagePrev()', 'dramaPageNext()');
}
function dramaPagePrev() { if (dramaPage > 1) { dramaPage--; loadList(); } }
function dramaPageNext() { var tp = Math.ceil(dramaTotal / dramaPageSize) || 1; if (dramaPage < tp) { dramaPage++; loadList(); } }
// ==================== 短剧 CRUD ====================
function showDramaModal() {
@@ -459,11 +477,16 @@ document.getElementById('confirmBtn').addEventListener('click', async function()
// ==================== 剧集管理 ====================
var episodePage = 1;
var episodeTotal = 0;
var episodePageSize = 10;
function openEpisodeModal(id) {
currentDramaId = id;
document.getElementById('episodeModal').classList.add('show');
document.getElementById('epModalDramaTitle').textContent = '加载中...';
document.getElementById('epModalDramaMeta').textContent = '';
episodePage = 1;
loadEpisodeDrama(id);
}
@@ -476,29 +499,33 @@ function hideEpisodeModal() {
async function loadEpisodeDrama(id) {
try {
const resp = await fetch('/drama/get?id=' + id);
const data = await resp.json();
if (data.code !== 0) throw new Error(data.message);
currentDrama = data.data;
const [dramaResp, epResp] = await Promise.all([
fetch('/drama/get?id=' + id),
fetch('/drama/episode/list?dramaId=' + id + '&page=' + episodePage + '&pageSize=' + episodePageSize)
]);
const dramaData = await dramaResp.json();
const epData = await epResp.json();
if (dramaData.code !== 0) throw new Error(dramaData.message);
if (epData.code !== 0) throw new Error(epData.message);
currentDrama = dramaData.data;
episodeTotal = epData.data.total;
document.getElementById('epModalDramaTitle').textContent = currentDrama.title || '未命名短剧';
document.getElementById('epModalDramaMeta').textContent = (currentDrama.style || '') + ' · ' + (currentDrama.episodeDuration || '-') + 's/集';
renderEps();
renderEps(epData.data.list);
} catch (err) {
document.getElementById('epModalDramaTitle').textContent = '加载失败';
alert('加载剧集失败: ' + err.message);
}
}
function renderEps() {
function renderEps(eps) {
const list = document.getElementById('epList');
const eps = currentDrama.episodes || [];
if (eps.length === 0) {
if (!eps || eps.length === 0) {
list.innerHTML = '<div style="color:#999;font-size:13px;padding:8px 0">暂无剧集,点击下方添加</div>';
return;
}
var taskMap = {};
if (currentDrama.tasks) { currentDrama.tasks.forEach(function(t) { taskMap[t.episodeId] = t; }); }
eps.sort(function(a, b) { return (a.index || 0) - (b.index || 0); });
list.innerHTML = eps.map(function(ep) {
const status = ep.status || 'pending';
const statusLabels = { pending:'待生成', generating:'生成中...', review:'待审核', completed:'已完成', failed:'生成失败' };
@@ -516,18 +543,21 @@ function renderEps() {
+ '<div class="status ' + status + '">'
+ (status === 'generating' ? '<span class="spinner-sm"></span>' : '')
+ (statusLabels[status] || status)
+ (ep.videoUrl ? ' · <button class="btn btn-sm btn-outline" onclick="previewEpisode(\'' + ep.id + '\')">预览</button>' : '')
+ (ep.videoUrl ? ' · <button class="btn btn-sm btn-outline" onclick="previewEpisode(' + ep.id + ')">预览</button>' : '')
+ segHtml
+ '</div></div>'
+ '<div class="actions">'
+ (status === 'pending' || status === 'failed' ? '<button class="btn btn-sm btn-success" onclick="generateEp(\'' + ep.id + '\')">生成视频</button>' : '')
+ (status === 'review' ? '<button class="btn btn-sm btn-primary" onclick="openReviewPanel(\'' + ep.id + '\')">审核</button>' : '')
+ '<button class="btn btn-sm btn-outline" onclick="editEp(\'' + ep.id + '\')">编辑</button>'
+ '<button class="btn btn-sm btn-danger" onclick="deleteEp(\'' + ep.id + '\',\'' + escAttr(ep.title) + '\')">删除</button>'
+ (status === 'pending' || status === 'failed' ? '<button class="btn btn-sm btn-success" onclick="generateEp(' + ep.id + ')">生成视频</button>' : '')
+ (status === 'review' ? '<button class="btn btn-sm btn-primary" onclick="openReviewPanel(' + ep.id + ')">审核</button>' : '')
+ '<button class="btn btn-sm btn-outline" onclick="editEp(' + ep.id + ')">编辑</button>'
+ '<button class="btn btn-sm btn-danger" onclick="deleteEp(' + ep.id + ',' + escAttr(ep.title) + ')">删除</button>'
+ '</div></div>';
}).join('');
}).join('') + renderPageInfo(episodeTotal, episodePage, episodePageSize, 'epPagePrev()', 'epPageNext()');
}
function epPagePrev() { if (episodePage > 1) { episodePage--; loadEpisodeDrama(currentDramaId); } }
function epPageNext() { var tp = Math.ceil(episodeTotal / episodePageSize) || 1; if (episodePage < tp) { episodePage++; loadEpisodeDrama(currentDramaId); } }
function segmentPreviews(task, epId) {
var sd; try { sd = JSON.parse(task.stepsData); } catch(e) { return ""; }
var segs = sd.segments || [];
@@ -821,26 +851,28 @@ async function savePrompt() {
<!-- 模型配置弹窗 -->
<div class="modal-overlay" id="configModal">
<div class="modal modal-wide" style="max-width:560px;">
<h3>模型配置</h3>
<div class="form-group"><label>对话 API Key</label><input type="password" id="cfg_chatApiKey" placeholder="sk-..."><div class="help-text">对话模型 API 密钥</div></div>
<h4 style="font-size:13px;margin:14px 0 8px;color:#1a1a2e;border-left:3px solid #4a6cf7;padding-left:8px;">对话模型(Agent 文字推理)</h4>
<div class="form-group"><label>API 地址</label><input type="text" id="cfg_chatBaseURL" placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"></div>
<div class="form-group"><label>对话模型</label><input type="text" id="cfg_chatModelName" placeholder="qwen3.7-plus, gpt-4o, deepseek-chat 等"></div>
<h4 style="font-size:13px;margin:14px 0 8px;color:#1a1a2e;border-left:3px solid #4a6cf7;padding-left:8px;">视频生成模型</h4>
<div class="form-group"><label>视频 API Key</label><input type="password" id="cfg_videoApiKey" placeholder="sk-..."><div class="help-text">视频生成 API 密钥,可与对话模型不同</div></div>
<div class="form-group"><label>视频 API 地址</label><input type="text" id="cfg_videoBaseURL" placeholder="https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis"></div>
<div class="form-group"><label>视频模型</label><input type="text" id="cfg_videoModelName" placeholder="wan2.7-t2v"></div>
<div class="form-group"><label>视频查询地址</label><input type="text" id="cfg_videoQueryURL" placeholder="https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"><div class="help-text">用 {task_id} 占位,轮询时自动替换</div></div>
<div class="row">
<div class="form-group"><label>Max Tokens</label><input type="number" id="cfg_maxTokens" value="4096" min="256" max="32768"></div>
<div class="form-group"><label>Temperature</label><input type="number" id="cfg_temperature" value="0.8" min="0" max="2" step="0.1"></div>
<div class="modal modal-wide" style="max-width:680px;padding:20px 24px;">
<h3 style="margin-bottom:14px;">模型配置</h3>
<div class="row" style="margin-bottom:10px;">
<div class="form-group" style="margin-bottom:0;"><label>对话 API Key</label><input type="password" id="cfg_chatApiKey" placeholder="sk-..."><div class="help-text">对话模型 API 密钥</div></div>
<div class="form-group" style="margin-bottom:0;"><label>视频 API Key</label><input type="password" id="cfg_videoApiKey" placeholder="sk-..."><div class="help-text">视频生成 API 密钥</div></div>
</div>
<div class="row">
<div class="form-group"><label>单段最小时长(秒)</label><input type="number" id="cfg_minSingleDuration" value="2" min="1" max="60"><div class="help-text">不足时从前面的段借</div></div>
<div class="form-group"><label>单段最大时长(秒)</label><input type="number" id="cfg_maxSingleDuration" value="30" min="5" max="300"><div class="help-text">超出会自动分段生成</div></div>
<div class="row" style="margin-bottom:10px;">
<div class="form-group" style="margin-bottom:0;"><label>对话 API 地址</label><input type="text" id="cfg_chatBaseURL" placeholder="https://dashscope.aliyuncs.com/compatible-mode/v1"></div>
<div class="form-group" style="margin-bottom:0;"><label>视频 API 地址</label><input type="text" id="cfg_videoBaseURL" placeholder="https://dashscope.aliyuncs.com/api/v1/services/aigc/video-generation/video-synthesis"></div>
</div>
<div style="margin-top:16px;display:flex;gap:10px;justify-content:center;align-items:center;">
<div class="row" style="margin-bottom:10px;">
<div class="form-group" style="margin-bottom:0;"><label>对话模型</label><input type="text" id="cfg_chatModelName" placeholder="qwen3.7-plus, gpt-4o, deepseek-chat 等"></div>
<div class="form-group" style="margin-bottom:0;"><label>视频模型</label><input type="text" id="cfg_videoModelName" placeholder="wan2.7-t2v"></div>
</div>
<div class="form-group" style="margin-bottom:10px;"><label>视频查询地址</label><input type="text" id="cfg_videoQueryURL" placeholder="https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"><div class="help-text">用 {task_id} 占位,轮询时自动替换</div></div>
<div class="row" style="margin-bottom:10px;">
<div class="form-group" style="margin-bottom:0;"><label>Max Tokens</label><input type="number" id="cfg_maxTokens" value="4096" min="256" max="32768"></div>
<div class="form-group" style="margin-bottom:0;"><label>Temperature</label><input type="number" id="cfg_temperature" value="0.8" min="0" max="2" step="0.1"></div>
<div class="form-group" style="margin-bottom:0;"><label>单段最小时长(秒)</label><input type="number" id="cfg_minSingleDuration" value="2" min="1" max="60"><div class="help-text">不足时从前面的段借</div></div>
<div class="form-group" style="margin-bottom:0;"><label>单段最大时长(秒)</label><input type="number" id="cfg_maxSingleDuration" value="30" min="5" max="300"><div class="help-text">超出会自动分段生成</div></div>
</div>
<div style="margin-top:14px;display:flex;gap:10px;justify-content:center;align-items:center;">
<div class="loading" id="cfg_loading" style="display:none;align-items:center;gap:6px;color:#888;font-size:13px;"><div class="spinner"></div><span>保存中...</span></div>
<button class="btn btn-primary" id="cfg_saveBtn" onclick="saveConfig()">保存配置</button>
<button class="btn btn-outline" onclick="hideConfigModal()">取消</button>