闪卡 | 分散推迟 & 释放高优先级卡 & 推迟低优先级卡

先安装番茄工具箱、Query & View 插件、query 挂件

image.png

把下面粘贴覆盖即可

//!js
const query = async () => {
  const dv = Query.DataView(protyle, item, top);

  // ── 1. UI 渲染 ──────────────────────────────────────────────────
  const widget = document.createElement("div");
  widget.innerHTML = `
    <style>
      .card-tool-widget { background-color: var(--b3-theme-surface); padding: 16px; border-radius: 6px; border: 1px solid var(--b3-theme-border); margin-top: 8px; display: flex; flex-direction: column; gap: 16px; }
      .ctw-section { padding: 12px; border: 1px solid var(--b3-theme-border-light); border-radius: 4px; }
      .ctw-title { font-weight: bold; font-size: 1.1em; color: var(--b3-theme-on-surface); margin-bottom: 4px; }
      .ctw-desc { font-size: 0.9em; color: var(--b3-theme-on-surface-light); margin-bottom: 12px; }
      .ctw-controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
      .ctw-controls .b3-button { margin-left: auto; }
      .ctw-status { margin-top: 16px; padding: 6px 10px; border-radius: 4px; font-size: 0.9em; display: none; }
      .ctw-status.is-shown { display: block; }
      .ctw-status--info { background-color: var(--b3-theme-surface-light); color: var(--b3-theme-on-surface); }
      .ctw-status--success { background-color: var(--b3-theme-success); color: var(--b3-theme-on-success); }
      .ctw-status--error { background-color: var(--b3-theme-error); color: var(--b3-theme-on-error); }
    </style>
    <div class="card-tool-widget">
      <div class="ctw-section">
        <div class="ctw-title">全局设置</div>
        <div class="ctw-desc">指定处理范围:已到期及未来 <b>m</b> 天内到期的闪卡。</div>
        <div class="ctw-controls">
          <input id="ctw-m-days" class="b3-text-field" placeholder="m 天内(例如: 7 或 0.5)" style="width: 240px;">
        </div>
      </div>

      <div class="ctw-section">
        <div class="ctw-title">功能一:分散推迟</div>
        <div class="ctw-desc">将上述范围内的卡,分散到接下来 <b>n</b> 天内(步长 <b>k</b>)。</div>
        <div class="ctw-controls">
          <input id="ctw-n-days" class="b3-text-field" placeholder="分散到 n 天" style="width: 160px;">
          <input id="ctw-step" class="b3-text-field" value="1" placeholder="步长 k (天)" style="width: 120px;">
          <select id="ctw-mode" class="b3-select" style="width: 120px;">
            <option value="round">均匀轮转</option>
            <option value="rand">随机分配</option>
          </select>
          <button id="ctw-scatter-btn" class="b3-button b3-button--primary">执行分散</button>
        </div>
      </div>

      <div class="ctw-section">
        <div class="ctw-title">功能二:释放高优先级卡</div>
        <div class="ctw-desc">将上述范围内的卡,若其优先级 ≥ <b>a</b>,则立即设为可复习。</div>
        <div class="ctw-controls">
          <input id="ctw-priority-a" class="b3-text-field" value="55" placeholder="优先级 a (≥51)" style="width: 180px;">
          <button id="ctw-release-btn" class="b3-button b3-button--outline">执行释放</button>
        </div>
      </div>

      <div class="ctw-section">
        <div class="ctw-title">功能三:推迟低优先级卡</div>
        <div class="ctw-desc">将上述范围内的卡,若其优先级 < <b>b</b>,统一推迟 <b>t</b> 天。</div>
        <div class="ctw-controls">
          <input id="ctw-low-priority-b" class="b3-text-field" value="40" placeholder="优先级 b (以下视为低优)" style="width: 180px;">
          <input id="ctw-low-delay-days" class="b3-text-field" value="7" placeholder="推迟 t 天" style="width: 140px;">
          <button id="ctw-low-delay-btn" class="b3-button b3-button--outline">推迟低优卡</button>
        </div>
      </div>

      <div id="ctw-status" class="ctw-status"></div>
    </div>
  `;
  dv.addele(widget);

  const mInput = widget.querySelector("#ctw-m-days");
  const nInput = widget.querySelector("#ctw-n-days");
  const stepInput = widget.querySelector("#ctw-step");
  const modeSelect = widget.querySelector("#ctw-mode");
  const scatterBtn = widget.querySelector("#ctw-scatter-btn");
  const aInput = widget.querySelector("#ctw-priority-a");
  const releaseBtn = widget.querySelector("#ctw-release-btn");
  const lowBInput = widget.querySelector("#ctw-low-priority-b");
  const lowDelayInput = widget.querySelector("#ctw-low-delay-days");
  const lowDelayBtn = widget.querySelector("#ctw-low-delay-btn");
  const statusBar = widget.querySelector("#ctw-status");

  const allControls = [
    mInput, nInput, stepInput, modeSelect,
    scatterBtn, aInput, releaseBtn,
    lowBInput, lowDelayInput, lowDelayBtn,
  ];

  // ── 2. UI 控制与工具函数 ────────────────────────────────────────
  function showStatus(message, type = 'info') {
    statusBar.textContent = message;
    statusBar.className = `ctw-status is-shown ctw-status--${type}`;
    console.log(`[Card Tool] ${message}`);
  }

  function setBusy(isBusy) {
    allControls.forEach(el => el.disabled = isBusy);
    scatterBtn.textContent = isBusy ? "处理中..." : "执行分散";
    releaseBtn.textContent = isBusy ? "处理中..." : "执行释放";
    lowDelayBtn.textContent = isBusy ? "处理中..." : "推迟低优卡";
  }

  function atStartOfDay(d){ const x = new Date(d); x.setHours(0,0,0,0); return x; }
  function addDays(d,n){ const x = new Date(d); x.setTime(x.getTime() + n*24*60*60*1000); return x; }
  const fmtDays = (v)=> (Math.round(v*1000)/1000).toString().replace(/\.?0+$/,"");
  const chunk = (arr, size=200) => Array.from({length: Math.ceil(arr.length/size)}, (_,i)=>arr.slice(i*size,(i+1)*size));

  function readDec(raw, fallback=NaN){
    if (raw == null) return fallback;
    let s = String(raw).trim().replace(/[0-9.,-天日]/g, ch => ({
      '0':'0','1':'1','2':'2','3':'3','4':'4',
      '5':'5','6':'6','7':'7','8':'8','9':'9',
      '.':'.',',':',','-':'-'
    }[ch] || ''));
    const m = s.match(/[-+]?\d+(?:[.,]\d+)?/);
    if (!m) return fallback;
    let num = m[0];
    if (num.includes(",") && !num.includes(".")) num = num.replace(",",".");

    else if (num.includes(",") && num.includes(".")) num = num.replace(/,/g,"");
    const x = parseFloat(num);
    return Number.isFinite(x) ? x : fallback;
  }
  function readInt(raw, fallback=NaN){
    const x = Math.round(readDec(raw, fallback));
    return Number.isFinite(x) ? x : fallback;
  }

  function parseRiffDue(raw){
    if (!raw) return null;
    if (raw instanceof Date) return raw;
    if (typeof raw === "number") return new Date(raw);
    const s = String(raw).trim();
    const m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(s);
    if (m) {
      const [_, Y, Mo, D, H, Mi, S] = m;
      return new Date(+Y, +Mo-1, +D, +H, +Mi, +S);
    }
    const t = Date.parse(s);
    return Number.isFinite(t) ? new Date(t) : null;
  }

  async function siyuanPost(route, data){
    if (tomato_zZmqus5PtYRi?.siyuan?.fetchPost) {
      return await tomato_zZmqus5PtYRi.siyuan.fetchPost(route, data);
    }
    const resp = await fetch(route, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(data || {})
    });
    return await resp.json();
  }

  const resolveBlockId = (card) => card?.riffCard?.blockId || card?.blockId || card?.block?.id || card?.id;
  const resolveCardId  = (card) => card?.riffCard?.id      || card?.riffCard?.cardId || card?.cardId || card?.id;

  const attrCache = new Map(), parentCache = new Map();
  async function batchGetAttrs(ids){
    const ret = await siyuanPost("/api/attr/batchGetBlockAttrs", { ids });
    if (ret?.code !== 0) throw new Error(ret?.msg || "batchGetBlockAttrs 失败");
    for (const [id, attrs] of Object.entries(ret.data || {})) {
      attrCache.set(id, attrs || {});
    }
  }
  async function getBlockAttrs(id){
    if (attrCache.has(id)) return attrCache.get(id);
    const ret = await siyuanPost("/api/attr/getBlockAttrs", { id });
    const attrs = ret?.code === 0 ? (ret.data || {}) : {};
    attrCache.set(id, attrs);
    return attrs;
  }
  async function getParentID(id){
    if (parentCache.has(id)) return parentCache.get(id);
    const pid = (await siyuanPost("/api/block/getBlockInfo", { id }))?.data?.parentID || null;
    parentCache.set(id, pid);
    return pid;
  }

  const PRIORITY_KEYS = ["custom-card-priority", "card-priority", "custom_card_priority", "card_priority"];
  function parsePriorityFromAttrs(attrs){
    if (!attrs) return { value: NaN };
    for (const k of PRIORITY_KEYS){
      if (Object.prototype.hasOwnProperty.call(attrs, k)) {
        const n = Number(String(attrs[k]).trim());
        if (Number.isFinite(n)) return { value: n, key: k, raw: attrs[k] };
      }
    }
    return { value: NaN };
  }

  async function getPriorityFromSelfOrParents(startId, maxUp=24){
    let cur = startId;
    for (let level=0; cur && level<=maxUp; level++){
      const { value } = parsePriorityFromAttrs(await getBlockAttrs(cur));
      if (Number.isFinite(value)) return { value };
      cur = await getParentID(cur);
    }
    return { value: NaN };
  }

  // ── 3. 事件绑定与执行逻辑 ──────────────────────────────────────
  // 功能一:分散推迟(沿用你原来的逻辑)
  scatterBtn.onclick = async () => {
    setBusy(true);
    try {
      showStatus("步骤 1/3: 校验输入参数...", 'info');
      const mDays = readDec(mInput.value);
      const nDays = readDec(nInput.value);
      const stepDays = readDec(stepInput.value, 1);
      const mode = modeSelect.value;
      if (!(mDays > 0)) throw new Error("请输入有效的 m 天数 (>0)");
      if (!(nDays > 0)) throw new Error("请输入有效的 n 天数 (>0)");
      if (!(stepDays > 0)) throw new Error("请输入有效的步长 (>0)");

      showStatus("步骤 2/3: 拉取所有闪卡并筛选...", 'info');
      const all = await tomato_zZmqus5PtYRi.siyuan.getRiffCardsAllFlat();
      const today = atStartOfDay(new Date()), mEnd = addDays(today, mDays);

      const pool = all
        .map(c => ({ c, due: parseRiffDue(c.riffCard?.due) }))
        .filter(x => x.due && x.due < mEnd)   // 包含已到期 + 未来 m 天
        .sort((a,b) => a.due - b.due)
        .map(x => x.c);

      if (pool.length === 0) {
        showStatus(`未来 ${fmtDays(mDays)} 天内及所有已到期卡均为 0,无需分散。`, 'success');
        return;
      }

      showStatus(`步骤 3/3: 找到 ${pool.length} 张卡,开始分散...`, 'info');
      const bucketCount = Math.max(1, Math.ceil(nDays / stepDays));
      const offsets = Array.from({length: bucketCount}, (_,i)=> Math.min((i+1)*stepDays, nDays));
      const buckets = Array.from({length: bucketCount}, () => []);

      if (mode === "rand") {
        for (const card of pool) {
          buckets[Math.floor(Math.random()*bucketCount)].push(card);
        }
      } else {
        for (let i=0;i<pool.length;i++) {
          buckets[i % bucketCount].push(pool[i]);
        }
      }

      let total = 0;
      for (let k=0;k<bucketCount;k++){
        const group = buckets[k];
        if (group.length === 0) continue;
        const days = offsets[k];
        showStatus(`处理第 ${k+1}/${bucketCount} 桶: ${group.length} 张卡推迟 ${fmtDays(days)} 天...`, 'info');
        for (const batch of chunk(group, 200)) {
          await tomato_zZmqus5PtYRi.cardPriorityBox.stopCards(batch, false, days);
        }
        total += group.length;
      }
      showStatus(`完成:共分散推迟 ${total} 张卡(m=${fmtDays(mDays)}, n=${fmtDays(nDays)}, 步长=${fmtDays(stepDays)})`, 'success');
    } catch(e) {
      console.error(e);
      showStatus(`分散推迟出错: ${e.message}`, 'error');
    } finally {
      setBusy(false);
    }
  };

  // 功能二:释放高优先级卡(使用 stopCards,days = 0)
  releaseBtn.onclick = async () => {
    setBusy(true);
    try {
      showStatus("步骤 1/4: 校验输入参数...", 'info');
      const mDays = readDec(mInput.value);
      const a = readInt(aInput.value);
      if (!(mDays > 0)) throw new Error("请先在全局设置中填写 m 天数 (>0)");
      if (!Number.isInteger(a)) throw new Error("请输入正确的优先级阈值 a (整数)");
      if (a < 50) throw new Error("为避免误操作,优先级阈值 a 需 ≥ 51");

      showStatus(`步骤 2/4: 拉取所有闪卡,筛选已到期及未来 ${fmtDays(mDays)} 天内到期的卡...`, 'info');
      const all = await tomato_zZmqus5PtYRi.siyuan.getRiffCardsAllFlat();
      const today = atStartOfDay(new Date()), mEnd = addDays(today, mDays);

      const rows = all
        .map(c => ({
          card: c,
          bid: resolveBlockId(c),
          due: parseRiffDue(c.riffCard?.due)
        }))
        .filter(x => x.bid && x.due && x.due < mEnd);

      if (!rows.length) {
        showStatus(`已到期及未来 ${fmtDays(mDays)} 天内无到期卡可处理。`, 'success');
        return;
      }

      showStatus(`步骤 3/4: 批量获取 ${rows.length} 张卡片属性并按优先级筛选...`, 'info');
      await batchGetAttrs(rows.map(x => x.bid));

      const targetCards = [];
      for (const r of rows) {
        const prInfo = await getPriorityFromSelfOrParents(r.bid, 24);
        if (Number.isFinite(prInfo.value) && prInfo.value >= a) {
          targetCards.push(r.card);
        }
      }

      if (!targetCards.length) {
        showStatus(`指定范围内没有找到优先级 ≥ ${a} 的卡。`, 'success');
        return;
      }

      // 使用番茄工具箱的 stopCards,把高优卡全部“拉回今天”
      for (const part of chunk(targetCards, 300)) {
        await tomato_zZmqus5PtYRi.cardPriorityBox.stopCards(part, false, 0);
      }

      showStatus(`完成:已将 ${targetCards.length} 张高优先级卡释放为“可复习”`, 'success');
    } catch(e) {
      console.error(e);
      showStatus(`释放失败: ${e.message}`, 'error');
    } finally {
      setBusy(false);
    }
  };

  // 功能三:推迟低优先级卡(使用 stopCards,days = t)
  lowDelayBtn.onclick = async () => {
    setBusy(true);
    try {
      showStatus("步骤 1/4: 校验输入参数...", 'info');
      const mDays = readDec(mInput.value);
      const b = readInt(lowBInput.value);
      const delayDays = readDec(lowDelayInput.value);

      if (!(mDays > 0)) throw new Error("请先在全局设置中填写 m 天数 (>0)");
      if (!Number.isInteger(b)) throw new Error("请输入正确的优先级阈值 b (整数)");
      if (!(delayDays > 0)) throw new Error("请填写要推迟的天数 t (>0)");

      showStatus(`步骤 2/4: 拉取所有闪卡,筛选已到期及未来 ${fmtDays(mDays)} 天内到期的卡...`, 'info');
      const all = await tomato_zZmqus5PtYRi.siyuan.getRiffCardsAllFlat();
      const today = atStartOfDay(new Date()), mEnd = addDays(today, mDays);

      const rows = all
        .map(c => ({
          card: c,
          bid: resolveBlockId(c),
          due: parseRiffDue(c.riffCard?.due)
        }))
        .filter(x => x.bid && x.due && x.due < mEnd);

      if (!rows.length) {
        showStatus(`已到期及未来 ${fmtDays(mDays)} 天内无到期卡可处理。`, 'success');
        return;
      }

      showStatus(`步骤 3/4: 批量获取 ${rows.length} 张卡片属性并按优先级筛选低优卡...`, 'info');
      await batchGetAttrs(rows.map(x => x.bid));

      const targetCards = [];
      for (const r of rows) {
        const prInfo = await getPriorityFromSelfOrParents(r.bid, 24);
        // 低优:优先级 < b
        if (Number.isFinite(prInfo.value) && prInfo.value < b) {
          targetCards.push(r.card);
        }
      }

      if (!targetCards.length) {
        showStatus(`指定范围内没有找到优先级 < ${b} 的低优卡。`, 'success');
        return;
      }

      showStatus(`步骤 4/4: 找到 ${targetCards.length} 张低优卡,统一推迟 ${fmtDays(delayDays)} 天...`, 'info');

      for (const part of chunk(targetCards, 200)) {
        await tomato_zZmqus5PtYRi.cardPriorityBox.stopCards(part, false, delayDays);
      }

      showStatus(`完成:已将 ${targetCards.length} 张低优先级卡统一推迟 ${fmtDays(delayDays)} 天`, 'success');
    } catch(e) {
      console.error(e);
      showStatus(`推迟低优卡失败: ${e.message}`, 'error');
    } finally {
      setBusy(false);
    }
  };

  dv.render();
};
return query();

  • 闪卡
    32 引用 • 152 回帖 • 4 关注
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    28442 引用 • 119756 回帖
  • QueryView
    21 引用 • 84 回帖
  • SQL
    134 引用 • 402 回帖 • 3 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...

