666 lines
45 KiB
JavaScript
666 lines
45 KiB
JavaScript
window.__ModuleLoader__.load({
|
||||
|
|
id: "dsh-plugin-douyin-accounts",
|
|||
|
|
factory: (require) => {
|
|||
|
|
var module = { exports: {} };
|
|||
|
|
var exports = module.exports;
|
|||
|
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|||
|
|
let react = require("react");
|
|||
|
|
let react_dom = require("react-dom");
|
|||
|
|
//#region plugin css(暗色适配:嵌套变量 + 无浅色 fallback,见踩坑手册 3.4)
|
|||
|
|
const css = ".dvAcc_page{position:relative;background:var(--dsw-alias-bg-base);width:100%;min-width:0;height:100%;min-height:0;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family);flex:1;grid-template-rows:minmax(0,1fr);display:grid;overflow:hidden}.dvAcc_body{display:flex;flex-direction:column;height:100%;min-height:0;padding:16px;box-sizing:border-box;gap:12px}.dvAcc_toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.dvAcc_title{font-size:16px;font-weight:600;white-space:nowrap}.dvAcc_select{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 8px;font:var(--dsw-font-s-14);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary)}.dvAcc_input{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 10px;font:var(--dsw-font-s-14);background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);min-width:200px}.dvAcc_btn{border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px;padding:6px 14px;font:var(--dsw-font-s-14);cursor:pointer;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));color:var(--dsw-alias-label-primary);transition:background var(--ds-transition-duration-fast) var(--ds-ease-in-out)}.dvAcc_btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvAcc_link{color:var(--dsw-alias-accent-fg);cursor:pointer;text-decoration:none;border-bottom:1px dashed var(--dsw-alias-accent-fg,transparent)}.dvAcc_link:hover{text-decoration:underline}.dvAcc_spacer{flex:1}.dvAcc_tableWrap{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));border-radius:8px}.dvAcc_table{width:100%;border-collapse:collapse;font:var(--dsw-font-s-14);min-width:1360px}.dvAcc_table th{position:sticky;top:0;background:var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated));text-align:left;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));font-weight:600;white-space:nowrap;user-select:none}.dvAcc_table td{padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border));vertical-align:middle}.dvAcc_table tr:hover{background:var(--dsw-alias-interactive-bg-hover)}.dvAcc_cellName{font-weight:600;white-space:nowrap}.dvAcc_cellTrack{white-space:nowrap}.dvAcc_cellContent{color:var(--dsw-alias-label-secondary);max-width:420px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvAcc_cellClamp{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;white-space:normal;word-break:break-all;line-height:1.5}.dvAcc_cellM{white-space:nowrap;text-align:center}.dvAcc_badge{display:inline-block;padding:1px 8px;border-radius:10px;font-size:12px;line-height:18px;white-space:nowrap}.dvAcc_badgeSigned{background:rgba(93,125,255,.18);color:var(--dsw-alias-label-primary)}.dvAcc_badgeExternal{background:rgba(46,190,140,.18);color:var(--dsw-alias-label-primary)}.dvAcc_pager{display:flex;align-items:center;gap:12px;justify-content:flex-end}.dvAcc_pagerTotal{margin-right:auto;color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-13)}.dvAcc_pagerInfo{color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-13)}.dvAcc_loading{display:flex;align-items:center;justify-content:center;height:100%;color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-14)}.dvAcc_error{color:#ff6b6b;padding:20px;font:var(--dsw-font-s-14)}.dvAcc_empty{padding:40px 20px;text-align:center;color:var(--dsw-alias-label-secondary);font:var(--dsw-font-s-14);line-height:1.8}.dvAcc_btnPrimary{background:var(--dsw-alias-brand-primary,#4d6bfe);color:#fff;border-color:transparent}.dvAcc_btnPrimary:hover{background:var(--dsw-alias-brand-primary-hover,#3a55d6)}.dvAcc_btnIcon{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;margin-right:4px;border-radius:6px;font-size:14px}.dvAcc_mask{position:fixed;inset:0;background:rgba(0,0,0,.4);display:flex;align-i
|
|||
|
|
const key = "dsh-plugin-douyin-accounts/DouyinAccounts.module.css";
|
|||
|
|
if (!document.querySelector("style[data-plugin-css=" + JSON.stringify(key) + "]")) {
|
|||
|
|
const el = document.createElement("style");
|
|||
|
|
el.dataset.plugin = "dsh-plugin-douyin-accounts";
|
|||
|
|
el.dataset.pluginCss = key;
|
|||
|
|
el.textContent = css;
|
|||
|
|
if (!document.querySelector('style[data-plugin-css="dvAcc-align"]')) {
|
|||
|
|
const el2 = document.createElement("style");
|
|||
|
|
el2.dataset.pluginCss = "dvAcc-align";
|
|||
|
|
el2.textContent = ".dvAcc_table thead th:last-child{text-align:center!important}.dvAcc_table tbody td:last-child{text-align:center!important}.tblAcc thead th:nth-child(7),.tblAcc thead th:nth-child(8),.tblAcc thead th:nth-child(9),.tblAcc thead th:nth-child(10),.tblAcc tbody td:nth-child(7),.tblAcc tbody td:nth-child(8),.tblAcc tbody td:nth-child(9),.tblAcc tbody td:nth-child(10){text-align:right}.tblAcc thead th:nth-child(1),.tblAcc thead th:nth-child(11),.tblAcc thead th:nth-child(12),.tblAcc tbody td:nth-child(1),.tblAcc tbody td:nth-child(11),.tblAcc tbody td:nth-child(12){text-align:center}.dvAcc_tableWrap{overflow:auto!important;min-width:0!important}.dvAcc_table{min-width:1180px}.tblAcc thead th:nth-child(5),.tblAcc tbody td:nth-child(5){max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dvAcc_table{width:auto!important;min-width:0!important;table-layout:auto}.dvAcc_page{width:100%!important;min-width:0!important;max-width:100%!important}.dvAcc_body{width:100%!important;min-width:0!important;max-width:100%!important}.dvAcc_tableWrap{width:100%!important;min-width:0!important;max-width:100%!important;overflow:auto}";
|
|||
|
|
document.head.appendChild(el2);
|
|||
|
|
}
|
|||
|
|
document.head.appendChild(el);
|
|||
|
|
}
|
|||
|
|
const cssMap = {
|
|||
|
|
"page": "dvAcc_page",
|
|||
|
|
"body": "dvAcc_body",
|
|||
|
|
"toolbar": "dvAcc_toolbar",
|
|||
|
|
"title": "dvAcc_title",
|
|||
|
|
"select": "dvAcc_select",
|
|||
|
|
"input": "dvAcc_input",
|
|||
|
|
"btn": "dvAcc_btn",
|
|||
|
|
"link": "dvAcc_link",
|
|||
|
|
"spacer": "dvAcc_spacer",
|
|||
|
|
"tableWrap": "dvAcc_tableWrap",
|
|||
|
|
"table": "dvAcc_table",
|
|||
|
|
"cellName": "dvAcc_cellName",
|
|||
|
|
"cellTrack": "dvAcc_cellTrack",
|
|||
|
|
"cellContent": "dvAcc_cellContent",
|
|||
|
|
"cellClamp": "dvAcc_cellClamp",
|
|||
|
|
"cellM": "dvAcc_cellM",
|
|||
|
|
"badge": "dvAcc_badge",
|
|||
|
|
"badgeSigned": "dvAcc_badgeSigned",
|
|||
|
|
"badgeExternal": "dvAcc_badgeExternal",
|
|||
|
|
"pager": "dvAcc_pager",
|
|||
|
|
"pagerTotal": "dvAcc_pagerTotal",
|
|||
|
|
"pagerInfo": "dvAcc_pagerInfo",
|
|||
|
|
"loading": "dvAcc_loading",
|
|||
|
|
"error": "dvAcc_error",
|
|||
|
|
"empty": "dvAcc_empty",
|
|||
|
|
"mask": "dvAcc_mask",
|
|||
|
|
"modal": "dvAcc_modal",
|
|||
|
|
"modalTitle": "dvAcc_modalTitle",
|
|||
|
|
"modalText": "dvAcc_modalText",
|
|||
|
|
"modalBtns": "dvAcc_modalBtns",
|
|||
|
|
"btnPrimary": "dvAcc_btnPrimary",
|
|||
|
|
"btnIcon": "dvAcc_btnIcon",
|
|||
|
|
"modalEdit": "dvAcc_modalEdit",
|
|||
|
|
"modalBody": "dvAcc_modalBody",
|
|||
|
|
"formGrid": "dvAcc_formGrid",
|
|||
|
|
"formItem": "dvAcc_formItem",
|
|||
|
|
"formItemWide": "dvAcc_formItemWide",
|
|||
|
|
"textarea": "dvAcc_textarea"
|
|||
|
|
};
|
|||
|
|
//#endregion
|
|||
|
|
//#region 抖音账号页面
|
|||
|
|
// 统一线性 SVG 图标(Lucide 风格:24 viewBox、stroke currentColor、圆角端点)
|
|||
|
|
const SvgBase = ({ d, size = 16 }) => react.createElement("svg", {
|
|||
|
|
viewBox: "0 0 24 24",
|
|||
|
|
width: size,
|
|||
|
|
height: size,
|
|||
|
|
fill: "none",
|
|||
|
|
stroke: "currentColor",
|
|||
|
|
strokeWidth: "2",
|
|||
|
|
strokeLinecap: "round",
|
|||
|
|
strokeLinejoin: "round",
|
|||
|
|
"aria-hidden": "true",
|
|||
|
|
style: { verticalAlign: "-2px", marginRight: "4px" },
|
|||
|
|
}, d);
|
|||
|
|
const IconDownload = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }), react.createElement("polyline", { key: "p2", points: "7 10 12 15 17 10" }), react.createElement("line", { key: "p3", x1: "12", y1: "15", x2: "12", y2: "3" })] });
|
|||
|
|
const IconRefresh = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" }), react.createElement("path", { key: "p2", d: "M21 3v5h-5" }), react.createElement("path", { key: "p3", d: "M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" }), react.createElement("path", { key: "p4", d: "M8 16H3v5" })] });
|
|||
|
|
const IconEdit = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" })] });
|
|||
|
|
const IconGlobe = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("circle", { key: "p1", cx: "12", cy: "12", r: "10" }), react.createElement("path", { key: "p2", d: "M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" }), react.createElement("path", { key: "p3", d: "M2 12h20" })] });
|
|||
|
|
const IconCheckSquare = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("rect", { key: "p1", x: "3", y: "3", width: "18", height: "18", rx: "2", ry: "2" }), react.createElement("path", { key: "p2", d: "m9 12 2 2 4-4" })] });
|
|||
|
|
const IconUsers = ({ size }) => react.createElement(SvgBase, { size, d: [react.createElement("path", { key: "p1", d: "M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" }), react.createElement("circle", { key: "p2", cx: "9", cy: "7", r: "4" }), react.createElement("path", { key: "p3", d: "M22 21v-2a4 4 0 0 0-3-3.87" }), react.createElement("path", { key: "p4", d: "M16 3.13a4 4 0 0 1 0 7.75" })] });
|
|||
|
|
// 账号类型:all 全部 / signed 签约(MCP 数据)/ external 外部(skill 数据,结构待确认)
|
|||
|
|
const TYPE_OPTIONS = [
|
|||
|
|
{ value: "all", label: "全部" },
|
|||
|
|
{ value: "signed", label: "内部" },
|
|||
|
|
{ value: "external", label: "外部" }
|
|||
|
|
];
|
|||
|
|
const TYPE_LABEL = { signed: "内部", external: "外部" };
|
|||
|
|
// 长文本缩短为约 1/3(上限 150 字),单元格保持单行省略展示;悬浮 title 仍为全文
|
|||
|
|
const shortText = (s) => {
|
|||
|
|
if (!s) return "-";
|
|||
|
|
const full = String(s);
|
|||
|
|
const n = Math.min(Math.ceil(full.length / 3), 150);
|
|||
|
|
return full.length > n ? full.slice(0, n) + "…" : full;
|
|||
|
|
};
|
|||
|
|
function AccountsPage() {
|
|||
|
|
const [type, setType] = react.useState("all"); // 默认全部
|
|||
|
|
const [search, setSearch] = react.useState("");
|
|||
|
|
const [track, setTrack] = react.useState("");
|
|||
|
|
const [tracks, setTracks] = react.useState([]);
|
|||
|
|
const [items, setItems] = react.useState([]);
|
|||
|
|
const [total, setTotal] = react.useState(0);
|
|||
|
|
const [page, setPage] = react.useState(1);
|
|||
|
|
const [pageSize] = react.useState(20);
|
|||
|
|
const [loading, setLoading] = react.useState(true);
|
|||
|
|
const [error, setError] = react.useState(null);
|
|||
|
|
const [toast, setToast] = react.useState(null);
|
|||
|
|
const [importDirOpen, setImportDirOpen] = react.useState(false);
|
|||
|
|
const [importDirInput, setImportDirInput] = react.useState("");
|
|||
|
|
// 网页获取账号信息(抖音账号分析技能):账号名称 / 抖音号 / 抖音账号地址
|
|||
|
|
const [importAccName, setImportAccName] = react.useState("");
|
|||
|
|
const [importAccDid, setImportAccDid] = react.useState("");
|
|||
|
|
const [importAccUrl, setImportAccUrl] = react.useState("");
|
|||
|
|
const [importing, setImporting] = react.useState(false);
|
|||
|
|
// 编辑账号信息弹窗状态
|
|||
|
|
const [editRow, setEditRow] = react.useState(null);
|
|||
|
|
const [editForm, setEditForm] = react.useState(null);
|
|||
|
|
const [saving, setSaving] = react.useState(false);
|
|||
|
|
// 任务状态轮询(creative/import 执行中 → 按钮置灰;导入/刷新完成后自动刷新列表)
|
|||
|
|
const [taskStatus, setTaskStatus] = react.useState({ creative: "idle", import: "idle" });
|
|||
|
|
// 等待 AI 会话处理完成的标记:处理后自动刷新列表数据
|
|||
|
|
const [pendingRefresh, setPendingRefresh] = react.useState(false);
|
|||
|
|
// V1.0 同步:批量栏 + 行内 AI创作(原创选题)状态
|
|||
|
|
const [batchOn, setBatchOn] = react.useState(false);
|
|||
|
|
const [selected, setSelected] = react.useState({});
|
|||
|
|
const [createRow, setCreateRow] = react.useState(null);
|
|||
|
|
const [createText, setCreateText] = react.useState("");
|
|||
|
|
const [creating, setCreating] = react.useState(false);
|
|||
|
|
// V1.0 对齐:更新关注账号(批量串行)状态
|
|||
|
|
const [updBusy, setUpdBusy] = react.useState(false);
|
|||
|
|
const [updLabel, setUpdLabel] = react.useState("");
|
|||
|
|
const prevImportRef = react.useRef("idle");
|
|||
|
|
// 刷新确认弹窗:待刷新账号
|
|||
|
|
const [refreshRow, setRefreshRow] = react.useState(null);
|
|||
|
|
react.useEffect(() => {
|
|||
|
|
let alive = true;
|
|||
|
|
const poll = () => {
|
|||
|
|
fetch("/mcn/api/task/status")
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!alive) return;
|
|||
|
|
const st = { creative: d.creative || "idle", import: d.import || "idle" };
|
|||
|
|
setTaskStatus(st);
|
|||
|
|
// 导入/刷新会话从 running → idle:AI 处理完成,刷新列表数据
|
|||
|
|
if (pendingRefresh && prevImportRef.current === "running" && st.import === "idle") {
|
|||
|
|
setPendingRefresh(false);
|
|||
|
|
setToast("数据处理完成,列表已刷新");
|
|||
|
|
load(1, type, search, track);
|
|||
|
|
}
|
|||
|
|
prevImportRef.current = st.import;
|
|||
|
|
})
|
|||
|
|
.catch(() => {});
|
|||
|
|
};
|
|||
|
|
poll();
|
|||
|
|
const timer = setInterval(poll, 5000);
|
|||
|
|
return () => { alive = false; clearInterval(timer); };
|
|||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|||
|
|
}, [pendingRefresh]);
|
|||
|
|
const creativeBusy = taskStatus.creative === "running";
|
|||
|
|
const importBusy = taskStatus.import === "running";
|
|||
|
|
|
|||
|
|
const loadTracks = () => {
|
|||
|
|
fetch("/mcn/api/tracks").then((r) => r.json()).then((d) => {
|
|||
|
|
if (d.ok) setTracks(d.tracks || []);
|
|||
|
|
}).catch(() => {});
|
|||
|
|
};
|
|||
|
|
const load = (p = page, t = type, s = search, tr = track) => {
|
|||
|
|
setLoading(true);
|
|||
|
|
const params = new URLSearchParams({ page: String(p), pageSize: String(pageSize), type: t, sortBy: "id", order: "asc" });
|
|||
|
|
if (s) params.set("search", s);
|
|||
|
|
if (tr) params.set("track", tr);
|
|||
|
|
fetch("/mcn/api/accounts?" + params.toString())
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "加载失败");
|
|||
|
|
setItems(d.items || []);
|
|||
|
|
setTotal(d.total || 0);
|
|||
|
|
setPage(d.page || 1);
|
|||
|
|
setError(null);
|
|||
|
|
})
|
|||
|
|
.catch((e) => setError(e.message))
|
|||
|
|
.finally(() => setLoading(false));
|
|||
|
|
};
|
|||
|
|
// 收藏/取消收藏:成功后刷新列表(收藏的账号置顶,按粉丝数降序)
|
|||
|
|
const toggleFavorite = (row) => {
|
|||
|
|
fetch("/mcn/api/account/favorite", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ accountId: row.id, favorite: row.favorite ? 0 : 1 }),
|
|||
|
|
})
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "操作失败");
|
|||
|
|
setToast(row.favorite ? `已取消收藏「${row.account_name}」` : `已收藏「${row.account_name}」`);
|
|||
|
|
load(1, type, search, track);
|
|||
|
|
})
|
|||
|
|
.catch((e) => setToast(e.message || "操作失败"));
|
|||
|
|
};
|
|||
|
|
react.useEffect(() => {
|
|||
|
|
loadTracks();
|
|||
|
|
load(1, "all", "", "");
|
|||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|||
|
|
}, []);
|
|||
|
|
react.useEffect(() => {
|
|||
|
|
if (!toast) return;
|
|||
|
|
const timer = setTimeout(() => setToast(null), 3000);
|
|||
|
|
return () => clearTimeout(timer);
|
|||
|
|
}, [toast]);
|
|||
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|||
|
|
const onTypeChange = (v) => { setType(v); load(1, v, search, track); };
|
|||
|
|
const onSearch = () => { load(1, type, search, track); };
|
|||
|
|
const onReset = () => { setSearch(""); setTrack(""); setType("all"); load(1, "all", "", ""); };
|
|||
|
|
const createFor = (row) => {
|
|||
|
|
const info = [
|
|||
|
|
`账号名:${row.account_name}`,
|
|||
|
|
row.track ? `赛道:${row.track}` : "",
|
|||
|
|
row.content ? `账号定位:${row.content}` : "",
|
|||
|
|
].filter(Boolean).join(";");
|
|||
|
|
const prompt = `请为达人「${row.account_name}」创作一条短视频脚本。\n达人信息:${info}\n请先进行账号设定分析,再生成完整脚本。`;
|
|||
|
|
fetch("/mcn/api/creative", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ prompt }),
|
|||
|
|
})
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "触发失败");
|
|||
|
|
setToast(d.message || "已开始创作,请切换到对应会话查看");
|
|||
|
|
})
|
|||
|
|
.catch((e) => setToast("创作触发失败: " + e.message));
|
|||
|
|
};
|
|||
|
|
const typeBadge = (t) => {
|
|||
|
|
const label = TYPE_LABEL[t] || "签约";
|
|||
|
|
const cls = t === "external" ? cssMap.badge + " " + cssMap.badgeExternal : cssMap.badge + " " + cssMap.badgeSigned;
|
|||
|
|
return react.createElement("span", { className: cls }, label);
|
|||
|
|
};
|
|||
|
|
// 编辑账号信息:打开弹窗(账号名称不可改)
|
|||
|
|
const openEdit = (row) => {
|
|||
|
|
setEditRow(row);
|
|||
|
|
setEditForm({
|
|||
|
|
track: row.track ?? "",
|
|||
|
|
is_ai: row.is_ai === "是" ? "是" : "否",
|
|||
|
|
content: row.content ?? "",
|
|||
|
|
tags: row.tags ?? "",
|
|||
|
|
masterpiece: row.masterpiece ?? "",
|
|||
|
|
douyin_id: row.douyin_id ?? "",
|
|||
|
|
followers: row.followers ?? "",
|
|||
|
|
total_likes: row.total_likes ?? "",
|
|||
|
|
works_count: row.works_count ?? "",
|
|||
|
|
ip_location: row.ip_location ?? "",
|
|||
|
|
location: row.location ?? "",
|
|||
|
|
age: row.age ?? "",
|
|||
|
|
bio: row.bio ?? "",
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
const saveEdit = () => {
|
|||
|
|
if (!editRow) return;
|
|||
|
|
setSaving(true);
|
|||
|
|
const payload = { id: editRow.id };
|
|||
|
|
for (const k of Object.keys(editForm)) {
|
|||
|
|
const val = String(editForm[k] ?? "").trim();
|
|||
|
|
if (k === "works_count" || k === "age") {
|
|||
|
|
payload[k] = val === "" ? null : (Number.isFinite(Number(val)) ? Number(val) : null);
|
|||
|
|
} else {
|
|||
|
|
payload[k] = val === "" ? null : val;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
fetch("/mcn/api/account/update", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify(payload),
|
|||
|
|
})
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "保存失败");
|
|||
|
|
setToast(d.message || "已保存");
|
|||
|
|
setEditRow(null);
|
|||
|
|
setEditForm(null);
|
|||
|
|
load(1, type, search, track);
|
|||
|
|
})
|
|||
|
|
.catch((e) => setToast("保存失败: " + e.message))
|
|||
|
|
.finally(() => setSaving(false));
|
|||
|
|
};
|
|||
|
|
// 编辑表单字段控件(label + input/textarea)
|
|||
|
|
const editField = (label, key, opts) => {
|
|||
|
|
const o = opts || {};
|
|||
|
|
const cls = cssMap.formItem + (o.wide ? " " + cssMap.formItemWide : "");
|
|||
|
|
const control = o.textarea
|
|||
|
|
? react.createElement("textarea", {
|
|||
|
|
className: cssMap.textarea,
|
|||
|
|
rows: o.rows || 3,
|
|||
|
|
value: editForm[key] ?? "",
|
|||
|
|
onChange: (e) => setEditForm({ ...editForm, [key]: e.target.value }),
|
|||
|
|
})
|
|||
|
|
: react.createElement("input", {
|
|||
|
|
className: cssMap.input,
|
|||
|
|
type: o.type || "text",
|
|||
|
|
style: { boxSizing: "border-box", width: "100%" },
|
|||
|
|
value: editForm[key] ?? "",
|
|||
|
|
onChange: (e) => setEditForm({ ...editForm, [key]: e.target.value }),
|
|||
|
|
});
|
|||
|
|
return react.createElement("label", { className: cls, key }, react.createElement("span", null, label), control);
|
|||
|
|
};
|
|||
|
|
// 导入:支持两种方式(AI 判断处理)
|
|||
|
|
// 1) 本地文件夹路径 → 提交给宿主端 → AI 读取分析确认后导入
|
|||
|
|
// 2) 抖音账号信息(名称/抖音号/地址)→ AI 调用抖音账号分析技能网页获取账号信息与视频列表并写入数据库
|
|||
|
|
const submitImport = () => {
|
|||
|
|
const dir = importDirInput.trim();
|
|||
|
|
const accName = importAccName.trim();
|
|||
|
|
const accDid = importAccDid.trim();
|
|||
|
|
const accUrl = importAccUrl.trim();
|
|||
|
|
if (!dir && !accName && !accDid && !accUrl) { setToast("请输入文件夹路径或抖音账号信息"); return; }
|
|||
|
|
setImporting(true);
|
|||
|
|
const body = dir
|
|||
|
|
? { dir }
|
|||
|
|
: { account: { name: accName, douyinId: accDid, url: accUrl } };
|
|||
|
|
fetch("/mcn/api/external/stage", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify(body),
|
|||
|
|
})
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "提交失败");
|
|||
|
|
setToast(d.message || (dir ? "已提交,AI 正在分析文件结构,请到对话中确认导入" : "已提交,AI 正在获取账号信息与视频列表,请到对话中查看"));
|
|||
|
|
setImportDirOpen(false);
|
|||
|
|
setImportDirInput("");
|
|||
|
|
setImportAccName("");
|
|||
|
|
setImportAccDid("");
|
|||
|
|
setImportAccUrl("");
|
|||
|
|
// AI 处理完成后自动刷新列表
|
|||
|
|
setPendingRefresh(true);
|
|||
|
|
})
|
|||
|
|
.catch((err) => setToast("提交失败: " + err.message))
|
|||
|
|
.finally(() => setImporting(false));
|
|||
|
|
};
|
|||
|
|
// 刷新账号数据:确认弹窗 → AI 会话(mcn-dou-analysis 技能)重新获取并更新数据库
|
|||
|
|
const submitRefresh = () => {
|
|||
|
|
const row = refreshRow;
|
|||
|
|
if (!row) return;
|
|||
|
|
setRefreshRow(null);
|
|||
|
|
fetch("/mcn/api/account/refresh-web", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ id: row.id }),
|
|||
|
|
})
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "提交失败");
|
|||
|
|
setToast(d.message || "已提交刷新,AI 正在重新获取数据");
|
|||
|
|
// AI 处理完成后自动刷新列表
|
|||
|
|
setPendingRefresh(true);
|
|||
|
|
})
|
|||
|
|
.catch((err) => setToast("刷新提交失败: " + err.message));
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// V1.0 对齐:更新所有已关注账号(⭐)——逐账号串行 refresh-web,等待各自完成;进度与汇总提示
|
|||
|
|
const updateFollowedAccounts = () => {
|
|||
|
|
if (updBusy) return;
|
|||
|
|
fetch("/mcn/api/accounts?type=all&page=1&pageSize=100")
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
const favs = ((d.items || [])).filter((a) => a.favorite);
|
|||
|
|
if (!favs.length) { setToast("⭐ 暂无已关注账号(先点击列表中的 ☆ 关注)"); return; }
|
|||
|
|
const names = favs.map((a) => String(a.account_name || "").trim()).filter(Boolean);
|
|||
|
|
if (!names.length) { setToast("已关注账号缺少账号名,无法更新"); return; }
|
|||
|
|
if (!window.confirm("将重新抓取 " + names.length + " 个已关注账号(⭐)的最新账号数据与视频列表(后台逐个执行,每个需数分钟;账号身份标识以本次抓取为准,强制覆盖刷新)。\n账号:" + names.slice(0, 8).join("、") + (names.length > 8 ? " 等" : ""))) return;
|
|||
|
|
setUpdBusy(true);
|
|||
|
|
const done = []; const failed = [];
|
|||
|
|
const loop = (i) => {
|
|||
|
|
if (i >= favs.length) {
|
|||
|
|
setUpdBusy(false); setUpdLabel("");
|
|||
|
|
setToast(done.length ? "✅ 已更新 " + done.length + " 个关注账号" + (failed.length ? ";" + failed.length + " 个未完成:" + failed.join("、") : "") : "❌ 更新未完成:" + failed.join("、"));
|
|||
|
|
load(1, type, search, track);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
const id = favs[i].id;
|
|||
|
|
setUpdLabel("更新中 " + (i + 1) + "/" + favs.length + "…");
|
|||
|
|
fetch("/mcn/api/account/refresh-web", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: id }) })
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d2) => {
|
|||
|
|
if (!d2.ok) { failed.push(names[i] + "(" + (d2.error || "提交失败") + ")"); loop(i + 1); return; }
|
|||
|
|
const deadline = Date.now() + 20 * 60 * 1000;
|
|||
|
|
const wait = () => {
|
|||
|
|
fetch("/mcn/api/task/status")
|
|||
|
|
.then((r2) => r2.json())
|
|||
|
|
.then((st) => {
|
|||
|
|
const ilist = Array.isArray(st.imports) ? st.imports : [];
|
|||
|
|
const t = ilist.find((x) => x.id === "refresh:" + id);
|
|||
|
|
const running = !!(t && t.status === "running");
|
|||
|
|
if (!running || Date.now() > deadline) { done.push(names[i]); loop(i + 1); return; }
|
|||
|
|
setTimeout(wait, 5000);
|
|||
|
|
})
|
|||
|
|
.catch(() => { done.push(names[i]); loop(i + 1); });
|
|||
|
|
};
|
|||
|
|
wait();
|
|||
|
|
})
|
|||
|
|
.catch((e) => { failed.push(names[i] + "(" + e.message + ")"); loop(i + 1); });
|
|||
|
|
};
|
|||
|
|
loop(0);
|
|||
|
|
})
|
|||
|
|
.catch((e) => setToast("获取账号列表失败: " + e.message));
|
|||
|
|
};
|
|||
|
|
// ---- V1.0 同步:批量操作(勾选→连坐删除)/ AI脚本列跳转 / 行内 AI创作(原创选题) ----
|
|||
|
|
const openScriptsOf = (row) => {
|
|||
|
|
try {
|
|||
|
|
if (window.__mcnNav && typeof window.__mcnNav.openView === "function") {
|
|||
|
|
window.__mcnNav.openView("ai-videos", { accountId: row.id, accountName: row.account_name });
|
|||
|
|
} else { setToast("请先打开工作台「AI写脚本」后重试"); }
|
|||
|
|
} catch (e) { /* 导航失败忽略 */ }
|
|||
|
|
};
|
|||
|
|
const toggleSelect = (id) => setSelected((prev) => { const n = Object.assign({}, prev); if (n[id]) delete n[id]; else n[id] = true; return n; });
|
|||
|
|
const pageAllSelected = items.length > 0 && items.every((r) => !!selected[r.id]);
|
|||
|
|
const toggleSelectAll = () => {
|
|||
|
|
const n = Object.assign({}, selected);
|
|||
|
|
if (pageAllSelected) items.forEach((r) => { delete n[r.id]; }); else items.forEach((r) => { n[r.id] = true; });
|
|||
|
|
setSelected(n);
|
|||
|
|
};
|
|||
|
|
const selCount = Object.keys(selected).filter((k) => selected[k]).length;
|
|||
|
|
const exitBatch = () => { setBatchOn(false); setSelected({}); };
|
|||
|
|
const submitBatchDelete = () => {
|
|||
|
|
const ids = Object.keys(selected).filter((k) => selected[k]).map(Number);
|
|||
|
|
if (!ids.length) { setToast("请先勾选要删除的账号"); return; }
|
|||
|
|
if (!window.confirm("将删除所选 " + ids.length + " 个账号及其视频数据、视频解析、账号设定、数据分析、AI脚本与复盘数据,不可恢复。确认删除?")) return;
|
|||
|
|
fetch("/mcn/api/batch/delete", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ kind: "accounts", ids: ids }),
|
|||
|
|
})
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "删除失败");
|
|||
|
|
setToast(d.message || ("已删除 " + d.deleted + " 个账号(含关联解析/复盘/分镜)"));
|
|||
|
|
exitBatch();
|
|||
|
|
load(1, type, search, track);
|
|||
|
|
})
|
|||
|
|
.catch((e) => setToast("删除失败: " + e.message));
|
|||
|
|
};
|
|||
|
|
const openCreate = (row) => { setCreateRow(row); setCreateText(""); };
|
|||
|
|
const submitCreate = () => {
|
|||
|
|
const row = createRow;
|
|||
|
|
if (!row || creating) return;
|
|||
|
|
setCreating(true);
|
|||
|
|
fetch("/mcn/api/rewrite/original", {
|
|||
|
|
method: "POST",
|
|||
|
|
headers: { "Content-Type": "application/json" },
|
|||
|
|
body: JSON.stringify({ accountId: row.id, reqText: createText.trim() }),
|
|||
|
|
})
|
|||
|
|
.then((r) => r.json())
|
|||
|
|
.then((d) => {
|
|||
|
|
if (!d.ok) throw new Error(d.error || "提交失败");
|
|||
|
|
setToast(d.message || "已提交原创选题创作,完成后自动入库到「AI写脚本 → 原创选题创作」");
|
|||
|
|
setCreateRow(null); setCreateText("");
|
|||
|
|
})
|
|||
|
|
.catch((e) => setToast("创作提交失败: " + e.message))
|
|||
|
|
.finally(() => setCreating(false));
|
|||
|
|
};
|
|||
|
|
const toolbar = react.createElement("div", { className: cssMap.toolbar },
|
|||
|
|
react.createElement("button", {
|
|||
|
|
type: "button",
|
|||
|
|
className: cssMap.btn,
|
|||
|
|
title: "返回工作台",
|
|||
|
|
onClick: () => { try { window.__mcnNav && window.__mcnNav.goHome(); } catch (e) {} },
|
|||
|
|
}, "←"),
|
|||
|
|
react.createElement("span", { className: cssMap.title }, "账号列表"),
|
|||
|
|
);
|
|||
|
|
const filterRow = react.createElement("div", { className: cssMap.toolbar, style: { flexWrap: "wrap" } }, react.createElement("select", {
|
|||
|
|
className: cssMap.select,
|
|||
|
|
value: type,
|
|||
|
|
onChange: (e) => onTypeChange(e.target.value),
|
|||
|
|
title: "账号类型",
|
|||
|
|
}, TYPE_OPTIONS.map((o) => react.createElement("option", { value: o.value, key: o.value }, o.label))),
|
|||
|
|
react.createElement("div", { style: { marginLeft: "auto", flex: "1 1 0%", minWidth: 0, display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", justifyContent: "flex-end" } },
|
|||
|
|
react.createElement("input", {
|
|||
|
|
className: cssMap.input,
|
|||
|
|
placeholder: "搜索账号名 / 内容 / 赛道",
|
|||
|
|
value: search,
|
|||
|
|
onChange: (e) => setSearch(e.target.value),
|
|||
|
|
onKeyDown: (e) => { if (e.key === "Enter") onSearch(); },
|
|||
|
|
}),
|
|||
|
|
react.createElement("select", {
|
|||
|
|
className: cssMap.select,
|
|||
|
|
value: track,
|
|||
|
|
onChange: (e) => { setTrack(e.target.value); load(1, type, search, e.target.value); },
|
|||
|
|
}, [react.createElement("option", { value: "", key: "" }, "全部赛道")].concat(tracks.map((t) => react.createElement("option", { value: t, key: t }, t)))),
|
|||
|
|
type !== "signed" && react.createElement("button", {
|
|||
|
|
type: "button",
|
|||
|
|
className: cssMap.btn,
|
|||
|
|
title: importBusy ? "导入分析任务执行中,请等待完成" : "导入外部账号数据(输入文件夹路径)",
|
|||
|
|
onClick: () => setImportDirOpen(true),
|
|||
|
|
disabled: importing || importBusy,
|
|||
|
|
}, importing || importBusy ? "导入执行中…" : react.createElement(react.Fragment, null, react.createElement(IconDownload, {}), "添加")),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, disabled: updBusy, title: updBusy ? "正在更新关注账号…" : "重新抓取所有已关注账号(⭐)的最新账号数据与视频列表(后台任务逐个执行)", onClick: updateFollowedAccounts }, updBusy ? updLabel : react.createElement(IconGlobe, { size: 15 })),
|
|||
|
|
toast ? react.createElement("span", { className: cssMap.pagerInfo }, toast) : null));let tableBody;
|
|||
|
|
if (loading) {
|
|||
|
|
tableBody = react.createElement("div", { className: cssMap.loading }, "加载中…");
|
|||
|
|
} else if (error) {
|
|||
|
|
tableBody = react.createElement("div", { className: cssMap.error }, error);
|
|||
|
|
} else if (items.length === 0) {
|
|||
|
|
tableBody = type === "external"
|
|||
|
|
? react.createElement("div", { className: cssMap.empty }, "暂无外部账号数据")
|
|||
|
|
: react.createElement("div", { className: cssMap.empty }, "暂无数据");
|
|||
|
|
} else {
|
|||
|
|
// 首页排行榜同款“弹性行”排版:左组紧凑靠左、中间弹性空隙、右组紧凑靠右(列宽不摊开)
|
|||
|
|
const C = (al, clip) => ({ padding: "9px 10px", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", textAlign: al || "left", minWidth: 0, whiteSpace: "nowrap", overflow: clip ? "hidden" : undefined, textOverflow: clip ? "ellipsis" : undefined, boxSizing: "border-box" });
|
|||
|
|
const HST = { padding: "9px 10px", textAlign: "center", borderBottom: "1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border))", color: "var(--dsw-alias-label-secondary,#6b7280)", fontWeight: 600, fontSize: 13, whiteSpace: "nowrap" };
|
|||
|
|
const template = batchOn ? "36px 50px 66px 120px 120px 48px minmax(56px,1fr) 68px 70px 62px 52px 66px 110px" : "50px 66px 120px 120px 48px minmax(56px,1fr) 68px 70px 62px 52px 66px 110px";
|
|||
|
|
const headCells = [];
|
|||
|
|
const rowCells = (row) => {
|
|||
|
|
const cells = [];
|
|||
|
|
if (batchOn) cells.push(react.createElement("div", { key: "b", style: C("center") }, react.createElement("input", { type: "checkbox", checked: !!selected[row.id], onChange: () => toggleSelect(row.id) })));
|
|||
|
|
cells.push(
|
|||
|
|
react.createElement("div", { key: "f", style: C("center") }, react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnSm, title: row.favorite ? "取消收藏(点击后从置顶移除)" : "收藏该账号(收藏的账号排在列表最前,按粉丝数降序)", onClick: () => toggleFavorite(row), style: row.favorite ? { color: "#f5b301", borderColor: "#f5b30166" } : { color: "var(--dsw-alias-label-secondary,#8a8f98)" } }, row.favorite ? "★" : "☆")),
|
|||
|
|
react.createElement("div", { key: "ty", style: C("left") }, typeBadge(row.account_type)),
|
|||
|
|
react.createElement("div", { key: "nm", style: Object.assign(C("left"), { whiteSpace: "normal", wordBreak: "break-all" }) }, react.createElement("span", { className: cssMap.link, title: "查看账号详情(账号信息 + 视频列表)", style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)", fontWeight: 600 }, onClick: () => window.__mcnNav?.open("douyin-account-detail", { accountId: row.id, accountName: row.account_name }) }, row.account_name)),
|
|||
|
|
react.createElement("div", { key: "tr", style: Object.assign(C("left"), { whiteSpace: "normal", wordBreak: "break-all" }) }, row.track || "-"),
|
|||
|
|
react.createElement("div", { key: "ai", style: C("left") }, row.is_ai === "是" ? "是" : "否"),
|
|||
|
|
react.createElement("div", { key: "bj", style: C("left", true) }, row.bio || "-"),
|
|||
|
|
react.createElement("div", { key: "f1", style: C("right") }, row.followers ?? "-"),
|
|||
|
|
react.createElement("div", { key: "f2", style: C("right") }, row.total_likes ?? "-"),
|
|||
|
|
react.createElement("div", { key: "f3", style: C("right") }, row.works_count ?? "-"),
|
|||
|
|
react.createElement("div", { key: "f4", style: C("right") }, row.video_count ?? 0),
|
|||
|
|
react.createElement("div", { key: "as", style: C("center", true) }, row.rewrite_count > 0 ? react.createElement("span", { className: cssMap.link, style: { cursor: "pointer", color: "var(--mcn-brand,#4176e6)" }, title: "该账号 AI 脚本数(参考选题+原创选题),点击进入 AI写脚本 页按账号查看", onClick: () => openScriptsOf(row) }, String(row.rewrite_count) + " 条") : "-"),
|
|||
|
|
react.createElement("div", { key: "op", style: C("center") }, react.createElement("div", { style: { display: "inline-flex", gap: 6, alignItems: "center" } },
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnIcon, title: "编辑账号信息(账号名称不可修改)", onClick: () => openEdit(row) }, react.createElement(IconEdit, { size: 14 })),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnIcon, title: "AI创作(原创选题:基于该账号设定创作全新脚本,完成后入库「AI写脚本 → 原创选题创作」)", onClick: () => openCreate(row) }, "创作"))));
|
|||
|
|
return cells;
|
|||
|
|
};
|
|||
|
|
const cols = [
|
|||
|
|
{ t: "关注", a: "center", w: 58 }, { t: "类型", a: "left", w: 64 }, { t: "账号名", a: "left", w: 150 }, { t: "赛道", a: "left", w: 92 }, { t: "AI制作", a: "left", w: 74 }, { t: "简介", a: "left", w: 220 }, { t: "粉丝", a: "right", w: 92 }, { t: "获赞", a: "right", w: 96 }, { t: "作品数", a: "right", w: 84 }, { t: "视频数", a: "right", w: 84 }, { t: "AI脚本", a: "center", w: 92 }, { t: "操作", a: "center", w: 150 }
|
|||
|
|
];
|
|||
|
|
if (batchOn) headCells.push(react.createElement("div", { key: "hb", style: HST }, react.createElement("input", { type: "checkbox", checked: pageAllSelected, title: "全选本页", onChange: () => toggleSelectAll() })));
|
|||
|
|
cols.forEach((cc, ci) => { if (!cc.gap) headCells.push(react.createElement("div", { key: "h" + ci, style: { ...HST, textAlign: cc.a } }, cc.t)); else headCells.push(react.createElement("div", { key: "hg" + ci })); });
|
|||
|
|
tableBody = react.createElement("div", { style: { display: "flex", flexDirection: "column", minWidth: 0 } },
|
|||
|
|
react.createElement("div", { style: { display: "grid", gridTemplateColumns: template, alignItems: "center", background: "var(--dsw-alias-bg-layer-1,var(--dsw-alias-bg-elevated))", position: "sticky", top: 0, zIndex: 2 } }, headCells),
|
|||
|
|
items.map((row) => react.createElement("div", { key: row.id, style: { display: "grid", gridTemplateColumns: template, alignItems: "center" } }, rowCells(row))));
|
|||
|
|
}
|
|||
|
|
return react.createElement("main", { className: cssMap.page, "aria-label": "抖音账号" },
|
|||
|
|
react.createElement("div", { className: cssMap.body },
|
|||
|
|
toolbar,
|
|||
|
|
filterRow,
|
|||
|
|
react.createElement("div", { className: cssMap.tableWrap }, tableBody),
|
|||
|
|
react.createElement("div", { className: cssMap.pager },
|
|||
|
|
react.createElement("div", { style: { display: "inline-flex", alignItems: "center", gap: 8, marginRight: 12 } },
|
|||
|
|
!batchOn
|
|||
|
|
? react.createElement("button", { type: "button", className: cssMap.btn, title: "批量操作:勾选多个账号后删除(连坐清理其视频/解析/设定/AI脚本)", onClick: () => { setBatchOn(true); setSelected({}); } }, react.createElement(IconCheckSquare, { size: 16 }))
|
|||
|
|
: react.createElement(react.Fragment, null,
|
|||
|
|
react.createElement("span", { className: cssMap.pagerInfo }, "已选 " + selCount),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, disabled: selCount === 0, style: selCount > 0 ? { borderColor: "#e5484d", color: "#e5484d" } : {}, onClick: submitBatchDelete }, "删除所选"),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, onClick: exitBatch }, "取消")),
|
|||
|
|
),
|
|||
|
|
react.createElement("span", { className: cssMap.pagerTotal }, `共 ${total} 条`),
|
|||
|
|
react.createElement("span", { className: cssMap.pagerInfo }, `第 ${page} / ${totalPages} 页`),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, disabled: page <= 1, onClick: () => load(page - 1) }, "上一页"),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, disabled: page >= totalPages, onClick: () => load(page + 1) }, "下一页"))),
|
|||
|
|
createRow && react.createElement("div", { className: cssMap.mask, onClick: () => setCreateRow(null) },
|
|||
|
|
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
|
|||
|
|
react.createElement("div", { className: cssMap.modalTitle }, "AI创作 · " + createRow.account_name),
|
|||
|
|
react.createElement("div", { className: cssMap.modalText }, "原创选题创作(无原视频):AI 将按「短视频工作台」技能完整流程(S1-S11)基于账号设定与下列创作需求创作全新脚本并自动入库。"),
|
|||
|
|
react.createElement("textarea", { className: cssMap.textarea, rows: 6, style: { width: "100%", boxSizing: "border-box", marginBottom: "12px" }, placeholder: "创作需求(可选):创意 / 创新爆点 / 痛点爽点 / 叙事结构 / 时长等,回车换行。空着则交由 AI 按人设自主决策", value: createText, onChange: (e) => setCreateText(e.target.value) }),
|
|||
|
|
react.createElement("div", { className: cssMap.modalBtns },
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setCreateRow(null) }, "取消"),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, disabled: creating, onClick: submitCreate }, creating ? "提交中…" : "提交创作")),
|
|||
|
|
)),
|
|||
|
|
refreshRow && react.createElement("div", { className: cssMap.mask, onClick: () => setRefreshRow(null) },
|
|||
|
|
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
|
|||
|
|
react.createElement("div", { className: cssMap.modalTitle }, "确认刷新账号数据"),
|
|||
|
|
react.createElement("div", { className: cssMap.modalText }, `将调用 AI 会话(抖音账号分析技能)通过抖音网页重新获取「${refreshRow.account_name}」的账号信息与视频列表,并更新到数据库。确认刷新?`),
|
|||
|
|
react.createElement("div", { className: cssMap.modalBtns },
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setRefreshRow(null) }, "取消"),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: submitRefresh }, "确认刷新")))),
|
|||
|
|
importDirOpen && react.createElement("div", { className: cssMap.mask, onClick: () => setImportDirOpen(false) },
|
|||
|
|
react.createElement("div", { className: cssMap.modal, onClick: (e) => e.stopPropagation() },
|
|||
|
|
react.createElement("div", { className: cssMap.modalTitle }, "导入外部账号数据"),
|
|||
|
|
react.createElement("div", { className: cssMap.modalText }, "方式一:填写抖音账号信息,AI 通过抖音网页获取账号信息与视频列表(写入数据库并保存到本地)。"),
|
|||
|
|
react.createElement("input", {
|
|||
|
|
className: cssMap.input,
|
|||
|
|
style: { width: "100%", boxSizing: "border-box", marginBottom: "8px" },
|
|||
|
|
placeholder: "抖音账号名称(如:王微斯)",
|
|||
|
|
value: importAccName,
|
|||
|
|
onChange: (e) => setImportAccName(e.target.value),
|
|||
|
|
}),
|
|||
|
|
react.createElement("input", {
|
|||
|
|
className: cssMap.input,
|
|||
|
|
style: { width: "100%", boxSizing: "border-box", marginBottom: "8px" },
|
|||
|
|
placeholder: "抖音号(如:1919714646)",
|
|||
|
|
value: importAccDid,
|
|||
|
|
onChange: (e) => setImportAccDid(e.target.value),
|
|||
|
|
}),
|
|||
|
|
react.createElement("input", {
|
|||
|
|
className: cssMap.input,
|
|||
|
|
style: { width: "100%", boxSizing: "border-box", marginBottom: "12px" },
|
|||
|
|
placeholder: "抖音账号地址(如:https://www.douyin.com/user/xxx)",
|
|||
|
|
value: importAccUrl,
|
|||
|
|
onChange: (e) => setImportAccUrl(e.target.value),
|
|||
|
|
}),
|
|||
|
|
react.createElement("div", { className: cssMap.modalText, style: { margin: "10px 0 8px" } }, "方式二:输入本地数据文件夹绝对路径,AI 直接读取目录分析文件结构,确认后导入数据库。"),
|
|||
|
|
react.createElement("input", {
|
|||
|
|
className: cssMap.input,
|
|||
|
|
style: { width: "100%", boxSizing: "border-box", marginBottom: "12px" },
|
|||
|
|
placeholder: "本地文件夹路径(如 C:\\Users\\Administrator\\Desktop\\账号对标)",
|
|||
|
|
value: importDirInput,
|
|||
|
|
onChange: (e) => setImportDirInput(e.target.value),
|
|||
|
|
onKeyDown: (e) => { if (e.key === "Enter") submitImport(); },
|
|||
|
|
}),
|
|||
|
|
react.createElement("div", { className: cssMap.modalBtns },
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setImportDirOpen(false) }, "取消"),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: submitImport, disabled: importing }, importing ? "提交中…" : "提交")))),
|
|||
|
|
editRow && editForm && react.createElement("div", { className: cssMap.mask, onClick: () => setEditRow(null) },
|
|||
|
|
react.createElement("div", { className: cssMap.modal + " " + cssMap.modalEdit, onClick: (e) => e.stopPropagation() },
|
|||
|
|
react.createElement("div", { className: cssMap.modalTitle }, "编辑账号:" + editRow.account_name),
|
|||
|
|
react.createElement("div", { className: cssMap.modalBody },
|
|||
|
|
react.createElement("div", { className: cssMap.formGrid },
|
|||
|
|
editField("赛道", "track"),
|
|||
|
|
react.createElement("label", { className: cssMap.formItem, key: "is_ai" },
|
|||
|
|
react.createElement("span", null, "AI制作"),
|
|||
|
|
react.createElement("select", {
|
|||
|
|
className: cssMap.input,
|
|||
|
|
style: { boxSizing: "border-box", width: "100%" },
|
|||
|
|
value: editForm.is_ai ?? "否",
|
|||
|
|
onChange: (e) => setEditForm({ ...editForm, is_ai: e.target.value }),
|
|||
|
|
},
|
|||
|
|
react.createElement("option", { value: "否", key: "n" }, "否"),
|
|||
|
|
react.createElement("option", { value: "是", key: "y" }, "是"))),
|
|||
|
|
editField("定位/内容", "content", { textarea: true, rows: 4, wide: true }),
|
|||
|
|
editField("抖音号", "douyin_id"),
|
|||
|
|
editField("粉丝", "followers"),
|
|||
|
|
editField("获赞", "total_likes"),
|
|||
|
|
editField("作品数", "works_count", { type: "number" }),
|
|||
|
|
editField("IP属地", "ip_location"),
|
|||
|
|
editField("地区", "location"),
|
|||
|
|
editField("年龄", "age", { type: "number" }),
|
|||
|
|
editField("标签", "tags"),
|
|||
|
|
editField("代表作", "masterpiece"),
|
|||
|
|
editField("简介", "bio", { textarea: true, rows: 4, wide: true }),
|
|||
|
|
)),
|
|||
|
|
react.createElement("div", { className: cssMap.modalBtns },
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn, onClick: () => setEditRow(null) }, "取消"),
|
|||
|
|
react.createElement("button", { type: "button", className: cssMap.btn + " " + cssMap.btnPrimary, onClick: saveEdit, disabled: saving }, saving ? "保存中…" : "保存")))));
|
|||
|
|
}
|
|||
|
|
//#endregion
|
|||
|
|
//#region plugin entry:注册到 MCN 工作台入口注册表
|
|||
|
|
const inject = [];
|
|||
|
|
function apply() {
|
|||
|
|
window.__mcnEntries = window.__mcnEntries || [];
|
|||
|
|
// 幂等:避免插件热重载重复注册
|
|||
|
|
if (!window.__mcnEntries.some((e) => e.id === "douyin-accounts")) {
|
|||
|
|
window.__mcnEntries.push({
|
|||
|
|
id: "douyin-accounts",
|
|||
|
|
icon: IconUsers,
|
|||
|
|
title: "热门账号",
|
|||
|
|
desc: "签约/外部账号分析",
|
|||
|
|
component: AccountsPage,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
//#endregion
|
|||
|
|
exports.apply = apply;
|
|||
|
|
exports.inject = inject;
|
|||
|
|
return module.exports;
|
|||
|
|
}
|
|||
|
|
});
|