推荐标签 标签

  • Chrome

    Chrome 又称 Google 浏览器,是一个由谷歌公司开发的网页浏览器。该浏览器是基于其他开源软件所编写,包括 WebKit,目标是提升稳定性、速度和安全性,并创造出简单且有效率的使用者界面。

    63 引用 • 289 回帖 • 1 关注
  • JWT

    JWT(JSON Web Token)是一种用于双方之间传递信息的简洁的、安全的表述性声明规范。JWT 作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以 JSON 的形式安全的传递信息。

    20 引用 • 15 回帖 • 27 关注
  • 阿里巴巴

    阿里巴巴网络技术有限公司(简称:阿里巴巴集团)是以曾担任英语教师的马云为首的 18 人,于 1999 年在中国杭州创立,他们相信互联网能够创造公平的竞争环境,让小企业通过创新与科技扩展业务,并在参与国内或全球市场竞争时处于更有利的位置。

    43 引用 • 221 回帖 • 11 关注
  • DNSPod

    DNSPod 建立于 2006 年 3 月份,是一款免费智能 DNS 产品。 DNSPod 可以为同时有电信、网通、教育网服务器的网站提供智能的解析,让电信用户访问电信的服务器,网通的用户访问网通的服务器,教育网的用户访问教育网的服务器,达到互联互通的效果。

    6 引用 • 26 回帖 • 548 关注
  • 尊园地产

    昆明尊园房地产经纪有限公司,即:Kunming Zunyuan Property Agency Company Limited(简称“尊园地产”)于 2007 年 6 月开始筹备,2007 年 8 月 18 日正式成立,注册资本 200 万元,公司性质为股份经纪有限公司,主营业务为:代租、代售、代办产权过户、办理银行按揭、担保、抵押、评估等。

    1 引用 • 22 回帖 • 838 关注
  • 微信

    腾讯公司 2011 年 1 月 21 日推出的一款手机通讯软件。用户可以通过摇一摇、搜索号码、扫描二维码等添加好友和关注公众平台,同时可以将自己看到的精彩内容分享到微信朋友圈。

    135 引用 • 798 回帖 • 2 关注
  • 游戏

    沉迷游戏伤身,强撸灰飞烟灭。

    188 引用 • 833 回帖 • 2 关注
  • Mac

    Mac 是苹果公司自 1984 年起以“Macintosh”开始开发的个人消费型计算机,如:iMac、Mac mini、Macbook Air、Macbook Pro、Macbook、Mac Pro 等计算机。

    168 引用 • 598 回帖
  • CodeMirror
    2 引用 • 17 回帖 • 197 关注
  • 链滴

    链滴是一个记录生活的地方。

    记录生活,连接点滴

    203 引用 • 4024 回帖
  • 持续集成

    持续集成(Continuous Integration)是一种软件开发实践,即团队开发成员经常集成他们的工作,通过每个成员每天至少集成一次,也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误。

    15 引用 • 7 回帖
  • SSL

    SSL(Secure Sockets Layer 安全套接层),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议。TLS 与 SSL 在传输层对网络连接进行加密。

    70 引用 • 193 回帖 • 404 关注
  • webpack

    webpack 是一个用于前端开发的模块加载器和打包工具,它能把各种资源,例如 JS、CSS(less/sass)、图片等都作为模块来使用和处理。

    43 引用 • 130 回帖 • 259 关注
  • MySQL

    MySQL 是一个关系型数据库管理系统,由瑞典 MySQL AB 公司开发,目前属于 Oracle 公司。MySQL 是最流行的关系型数据库管理系统之一。

    695 引用 • 538 回帖 • 2 关注
  • Outlook
    1 引用 • 5 回帖 • 1 关注
  • CSS

    CSS(Cascading Style Sheet)“层叠样式表”是用于控制网页样式并允许将样式信息与网页内容分离的一种标记性语言。

    200 引用 • 545 回帖
  • Visio
    1 引用 • 2 回帖
  • Ngui

    Ngui 是一个 GUI 的排版显示引擎和跨平台的 GUI 应用程序开发框架,基于
    Node.js / OpenGL。目标是在此基础上开发 GUI 应用程序可拥有开发 WEB 应用般简单与速度同时兼顾 Native 应用程序的性能与体验。

    7 引用 • 9 回帖 • 430 关注
  • 程序员

    程序员是从事程序开发、程序维护的专业人员。

    599 引用 • 3541 回帖
  • RIP

    愿逝者安息!

    8 引用 • 92 回帖 • 429 关注
  • TGIF

    Thank God It's Friday! 感谢老天,总算到星期五啦!

    293 引用 • 4496 回帖 • 688 关注
  • JetBrains

    JetBrains 是一家捷克的软件开发公司,该公司位于捷克的布拉格,并在俄国的圣彼得堡及美国麻州波士顿都设有办公室,该公司最为人所熟知的产品是 Java 编程语言开发撰写时所用的集成开发环境:IntelliJ IDEA

    18 引用 • 54 回帖
  • OkHttp

    OkHttp 是一款 HTTP & HTTP/2 客户端库,专为 Android 和 Java 应用打造。

    16 引用 • 6 回帖 • 99 关注
  • Folo

    Folo 是一个 RSS 阅读和信息聚合应用,整合多种内容源到统一时间线。

    项目地址:https://github.com/RSSNext/Folo

    1 引用 • 3 回帖 • 2 关注
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖
  • 链书

    链书(Chainbook)是 B3log 开源社区提供的区块链纸质书交易平台,通过 B3T 实现共享激励与价值链。可将你的闲置书籍上架到链书,我们共同构建这个全新的交易平台,让闲置书籍继续发挥它的价值。

    链书社

    链书目前已经下线,也许以后还有机会重制。

    14 引用 • 258 回帖
  • SQLServer

    SQL Server 是由 [微软] 开发和推广的关系数据库管理系统(DBMS),它最初是由 微软、Sybase 和 Ashton-Tate 三家公司共同开发的,并于 1988 年推出了第一个 OS/2 版本。

    21 引用 • 31 回帖 • 1 关注